Merge branch 'new-crash-dialog'

Closes #56.
This commit is contained in:
Florian Bruhin 2014-11-02 19:09:57 +01:00
commit 6ede565ffd
4 changed files with 346 additions and 146 deletions

View File

@ -193,7 +193,7 @@ class Application(QApplication):
if data: if data:
# Crashlog exists and has data in it, so something crashed # Crashlog exists and has data in it, so something crashed
# previously. # previously.
self._crashdlg = crash.FatalCrashDialog(data) self._crashdlg = crash.FatalCrashDialog(self._args.debug, data)
self._crashdlg.show() self._crashdlg.show()
else: else:
# There's no log file, so we can use this to display crashes to the # There's no log file, so we can use this to display crashes to the
@ -490,8 +490,8 @@ class Application(QApplication):
except TypeError: except TypeError:
log.destroy.exception("Error while preventing shutdown") log.destroy.exception("Error while preventing shutdown")
QApplication.closeAllWindows() QApplication.closeAllWindows()
self._crashdlg = crash.ExceptionCrashDialog(pages, history, exc, self._crashdlg = crash.ExceptionCrashDialog(
objects) self._args.debug, pages, history, exc, objects)
ret = self._crashdlg.exec_() ret = self._crashdlg.exec_()
if ret == QDialog.Accepted: # restore if ret == QDialog.Accepted: # restore
self.restart(shutdown=False, pages=pages) self.restart(shutdown=False, pages=pages)

View File

