1# -*- coding: utf-8 -*- 2# ----------------------------------------------------------------------------- 3# Copyright © Spyder Project Contributors 4# 5# Licensed under the terms of the MIT License 6# (see spyder/__init__.py for details) 7# ----------------------------------------------------------------------------- 8 9"""Report Error Dialog""" 10 11# Standard library imports 12import sys 13 14# Third party imports 15from qtpy.QtWidgets import (QApplication, QCheckBox, QDialog, QHBoxLayout, 16 QLabel, QPlainTextEdit, QPushButton, QVBoxLayout) 17from qtpy.QtCore import Qt, Signal 18 19# Local imports 20from spyder import __project_url__, __trouble_url__ 21from spyder.config.base import _ 22from spyder.config.gui import get_font 23from spyder.utils.qthelpers import restore_keyevent 24from spyder.widgets.sourcecode.codeeditor import CodeEditor 25from spyder.widgets.mixins import BaseEditMixin, TracebackLinksMixin 26from spyder.widgets.sourcecode.base import ConsoleBaseWidget 27 28 29# Minimum number of characters to introduce in the description field 30# before being able to send the report to Github. 31MIN_CHARS = 20 32 33 34class DescriptionWidget(CodeEditor): 35 """Widget to enter error description.""" 36 37 def __init__(self, parent=None): 38 CodeEditor.__init__(self, parent) 39 40 # Editor options 41 self.setup_editor( 42 language='md', 43 color_scheme='Scintilla', 44 linenumbers=False, 45 scrollflagarea=False, 46 wrap=True, 47 edge_line=False, 48 highlight_current_line=False, 49 highlight_current_cell=False, 50 occurrence_highlighting=False, 51 auto_unindent=False) 52 53 # Set font 54 self.set_font(get_font()) 55 56 # Header 57 self.header = ( 58 "### What steps will reproduce the problem?\n\n" 59 "<!--- You can use Markdown here --->\n\n") 60 self.set_text(self.header) 61 self.move_cursor(len(self.header)) 62 self.header_end_pos = self.get_position('eof') 63 64 def remove_text(self): 65 """Remove text.""" 66 self.truncate_selection(self.header_end_pos) 67 self.remove_selected_text() 68 69 def cut(self): 70 """Cut text""" 71 self.truncate_selection(self.header_end_pos) 72 if self.has_selected_text(): 73 CodeEditor.cut(self) 74 75 def keyPressEvent(self, event): 76 """Reimplemented Qt Method to avoid removing the header.""" 77 event, text, key, ctrl, shift = restore_keyevent(event) 78 cursor_position = self.get_position('cursor') 79 80 if cursor_position < self.header_end_pos: 81 self.restrict_cursor_position(self.header_end_pos, 'eof') 82 elif key == Qt.Key_Delete: 83 if self.has_selected_text(): 84 self.remove_text() 85 else: 86 self.stdkey_clear() 87 elif key == Qt.Key_Backspace: 88 if self.has_selected_text(): 89 self.remove_text() 90 elif self.header_end_pos == cursor_position: 91 return 92 else: 93 self.stdkey_backspace() 94 elif key == Qt.Key_X and ctrl: 95 self.cut() 96 else: 97 CodeEditor.keyPressEvent(self, event) 98 99 def contextMenuEvent(self, event): 100 """Reimplemented Qt Method to not show the context menu.""" 101 pass 102 103 104class ShowErrorWidget(TracebackLinksMixin, ConsoleBaseWidget, BaseEditMixin): 105 """Widget to show errors as they appear in the Internal console.""" 106 QT_CLASS = QPlainTextEdit 107 go_to_error = Signal(str) 108 109 def __init__(self, parent=None): 110 ConsoleBaseWidget.__init__(self, parent) 111 BaseEditMixin.__init__(self) 112 TracebackLinksMixin.__init__(self) 113 self.setReadOnly(True) 114 115 116class SpyderErrorDialog(QDialog): 117 """Custom error dialog for error reporting.""" 118 119 def __init__(self, parent=None): 120 QDialog.__init__(self, parent) 121 self.setWindowTitle(_("Spyder internal error")) 122 self.setModal(True) 123 124 # To save the traceback sent to the internal console 125 self.error_traceback = "" 126 127 # Dialog main label 128 self.main_label = QLabel( 129 _("""<b>Spyder has encountered an internal problem</b><hr> 130 Before reporting it, <i>please</i> consult our comprehensive 131 <b><a href=\"{0!s}\">Troubleshooting Guide</a></b> 132 which should help solve most issues, and search for 133 <b><a href=\"{1!s}\">known bugs</a></b> matching your error 134 message or problem description for a quicker solution. 135 <br><br> 136 If you don't find anything, please enter a detailed step-by-step 137 description (in English) of what led up to the problem below. 138 Issue reports without a clear way to reproduce them will be 139 closed.<br><br> 140 Thanks for helping us making Spyder better for everyone! 141 """).format(__trouble_url__, __project_url__)) 142 self.main_label.setOpenExternalLinks(True) 143 self.main_label.setWordWrap(True) 144 self.main_label.setAlignment(Qt.AlignJustify) 145 146 # Field to input the description of the problem 147 self.input_description = DescriptionWidget(self) 148 149 # Only allow to submit to Github if we have a long enough description 150 self.input_description.textChanged.connect(self._description_changed) 151 152 # Widget to show errors 153 self.details = ShowErrorWidget(self) 154 self.details.set_pythonshell_font(get_font()) 155 self.details.hide() 156 157 # Label to show missing chars 158 self.initial_chars = len(self.input_description.toPlainText()) 159 self.chars_label = QLabel(_("Enter at least {} " 160 "characters").format(MIN_CHARS)) 161 162 # Checkbox to dismiss future errors 163 self.dismiss_box = QCheckBox() 164 self.dismiss_box.setText(_("Hide all future errors this session")) 165 166 # Labels layout 167 labels_layout = QHBoxLayout() 168 labels_layout.addWidget(self.chars_label) 169 labels_layout.addWidget(self.dismiss_box, 0, Qt.AlignRight) 170 171 # Dialog buttons 172 self.submit_btn = QPushButton(_('Submit to Github')) 173 self.submit_btn.setEnabled(False) 174 self.submit_btn.clicked.connect(self._submit_to_github) 175 176 self.details_btn = QPushButton(_('Show details')) 177 self.details_btn.clicked.connect(self._show_details) 178 179 self.close_btn = QPushButton(_('Close')) 180 181 # Buttons layout 182 buttons_layout = QHBoxLayout() 183 buttons_layout.addWidget(self.submit_btn) 184 buttons_layout.addWidget(self.details_btn) 185 buttons_layout.addWidget(self.close_btn) 186 187 # Main layout 188 vlayout = QVBoxLayout() 189 vlayout.addWidget(self.main_label) 190 vlayout.addWidget(self.input_description) 191 vlayout.addWidget(self.details) 192 vlayout.addLayout(labels_layout) 193 vlayout.addLayout(buttons_layout) 194 self.setLayout(vlayout) 195 196 self.resize(600, 420) 197 self.input_description.setFocus() 198 199 def _submit_to_github(self): 200 """Action to take when pressing the submit button.""" 201 main = self.parent().main 202 203 # Getting description and traceback 204 description = self.input_description.toPlainText() 205 traceback = self.error_traceback[:-1] # Remove last EOL 206 207 # Render issue 208 issue_text = main.render_issue(description=description, 209 traceback=traceback) 210 211 # Copy issue to clipboard 212 QApplication.clipboard().setText(issue_text) 213 214 # Submit issue to Github 215 issue_body = ("<!--- IMPORTANT: Paste the contents of your clipboard " 216 "here to complete reporting the problem. --->\n\n") 217 main.report_issue(body=issue_body, 218 title="Automatic error report") 219 220 def append_traceback(self, text): 221 """Append text to the traceback, to be displayed in details.""" 222 self.error_traceback += text 223 224 def _show_details(self): 225 """Show traceback on its own dialog""" 226 if self.details.isVisible(): 227 self.details.hide() 228 self.details_btn.setText(_('Show details')) 229 else: 230 self.resize(600, 550) 231 self.details.document().setPlainText('') 232 self.details.append_text_to_shell(self.error_traceback, 233 error=True, 234 prompt=False) 235 self.details.show() 236 self.details_btn.setText(_('Hide details')) 237 238 def _description_changed(self): 239 """Activate submit_btn if we have a long enough description.""" 240 chars = len(self.input_description.toPlainText()) - self.initial_chars 241 if chars < MIN_CHARS: 242 self.chars_label.setText( 243 u"{} {}".format(MIN_CHARS - chars, 244 _("more characters to go..."))) 245 else: 246 self.chars_label.setText(_("Submission enabled; thanks!")) 247 self.submit_btn.setEnabled(chars >= MIN_CHARS) 248 249 250def test(): 251 from spyder.utils.qthelpers import qapplication 252 app = qapplication() 253 dlg = SpyderErrorDialog() 254 dlg.show() 255 sys.exit(dlg.exec_()) 256 257 258if __name__ == "__main__": 259 test() 260