diff --git a/qutebrowser/app.py b/qutebrowser/app.py
index 313a51a0e..da66d45b4 100644
--- a/qutebrowser/app.py
+++ b/qutebrowser/app.py
@@ -193,7 +193,7 @@ class Application(QApplication):
if data:
# Crashlog exists and has data in it, so something crashed
# previously.
- self._crashdlg = crash.FatalCrashDialog(data)
+ self._crashdlg = crash.FatalCrashDialog(self._args.debug, data)
self._crashdlg.show()
else:
# There's no log file, so we can use this to display crashes to the
@@ -490,8 +490,8 @@ class Application(QApplication):
except TypeError:
log.destroy.exception("Error while preventing shutdown")
QApplication.closeAllWindows()
- self._crashdlg = crash.ExceptionCrashDialog(pages, history, exc,
- objects)
+ self._crashdlg = crash.ExceptionCrashDialog(
+ self._args.debug, pages, history, exc, objects)
ret = self._crashdlg.exec_()
if ret == QDialog.Accepted: # restore
self.restart(shutdown=False, pages=pages)
diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py
index 536c0034c..3a96bc09d 100644
--- a/qutebrowser/utils/utils.py
+++ b/qutebrowser/utils/utils.py
@@ -147,14 +147,23 @@ def safe_shlex_split(s):
orig_s, i, s)) # pylint: disable=undefined-loop-variable
-def pastebin(text):
- """Paste the text into a pastebin and return the URL."""
+def pastebin(name, title, text, parent=None):
+ """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/'
data = {
'text': text,
- 'title': "qutebrowser crash",
- 'name': "qutebrowser",
+ 'title': title,
+ 'name': name,
}
+ if parent is not None:
+ data['reply'] = parent
encoded_data = urllib.parse.urlencode(data).encode('utf-8')
create_url = urllib.parse.urljoin(api_url, 'create')
headers = {
diff --git a/qutebrowser/widgets/crash.py b/qutebrowser/widgets/crash.py
index a98791e78..6bf3af973 100644
--- a/qutebrowser/widgets/crash.py
+++ b/qutebrowser/widgets/crash.py
@@ -22,59 +22,88 @@
"""The dialog which gets shown when qutebrowser crashes."""
import sys
-import traceback
+import html
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,
- QVBoxLayout, QHBoxLayout)
+ QVBoxLayout, QHBoxLayout, QCheckBox)
from qutebrowser.utils import version, log, utils, objreg
+from qutebrowser.widgets.misc import DetailFold
class _CrashDialog(QDialog):
"""Dialog which gets shown after there was a crash.
+ Class attributes:
+ NAME: The kind of condition we report.
+
Attributes:
These are just here to have a static reference to avoid GCing.
_vbox: The main QVBoxLayout
_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
_url: Pastebin URL QLabel.
_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 "
- "it to "
- "crash@qutebrowser.org or click 'Report'.
"
- "Note that without your help, I can't fix the bug you "
- "encountered. With the report, I most probably will."
- "
")
+ NAME = None
- def __init__(self, parent=None):
- """Constructor for CrashDialog."""
+ def __init__(self, debug, parent=None):
+ """Constructor for CrashDialog.
+
+ Args:
+ debug: Whether --debug was given.
+ """
super().__init__(parent)
# We don't set WA_DeleteOnClose here as on an exception, we'll get
# closed anyways, and it only could have unintended side-effects.
- self._crash_info = None
+ self._buttons = []
+ self._crash_info = []
self._hbox = None
self._lbl = None
- self._gather_crash_info()
+ self._chk_report = None
self.setWindowTitle("Whoops!")
- self.resize(QSize(800, 600))
+ self.resize(QSize(640, 600))
self._vbox = QVBoxLayout(self)
self._init_text()
- self._txt = QTextEdit()
- self._txt.setText(self._format_crash_info())
- self._vbox.addWidget(self._txt)
- self._url = QLabel()
- self._set_text_flags(self._url)
- self._vbox.addWidget(self._url)
+
+ info = QLabel("What were you doing when this crash/bug happened?")
+ self._vbox.addWidget(info)
+ self._info = QTextEdit(tabChangesFocus=True, acceptRichText=False)
+ self._info.setPlaceholderText("- Opened http://www.example.com/\n"
+ "- 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("You can edit the log below to remove sensitive "
+ "information.", 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()
def __repr__(self):
@@ -84,31 +113,33 @@ class _CrashDialog(QDialog):
"""Initialize the main text to be displayed on an exception.
Should be extended by superclass to set the actual text."""
- self._lbl = QLabel()
- self._lbl.setWordWrap(True)
- self._set_text_flags(self._lbl)
+ self._lbl = QLabel(wordWrap=True, openExternalLinks=True,
+ textInteractionFlags=Qt.LinksAccessibleByMouse)
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("Note that without your help, I can't fix the "
+ "bug you encountered.", wordWrap=True)
+ self._vbox.addWidget(info_label)
+
def _init_buttons(self):
"""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._vbox.addLayout(self._hbox)
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):
"""Gather crash information to display.
@@ -117,14 +148,6 @@ class _CrashDialog(QDialog):
cmdhist: A list with the command history (as strings)
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:
self._crash_info.append(("Version info", version.version()))
except Exception:
@@ -135,33 +158,60 @@ class _CrashDialog(QDialog):
except Exception:
self._crash_info.append(("Config", traceback.format_exc()))
- def _format_crash_info(self):
- """Format the gathered crash info to be displayed.
-
- Return:
- The string to display.
- """
- 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)."]
+ def _set_crash_info(self):
+ """Set/update the crash info."""
+ self._crash_info = []
+ self._gather_crash_info()
+ chunks = []
for (header, body) in self._crash_info:
if body is not None:
h = '==== {} ===='.format(header)
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."""
+ 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:
- 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:
log.misc.exception("Error while paste-binning")
- self._url.setText('Error while reporting: {}: {}'.format(
- e.__class__.__name__, e))
- return
- self._btn_pastebin.setEnabled(False)
- self._url.setText("Reported to: {}".format(url, url))
+ exc_text = '{}: {}'.format(e.__class__.__name__, e)
+ error_dlg = ReportErrorDialog(exc_text, text, self)
+ error_dlg.exec_()
+
+ @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):
@@ -169,65 +219,85 @@ class ExceptionCrashDialog(_CrashDialog):
"""Dialog which gets shown on an exception.
Attributes:
- _btn_quit: The quit button
- _btn_restore: the restore button
- _btn_pastebin: the pastebin button
+ _buttons: A list of buttons.
_pages: A list of lists of the open pages (URLs as strings)
_cmdhist: A list with the command history (as strings)
_exc: An exception tuple (type, value, traceback)
_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._cmdhist = cmdhist
self._exc = exc
- self._btn_quit = None
- self._btn_restore = None
- self._btn_pastebin = None
self._objects = objects
- super().__init__(parent)
self.setModal(True)
+ self._set_crash_info()
def _init_text(self):
super()._init_text()
- text = ("Argh! qutebrowser crashed unexpectedly.
" +
- self.CRASHTEXT)
- if self._pages:
- text += ("You can click 'Restore tabs' after reporting to attempt "
- "to reopen your open tabs.")
+ text = "Argh! qutebrowser crashed unexpectedly."
self._lbl.setText(text)
def _init_buttons(self):
super()._init_buttons()
- self._btn_quit = QPushButton()
- self._btn_quit.setText("Quit")
- self._btn_quit.clicked.connect(self.reject)
- self._hbox.addWidget(self._btn_quit)
- if self._pages:
- self._btn_restore = QPushButton()
- self._btn_restore.setText("Restore tabs")
- self._btn_restore.clicked.connect(self.accept)
- self._hbox.addWidget(self._btn_restore)
- self._btn_pastebin = QPushButton()
- self._btn_pastebin.setText("Report")
- self._btn_pastebin.clicked.connect(self.pastebin)
- self._btn_pastebin.setDefault(True)
- self._hbox.addWidget(self._btn_pastebin)
+ btn_quit = QPushButton("Quit")
+ btn_quit.clicked.connect(
+ functools.partial(self.on_button_clicked, btn_quit, False))
+ self._hbox.addWidget(btn_quit)
+
+ btn_restart = QPushButton("Restart", default=True)
+ btn_restart.clicked.connect(
+ functools.partial(self.on_button_clicked, btn_restart, True))
+ self._hbox.addWidget(btn_restart)
+
+ self._buttons = [btn_quit, btn_restart]
+
+ def _init_checkboxes(self, debug):
+ """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("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.", wordWrap=True)
+ self._vbox.addWidget(info_label)
+ self._chk_report.toggled.connect(self.on_chk_report_toggled)
def _gather_crash_info(self):
- super()._gather_crash_info()
self._crash_info += [
("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:
- self._crash_info.append(("Debug log", log.ram_handler.dump_log()))
- except Exception:
- self._crash_info.append(("Debug log", traceback.format_exc()))
+ if self._chk_log.isChecked():
+ self._crash_info += [
+ ("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:
+ 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):
@@ -236,40 +306,39 @@ class FatalCrashDialog(_CrashDialog):
Attributes:
_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._btn_ok = None
- self._btn_pastebin = None
- super().__init__(parent)
self.setAttribute(Qt.WA_DeleteOnClose)
+ self._set_crash_info()
def _init_text(self):
super()._init_text()
text = ("qutebrowser was restarted after a fatal crash.
"
- "
" + self.CRASHTEXT)
+ "
Note: Crash reports for fatal crashes sometimes don't "
+ "contain the information necessary to fix an issue. Please "
+ "follow the steps in "
+ "stacktrace.asciidoc to submit a stacktrace.
")
self._lbl.setText(text)
def _init_buttons(self):
super()._init_buttons()
- self._btn_ok = QPushButton()
- self._btn_ok.setText("OK")
- self._btn_ok.clicked.connect(self.accept)
- self._hbox.addWidget(self._btn_ok)
- self._btn_pastebin = QPushButton()
- self._btn_pastebin.setText("Report")
- self._btn_pastebin.clicked.connect(self.pastebin)
- self._btn_pastebin.setDefault(True)
- self._hbox.addWidget(self._btn_pastebin)
+ btn_ok = QPushButton(text="OK", default=True)
+ btn_ok.clicked.connect(
+ functools.partial(self.on_button_clicked, btn_ok, True))
+ self._hbox.addWidget(btn_ok)
+ self._buttons = [btn_ok]
def _gather_crash_info(self):
- super()._gather_crash_info()
self._crash_info += [
("Fault log", self._log),
]
+ super()._gather_crash_info()
class ReportDialog(_CrashDialog):
@@ -277,40 +346,37 @@ class ReportDialog(_CrashDialog):
"""Dialog which gets shown when the user wants to report an issue by hand.
Attributes:
- _btn_ok: The OK button.
- _btn_pastebin: The pastebin button.
_pages: A list of the open pages (URLs as strings)
_cmdhist: A list with the command history (as strings)
_objects: A list of all QObjects as string.
"""
+ NAME = 'report'
+
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._cmdhist = cmdhist
- self._btn_ok = None
- self._btn_pastebin = None
self._objects = objects
- super().__init__(parent)
- self.setAttribute(Qt.WA_DeleteOnClose)
+ self._set_crash_info()
def _init_text(self):
super()._init_text()
- text = ("Please describe the bug you encountered below, then either "
- "submit it to "
- "crash@qutebrowser.org or click 'Report'.")
+ text = "Please describe the bug you encountered below."
self._lbl.setText(text)
def _init_buttons(self):
super()._init_buttons()
- self._btn_ok = QPushButton()
- self._btn_ok.setText("OK")
- self._btn_ok.clicked.connect(self.accept)
- self._hbox.addWidget(self._btn_ok)
- self._btn_pastebin = QPushButton()
- self._btn_pastebin.setText("Report")
- self._btn_pastebin.clicked.connect(self.pastebin)
- self._btn_pastebin.setDefault(True)
- self._hbox.addWidget(self._btn_pastebin)
+ self._btn_report = QPushButton("Report", default=True)
+ self._btn_report.clicked.connect(self.report)
+ self._btn_report.clicked.connect(self.close)
+ self._hbox.addWidget(self._btn_report)
+
+ def _init_checkboxes(self, _debug):
+ """We don't want any checkboxes as the user wanted to report."""
+ pass
def _gather_crash_info(self):
super()._gather_crash_info()
@@ -324,3 +390,40 @@ class ReportDialog(_CrashDialog):
self._crash_info.append(("Debug log", log.ram_handler.dump_log()))
except Exception:
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("There was an error while reporting the crash:"
+ "
{}
"
+ "Please copy the text below and send a mail to "
+ ""
+ "crash@qutebrowser.org - 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)
diff --git a/qutebrowser/widgets/misc.py b/qutebrowser/widgets/misc.py
index b212ffa78..910d1c6d3 100644
--- a/qutebrowser/widgets/misc.py
+++ b/qutebrowser/widgets/misc.py
@@ -19,9 +19,10 @@
"""Misc. widgets used at different places."""
-from PyQt5.QtCore import pyqtSlot, Qt
-from PyQt5.QtWidgets import QLineEdit, QApplication
-from PyQt5.QtGui import QValidator, QClipboard
+from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize
+from PyQt5.QtWidgets import (QLineEdit, QApplication, QWidget, QHBoxLayout,
+ QLabel, QStyleOption, QStyle)
+from PyQt5.QtGui import QValidator, QClipboard, QPainter
from qutebrowser.models import cmdhistory
from qutebrowser.utils import utils
@@ -135,3 +136,90 @@ class _CommandValidator(QValidator):
return (QValidator.Acceptable, string, pos)
else:
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)