@ -147,14 +147,23 @@ def safe_shlex_split(s):
orig_s, i, s)) # pylint: disable=undefined-loop-variable orig_s, i, s)) # pylint: disable=undefined-loop-variable
def pastebin(text): def pastebin(name, title, text, parent=None):
"""Paste the text into a pastebin and return the URL.""" """Paste the text into a pastebin and return the URL.
Args:
name: The username to post as.
title: The post title.
text: The text to post.
parent: The parent paste to reply to.
"""
api_url = 'http://paste.the-compiler.org/api/' api_url = 'http://paste.the-compiler.org/api/'
data = { data = {
'text': text, 'text': text,
'title': "qutebrowser crash", 'title': title,
'name': "qutebrowser", 'name': name,
} }
if parent is not None:
data['reply'] = parent
encoded_data = urllib.parse.urlencode(data).encode('utf-8') encoded_data = urllib.parse.urlencode(data).encode('utf-8')
create_url = urllib.parse.urljoin(api_url, 'create') create_url = urllib.parse.urljoin(api_url, 'create')
headers = { headers = {

View File

@ -22,59 +22,88 @@
"""The dialog which gets shown when qutebrowser crashes.""" """The dialog which gets shown when qutebrowser crashes."""
import sys import sys
import traceback import html
import getpass import getpass
import traceback
import functools
from PyQt5.QtCore import Qt, QSize from PyQt5.QtCore import pyqtSlot, Qt, QSize
from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton, from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
QVBoxLayout, QHBoxLayout) QVBoxLayout, QHBoxLayout, QCheckBox)
from qutebrowser.utils import version, log, utils, objreg from qutebrowser.utils import version, log, utils, objreg
from qutebrowser.widgets.misc import DetailFold
class _CrashDialog(QDialog): class _CrashDialog(QDialog):
"""Dialog which gets shown after there was a crash. """Dialog which gets shown after there was a crash.
Class attributes:
NAME: The kind of condition we report.
Attributes: Attributes:
These are just here to have a static reference to avoid GCing. These are just here to have a static reference to avoid GCing.
_vbox: The main QVBoxLayout _vbox: The main QVBoxLayout
_lbl: The QLabel with the static text _lbl: The QLabel with the static text
_txt: The QTextEdit with the crash information _debug_log: The QTextEdit with the crash information
_hbox: The QHboxLayout containing the buttons _hbox: The QHboxLayout containing the buttons
_url: Pastebin URL QLabel. _url: Pastebin URL QLabel.
_crash_info: A list of tuples with title and crash information. _crash_info: A list of tuples with title and crash information.
Class attributes:
CRASHTEXT: The text to be displayed in the dialog.
""" """
CRASHTEXT = ("Please review and edit the info below, then either submit " NAME = None
"it to <a href='mailto:crash@qutebrowser.org'>"
"crash@qutebrowser.org</a> or click 'Report'.<br/><br/>"
"<i>Note that without your help, I can't fix the bug you "
"encountered. With the report, I most probably will."
"</i><br/><br/>")
def __init__(self, parent=None): def __init__(self, debug, parent=None):
"""Constructor for CrashDialog.""" """Constructor for CrashDialog.
Args:
debug: Whether --debug was given.
"""
super().__init__(parent) super().__init__(parent)
# We don't set WA_DeleteOnClose here as on an exception, we'll get # We don't set WA_DeleteOnClose here as on an exception, we'll get
# closed anyways, and it only could have unintended side-effects. # closed anyways, and it only could have unintended side-effects.
self._crash_info = None self._buttons = []
self._crash_info = []
self._hbox = None self._hbox = None
self._lbl = None self._lbl = None
self._gather_crash_info() self._chk_report = None
self.setWindowTitle("Whoops!") self.setWindowTitle("Whoops!")
self.resize(QSize(800, 600)) self.resize(QSize(640, 600))
self._vbox = QVBoxLayout(self) self._vbox = QVBoxLayout(self)
self._init_text() self._init_text()
self._txt = QTextEdit()
self._txt.setText(self._format_crash_info()) info = QLabel("What were you doing when this crash/bug happened?")
self._vbox.addWidget(self._txt) self._vbox.addWidget(info)
self._url = QLabel() self._info = QTextEdit(tabChangesFocus=True, acceptRichText=False)
self._set_text_flags(self._url) self._info.setPlaceholderText("- Opened http://www.example.com/\n"
self._vbox.addWidget(self._url) "- Switched tabs\n"
"- etc...")
self._vbox.addWidget(self._info, 5)
contact = QLabel("How can I contact you if I need more info?")
self._vbox.addWidget(contact)
self._contact = QTextEdit(tabChangesFocus=True, acceptRichText=False)
self._contact.setPlaceholderText("Github username, mail or IRC")
self._vbox.addWidget(self._contact, 2)
self._vbox.addSpacing(15)
self._debug_log = QTextEdit(tabChangesFocus=True, acceptRichText=False,
lineWrapMode=QTextEdit.NoWrap)
self._debug_log.hide()
info = QLabel("<i>You can edit the log below to remove sensitive "
"information.</i>", wordWrap=True)
info.hide()
self._fold = DetailFold("Show log", self)
self._fold.toggled.connect(self._debug_log.setVisible)
self._fold.toggled.connect(info.setVisible)
if debug:
self._fold.toggle()
self._vbox.addWidget(self._fold)
self._vbox.addWidget(info)
self._vbox.addWidget(self._debug_log, 10)
self._vbox.addSpacing(15)
self._init_checkboxes(debug)
self._init_buttons() self._init_buttons()
def __repr__(self): def __repr__(self):
@ -84,31 +113,33 @@ class _CrashDialog(QDialog):
"""Initialize the main text to be displayed on an exception. """Initialize the main text to be displayed on an exception.
Should be extended by superclass to set the actual text.""" Should be extended by superclass to set the actual text."""
self._lbl = QLabel() self._lbl = QLabel(wordWrap=True, openExternalLinks=True,
self._lbl.setWordWrap(True) textInteractionFlags=Qt.LinksAccessibleByMouse)
self._set_text_flags(self._lbl)
self._vbox.addWidget(self._lbl) self._vbox.addWidget(self._lbl)
def _init_checkboxes(self, debug):
"""Initialize the checkboxes.
Args:
debug: Whether a --debug arg was given.
"""
self._chk_report = QCheckBox("Send a report")
if not debug:
self._chk_report.setChecked(True)
self._vbox.addWidget(self._chk_report)
info_label = QLabel("<i>Note that without your help, I can't fix the "
"bug you encountered.</i>", wordWrap=True)
self._vbox.addWidget(info_label)
def _init_buttons(self): def _init_buttons(self):
"""Initialize the buttons. """Initialize the buttons.
Should be extended by superclass to provide the actual buttons. Should be extended by subclasses to provide the actual buttons.
""" """
self._hbox = QHBoxLayout() self._hbox = QHBoxLayout()
self._vbox.addLayout(self._hbox) self._vbox.addLayout(self._hbox)
self._hbox.addStretch() self._hbox.addStretch()
def _set_text_flags(self, obj):
"""Set text interaction flags of a widget to allow link clicking.
Args:
obj: A QLabel.
"""
obj.setTextInteractionFlags(Qt.TextSelectableByMouse |
Qt.TextSelectableByKeyboard |
Qt.LinksAccessibleByMouse |
Qt.LinksAccessibleByKeyboard)
def _gather_crash_info(self): def _gather_crash_info(self):
"""Gather crash information to display. """Gather crash information to display.
@ -117,14 +148,6 @@ class _CrashDialog(QDialog):
cmdhist: A list with the command history (as strings) cmdhist: A list with the command history (as strings)
exc: An exception tuple (type, value, traceback) exc: An exception tuple (type, value, traceback)
""" """
self._crash_info = [
("How did it happen?", ""),
]
try:
self._crash_info.append(("Contact info",
"User: {}".format(getpass.getuser())))
except Exception:
self._crash_info.append(("Contact info", traceback.format_exc()))
try: try:
self._crash_info.append(("Version info", version.version())) self._crash_info.append(("Version info", version.version()))
except Exception: except Exception:
@ -135,33 +158,60 @@ class _CrashDialog(QDialog):
except Exception: except Exception:
self._crash_info.append(("Config", traceback.format_exc())) self._crash_info.append(("Config", traceback.format_exc()))
def _format_crash_info(self): def _set_crash_info(self):
"""Format the gathered crash info to be displayed. """Set/update the crash info."""
self._crash_info = []
Return: self._gather_crash_info()
The string to display. chunks = []
"""
chunks = ["Please edit this report to remove sensitive info, and add "
"as much info as possible about how it happened.\n"
"If it's okay if I contact you about this bug report, "
"please also add your contact info (Mail/IRC/Jabber)."]
for (header, body) in self._crash_info: for (header, body) in self._crash_info:
if body is not None: if body is not None:
h = '==== {} ===='.format(header) h = '==== {} ===='.format(header)
chunks.append('\n'.join([h, body])) chunks.append('\n'.join([h, body]))
return '\n\n'.join(chunks) text = '\n\n'.join(chunks)
self._debug_log.setText(text)
def pastebin(self): def report(self):
"""Paste the crash info into the pastebin.""" """Paste the crash info into the pastebin."""
lines = []
lines.append("========== Report ==========")
lines.append(self._info.toPlainText())
lines.append("========== Contact ==========")
lines.append(self._contact.toPlainText())
lines.append("========== Debug log ==========")
lines.append(self._debug_log.toPlainText())
text = '\n\n'.join(lines)
try: try:
url = utils.pastebin(self._txt.toPlainText()) user = getpass.getuser()
except Exception as e:
log.misc.exception("Error while getting user")
user = 'unknown'
try:
utils.pastebin(user, "qutebrowser {}".format(self.NAME), text,
parent='90286958') # http://p.cmpl.cc/90286958
except Exception as e: except Exception as e:
log.misc.exception("Error while paste-binning") log.misc.exception("Error while paste-binning")
self._url.setText('Error while reporting: {}: {}'.format( exc_text = '{}: {}'.format(e.__class__.__name__, e)
e.__class__.__name__, e)) error_dlg = ReportErrorDialog(exc_text, text, self)
return error_dlg.exec_()
self._btn_pastebin.setEnabled(False)
self._url.setText("Reported to: <a href='{}'>{}</a>".format(url, url)) @pyqtSlot()
def on_button_clicked(self, button, accept):
"""Report and close dialog if button was clicked."""
button.setText("Reporting...")
for btn in self._buttons:
btn.setEnabled(False)
self.hide()
self.maybe_report()
if accept:
self.accept()
else:
self.reject()
@pyqtSlot()
def maybe_report(self):
"""Report the bug if the user allowed us to."""
if self._chk_report.isChecked():
self.report()
class ExceptionCrashDialog(_CrashDialog): class ExceptionCrashDialog(_CrashDialog):
@ -169,65 +219,85 @@ class ExceptionCrashDialog(_CrashDialog):
"""Dialog which gets shown on an exception. """Dialog which gets shown on an exception.
Attributes: Attributes:
_btn_quit: The quit button _buttons: A list of buttons.
_btn_restore: the restore button
_btn_pastebin: the pastebin button
_pages: A list of lists of the open pages (URLs as strings) _pages: A list of lists of the open pages (URLs as strings)
_cmdhist: A list with the command history (as strings) _cmdhist: A list with the command history (as strings)
_exc: An exception tuple (type, value, traceback) _exc: An exception tuple (type, value, traceback)
_objects: A list of all QObjects as string. _objects: A list of all QObjects as string.
""" """
def __init__(self, pages, cmdhist, exc, objects, parent=None): NAME = 'exception'
def __init__(self, debug, pages, cmdhist, exc, objects, parent=None):
super().__init__(debug, parent)
self._pages = pages self._pages = pages
self._cmdhist = cmdhist self._cmdhist = cmdhist
self._exc = exc self._exc = exc
self._btn_quit = None
self._btn_restore = None
self._btn_pastebin = None
self._objects = objects self._objects = objects
super().__init__(parent)
self.setModal(True) self.setModal(True)
self._set_crash_info()
def _init_text(self): def _init_text(self):
super()._init_text() super()._init_text()
text = ("<b>Argh! qutebrowser crashed unexpectedly.</b><br/><br/>" + text = "<b>Argh! qutebrowser crashed unexpectedly.</b>"
self.CRASHTEXT)
if self._pages:
text += ("You can click 'Restore tabs' after reporting to attempt "
"to reopen your open tabs.")
self._lbl.setText(text) self._lbl.setText(text)
def _init_buttons(self): def _init_buttons(self):
super()._init_buttons() super()._init_buttons()
self._btn_quit = QPushButton() btn_quit = QPushButton("Quit")
self._btn_quit.setText("Quit") btn_quit.clicked.connect(
self._btn_quit.clicked.connect(self.reject) functools.partial(self.on_button_clicked, btn_quit, False))
self._hbox.addWidget(self._btn_quit) self._hbox.addWidget(btn_quit)
if self._pages:
self._btn_restore = QPushButton() btn_restart = QPushButton("Restart", default=True)
self._btn_restore.setText("Restore tabs") btn_restart.clicked.connect(
self._btn_restore.clicked.connect(self.accept) functools.partial(self.on_button_clicked, btn_restart, True))
self._hbox.addWidget(self._btn_restore) self._hbox.addWidget(btn_restart)
self._btn_pastebin = QPushButton()
self._btn_pastebin.setText("Report") self._buttons = [btn_quit, btn_restart]
self._btn_pastebin.clicked.connect(self.pastebin)
self._btn_pastebin.setDefault(True) def _init_checkboxes(self, debug):
self._hbox.addWidget(self._btn_pastebin) """Add checkboxes to send crash report."""
super()._init_checkboxes(debug)
self._chk_log = QCheckBox("Include a debug log and a list of open "
"pages", checked=True)
if debug:
self._chk_log.setChecked(False)
self._chk_log.setEnabled(False)
self._chk_log.toggled.connect(self._set_crash_info)
self._vbox.addWidget(self._chk_log)
info_label = QLabel("<i>This makes it a lot easier to diagnose the "
"crash, but it might contain sensitive "
"information such as which pages you visited "
"or keyboard input.</i>", wordWrap=True)
self._vbox.addWidget(info_label)
self._chk_report.toggled.connect(self.on_chk_report_toggled)
def _gather_crash_info(self): def _gather_crash_info(self):
super()._gather_crash_info()
self._crash_info += [ self._crash_info += [
("Exception", ''.join(traceback.format_exception(*self._exc))), ("Exception", ''.join(traceback.format_exception(*self._exc))),
("Commandline args", ' '.join(sys.argv[1:])),
("Open Pages", '\n\n'.join('\n'.join(e) for e in self._pages)),
("Command history", '\n'.join(self._cmdhist)),
("Objects", self._objects),
] ]
try: if self._chk_log.isChecked():
self._crash_info.append(("Debug log", log.ram_handler.dump_log())) self._crash_info += [
except Exception: ("Commandline args", ' '.join(sys.argv[1:])),
self._crash_info.append(("Debug log", traceback.format_exc())) ("Open Pages", '\n\n'.join('\n'.join(e) for e in self._pages)),
("Command history", '\n'.join(self._cmdhist)),
("Objects", self._objects),
]
try:
self._crash_info.append(
("Debug log", log.ram_handler.dump_log()))
except Exception:
self._crash_info.append(
("Debug log", traceback.format_exc()))
super()._gather_crash_info()
@pyqtSlot()
def on_chk_report_toggled(self):
"""Disable log checkbox if report is disabled."""
is_checked = self._chk_report.isChecked()
self._chk_log.setEnabled(is_checked)
self._chk_log.setChecked(is_checked)
class FatalCrashDialog(_CrashDialog): class FatalCrashDialog(_CrashDialog):
@ -236,40 +306,39 @@ class FatalCrashDialog(_CrashDialog):
Attributes: Attributes:
_log: The log text to display. _log: The log text to display.
_btn_ok: The OK button.
_btn_pastebin: The pastebin button.
""" """
def __init__(self, text, parent=None): NAME = 'segfault'
def __init__(self, debug, text, parent=None):
super().__init__(debug, parent)
self._log = text self._log = text
self._btn_ok = None
self._btn_pastebin = None
super().__init__(parent)
self.setAttribute(Qt.WA_DeleteOnClose) self.setAttribute(Qt.WA_DeleteOnClose)
self._set_crash_info()
def _init_text(self): def _init_text(self):
super()._init_text() super()._init_text()
text = ("<b>qutebrowser was restarted after a fatal crash.</b><br/>" text = ("<b>qutebrowser was restarted after a fatal crash.</b><br/>"
"<br/>" + self.CRASHTEXT) "<br/>Note: Crash reports for fatal crashes sometimes don't "
"contain the information necessary to fix an issue. Please "
"follow the steps in <a href='https://github.com/The-Compiler/"
"qutebrowser/blob/master/doc/stacktrace.asciidoc'>"
"stacktrace.asciidoc</a> to submit a stacktrace.<br/>")
self._lbl.setText(text) self._lbl.setText(text)
def _init_buttons(self): def _init_buttons(self):
super()._init_buttons() super()._init_buttons()
self._btn_ok = QPushButton() btn_ok = QPushButton(text="OK", default=True)
self._btn_ok.setText("OK") btn_ok.clicked.connect(
self._btn_ok.clicked.connect(self.accept) functools.partial(self.on_button_clicked, btn_ok, True))
self._hbox.addWidget(self._btn_ok) self._hbox.addWidget(btn_ok)
self._btn_pastebin = QPushButton() self._buttons = [btn_ok]
self._btn_pastebin.setText("Report")
self._btn_pastebin.clicked.connect(self.pastebin)
self._btn_pastebin.setDefault(True)
self._hbox.addWidget(self._btn_pastebin)
def _gather_crash_info(self): def _gather_crash_info(self):
super()._gather_crash_info()
self._crash_info += [ self._crash_info += [
("Fault log", self._log), ("Fault log", self._log),
] ]
super()._gather_crash_info()
class ReportDialog(_CrashDialog): class ReportDialog(_CrashDialog):
@ -277,40 +346,37 @@ class ReportDialog(_CrashDialog):
"""Dialog which gets shown when the user wants to report an issue by hand. """Dialog which gets shown when the user wants to report an issue by hand.
Attributes: Attributes:
_btn_ok: The OK button.
_btn_pastebin: The pastebin button.
_pages: A list of the open pages (URLs as strings) _pages: A list of the open pages (URLs as strings)
_cmdhist: A list with the command history (as strings) _cmdhist: A list with the command history (as strings)
_objects: A list of all QObjects as string. _objects: A list of all QObjects as string.
""" """
NAME = 'report'
def __init__(self, pages, cmdhist, objects, parent=None): def __init__(self, pages, cmdhist, objects, parent=None):
super().__init__(False, parent)
self.setAttribute(Qt.WA_DeleteOnClose)
self._btn_report = None
self._pages = pages self._pages = pages
self._cmdhist = cmdhist self._cmdhist = cmdhist
self._btn_ok = None
self._btn_pastebin = None
self._objects = objects self._objects = objects
super().__init__(parent) self._set_crash_info()
self.setAttribute(Qt.WA_DeleteOnClose)
def _init_text(self): def _init_text(self):
super()._init_text() super()._init_text()
text = ("Please describe the bug you encountered below, then either " text = "Please describe the bug you encountered below."
"submit it to <a href='mailto:crash@qutebrowser.org'>"
"crash@qutebrowser.org</a> or click 'Report'.")
self._lbl.setText(text) self._lbl.setText(text)
def _init_buttons(self): def _init_buttons(self):
super()._init_buttons() super()._init_buttons()
self._btn_ok = QPushButton() self._btn_report = QPushButton("Report", default=True)
self._btn_ok.setText("OK") self._btn_report.clicked.connect(self.report)
self._btn_ok.clicked.connect(self.accept) self._btn_report.clicked.connect(self.close)
self._hbox.addWidget(self._btn_ok) self._hbox.addWidget(self._btn_report)
self._btn_pastebin = QPushButton()
self._btn_pastebin.setText("Report") def _init_checkboxes(self, _debug):
self._btn_pastebin.clicked.connect(self.pastebin) """We don't want any checkboxes as the user wanted to report."""
self._btn_pastebin.setDefault(True) pass
self._hbox.addWidget(self._btn_pastebin)
def _gather_crash_info(self): def _gather_crash_info(self):
super()._gather_crash_info() super()._gather_crash_info()
@ -324,3 +390,40 @@ class ReportDialog(_CrashDialog):
self._crash_info.append(("Debug log", log.ram_handler.dump_log())) self._crash_info.append(("Debug log", log.ram_handler.dump_log()))
except Exception: except Exception:
self._crash_info.append(("Debug log", traceback.format_exc())) self._crash_info.append(("Debug log", traceback.format_exc()))
@pyqtSlot()
def maybe_report(self):
"""Report the crash.
We don't have a "Send a report" checkbox here because it was a manual
report, which would be pretty useless without this info.
"""
self.report()
class ReportErrorDialog(QDialog):
"""An error dialog shown on unsuccessful reports."""
def __init__(self, exc_text, text, parent=None):
super().__init__(parent)
vbox = QVBoxLayout(self)
label = QLabel("<b>There was an error while reporting the crash</b>:"
"<br/>{}<br/><br/>"
"Please copy the text below and send a mail to "
"<a href='mailto:crash@qutebrowser.org'>"
"crash@qutebrowser.org</a> - Thanks!".format(
html.escape(exc_text)))
vbox.addWidget(label)
txt = QTextEdit(readOnly=True, tabChangesFocus=True,
acceptRichText=False)
txt.setText(text)
txt.selectAll()
vbox.addWidget(txt)
hbox = QHBoxLayout()
hbox.addStretch()
btn = QPushButton("Close")
btn.clicked.connect(self.close)
hbox.addWidget(btn)
vbox.addLayout(hbox)

