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