View File

@ -19,9 +19,10 @@
"""Misc. widgets used at different places.""" """Misc. widgets used at different places."""
from PyQt5.QtCore import pyqtSlot, Qt from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize
from PyQt5.QtWidgets import QLineEdit, QApplication from PyQt5.QtWidgets import (QLineEdit, QApplication, QWidget, QHBoxLayout,
from PyQt5.QtGui import QValidator, QClipboard QLabel, QStyleOption, QStyle)
from PyQt5.QtGui import QValidator, QClipboard, QPainter
from qutebrowser.models import cmdhistory from qutebrowser.models import cmdhistory
from qutebrowser.utils import utils from qutebrowser.utils import utils
@ -135,3 +136,90 @@ class _CommandValidator(QValidator):
return (QValidator.Acceptable, string, pos) return (QValidator.Acceptable, string, pos)
else: else:
return (QValidator.Invalid, string, pos) return (QValidator.Invalid, string, pos)
class DetailFold(QWidget):
"""A "fold" widget with an arrow to show/hide details.
Attributes:
_folded: Whether the widget is currently folded or not.
_hbox: The HBoxLayout the arrow/label are in.
_arrow: The FoldArrow widget.
Signals:
toggled: Emitted when the widget was folded/unfolded.
arg 0: bool, if the contents are currently visible.
"""
toggled = pyqtSignal(bool)
def __init__(self, text, parent=None):
super().__init__(parent)
self._folded = True
self._hbox = QHBoxLayout(self)
self._hbox.setContentsMargins(0, 0, 0, 0)
self._arrow = _FoldArrow()
self._hbox.addWidget(self._arrow)
label = QLabel(text)
self._hbox.addWidget(label)
self._hbox.addStretch()
def toggle(self):
"""Toggle the fold of the widget."""
self._folded = not self._folded
self._arrow.fold(self._folded)
self.toggled.emit(not self._folded)
def mousePressEvent(self, e):
"""Toggle the fold if the widget was pressed.
Args:
e: The QMouseEvent.
"""
if e.button() == Qt.LeftButton:
e.accept()
self.toggle()
else:
super().mousePressEvent(e)
class _FoldArrow(QWidget):
"""The arrow shown for the DetailFold widget.
Attributes:
_folded: Whether the widget is currently folded or not.
"""
def __init__(self, parent=None):
super().__init__(parent)
self._folded = True
def fold(self, folded):
"""Fold/unfold the widget.
Args:
folded: The new desired state.
"""
self._folded = folded
self.update()
def paintEvent(self, _event):
"""Paint the arrow.
Args:
_paint: The QPaintEvent (unused).
"""
opt = QStyleOption()
opt.initFrom(self)
painter = QPainter(self)
if self._folded:
elem = QStyle.PE_IndicatorArrowRight
else:
elem = QStyle.PE_IndicatorArrowDown
self.style().drawPrimitive(elem, opt, painter, self)
def minimumSizeHint(self):
"""Return a sensible size."""
return QSize(8, 8)