File: errorDlg.py

package info (click to toggle)
psychopy 2020.2.10%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 44,056 kB
  • sloc: python: 119,649; javascript: 3,022; makefile: 148; sh: 125; xml: 9
file content (223 lines) | stat: -rw-r--r-- 8,507 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Part of the PsychoPy library
# Copyright (C) 2002-2018 Jonathan Peirce (C) 2019-2020 Open Science Tools Ltd.
# Distributed under the terms of the GNU General Public License (GPL).

"""Error dialog for showing unhandled exceptions that occur within the PsychoPy
app."""

import wx
import traceback
import psychopy.preferences
import sys

_error_dlg = None  # keep error dialogs from stacking


class ErrorMsgDialog(wx.Dialog):
    """Class for creating an error report dialog. Should never be created
    directly.
    """
    def __init__(self, parent, traceback=''):
        wx.Dialog.__init__(self, parent, id=wx.ID_ANY, title=u"PsychoPy3 Error",
                           pos=wx.DefaultPosition, size=wx.Size(750, -1),
                           style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)

        self.details = traceback

        # message to show at the top of the error box, needs translation
        msg = u"PsychoPy encountered an unhandled internal error! " \
              u"Please send the report under \"Details\" to the " \
              u"developers with a description of what you were doing " \
              u"with the software when the error occurred."

        self.SetSizeHints(wx.DefaultSize, wx.DefaultSize)
        szErrorMsg = wx.BoxSizer(wx.VERTICAL)
        szHeader = wx.FlexGridSizer(0, 3, 0, 0)
        szHeader.AddGrowableCol(1)
        szHeader.SetFlexibleDirection(wx.BOTH)
        szHeader.SetNonFlexibleGrowMode(wx.FLEX_GROWMODE_SPECIFIED)
        self.imgErrorIcon = wx.StaticBitmap(
            self, wx.ID_ANY, wx.ArtProvider.GetBitmap(
                wx.ART_ERROR, wx.ART_MESSAGE_BOX),
            wx.DefaultPosition, wx.DefaultSize, 0)
        szHeader.Add(self.imgErrorIcon, 0, wx.ALL, 5)
        self.lblErrorMsg = wx.StaticText(
            self, wx.ID_ANY, msg, wx.DefaultPosition, wx.DefaultSize, 0)
        self.lblErrorMsg.Wrap(560)
        szHeader.Add(self.lblErrorMsg, 0, wx.ALL, 5)
        szHeaderButtons = wx.BoxSizer(wx.VERTICAL)
        self.cmdOK = wx.Button(
            self, wx.ID_OK, u"&OK", wx.DefaultPosition, wx.DefaultSize, 0)
        szHeaderButtons.Add(self.cmdOK, 0, wx.LEFT | wx.EXPAND, 5)
        self.cmdExit = wx.Button(
            self, wx.ID_EXIT, u"E&xit PsychoPy", wx.DefaultPosition,
            wx.DefaultSize, 0)
        szHeaderButtons.Add(self.cmdExit, 0, wx.TOP | wx.LEFT | wx.EXPAND, 5)
        szHeader.Add(szHeaderButtons, 0, wx.ALL | wx.EXPAND, 5)
        szErrorMsg.Add(szHeader, 0, wx.TOP | wx.LEFT | wx.RIGHT | wx.EXPAND, 5)

        self.pnlDetails = wx.CollapsiblePane(
            self, wx.ID_ANY, u"&Details", wx.DefaultPosition, wx.DefaultSize,
            wx.CP_DEFAULT_STYLE)
        self.pnlDetails.Collapse(True)
        szDetailsPane = wx.BoxSizer(wx.VERTICAL)
        self.txtErrorOutput = wx.TextCtrl(
            self.pnlDetails.GetPane(), wx.ID_ANY, self.details,
            wx.DefaultPosition, wx.Size(640, 150),
            wx.TE_AUTO_URL | wx.TE_BESTWRAP | wx.TE_MULTILINE | wx.TE_READONLY |
            wx.TE_WORDWRAP)
        szDetailsPane.Add(self.txtErrorOutput, 1, wx.ALL | wx.EXPAND, 5)
        szTextButtons = wx.BoxSizer(wx.HORIZONTAL)
        self.cmdCopyError = wx.Button(
            self.pnlDetails.GetPane(), wx.ID_ANY, u"&Copy", wx.DefaultPosition,
            wx.DefaultSize, 0)
        szTextButtons.Add(self.cmdCopyError, 0, wx.RIGHT, 5)
        self.cmdSaveError = wx.Button(
            self.pnlDetails.GetPane(), wx.ID_ANY, u"&Save", wx.DefaultPosition,
            wx.DefaultSize, 0)
        szTextButtons.Add(self.cmdSaveError, 0)
        szDetailsPane.Add(szTextButtons, 0, wx.ALL | wx.ALIGN_RIGHT, 5)
        self.pnlDetails.Expand()
        self.pnlDetails.GetPane().SetSizer(szDetailsPane)
        self.pnlDetails.GetPane().Layout()
        szDetailsPane.Fit(self.pnlDetails.GetPane())
        szErrorMsg.Add(self.pnlDetails, 1, wx.ALL | wx.BOTTOM | wx.EXPAND, 5)

        self.SetSizer(szErrorMsg)
        self.Layout()
        self.Fit()

        self.Centre(wx.BOTH)

        # Connect Events
        self.cmdOK.Bind(wx.EVT_BUTTON, self.onOkay)
        self.cmdExit.Bind(wx.EVT_BUTTON, self.onExit)
        self.cmdCopyError.Bind(wx.EVT_BUTTON, self.onCopyDetails)
        self.cmdSaveError.Bind(wx.EVT_BUTTON, self.onSaveDetails)

        # ding!
        wx.Bell()

    def __del__(self):
        pass

    def onOkay(self, event):
        """Called when OK is clicked."""
        event.Skip()

    def onExit(self, event):
        """Called when the user requests to close PsychoPy. This can be called
        if the error if unrecoverable or if the errors are being constantly
        generated.

        Will try to close things safely, allowing the user to save files while
        suppressing further errors.

        """
        dlg = wx.MessageDialog(
            self,
            "Are you sure you want to exit PsychoPy? Unsaved work may be lost "
            "(but we'll try to save opened files).",
            "Exit PsychoPy?",
            wx.YES_NO | wx.NO_DEFAULT | wx.ICON_WARNING | wx.CENTRE)
        if dlg.ShowModal() == wx.ID_YES:
            wx.GetApp().quit()
            # wx.Exit()  # nuclear option
        else:
            dlg.Destroy()
            event.Skip()

    def onCopyDetails(self, event):
        """Copy the contents of the details text box to the clipboard. This is
        to allow the user to paste the traceback into an email, forum post,
        issue ticket, etc. to report the error to the developers. If there is a
        selection range, only that text will be copied.

        """
        # check if we have a selection
        start, end = self.txtErrorOutput.GetSelection()
        if start != end:
            txt = self.txtErrorOutput.GetStringSelection()
        else:
            txt = self.txtErrorOutput.GetValue()

        if wx.TheClipboard.Open():
            wx.TheClipboard.SetData(wx.TextDataObject(txt))
            wx.TheClipboard.Close()

        event.Skip()

    def onSaveDetails(self, event):
        """Dump the traceback data to a file. This can be used to save the error
        data so it can be reported to the developers at a later time. Brings up
        a file save dialog to select where to write the file.

        """
        with wx.FileDialog(
                self, "Save error traceback",
                wildcard="Text files (*.txt)|*.txt",
                defaultFile='psychopy_traceback.txt',
                style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as fileDialog:

            if fileDialog.ShowModal() == wx.ID_CANCEL:
                return  # the user changed their mind

            # dump traceback to file
            pathname = fileDialog.GetPath()
            try:
                with open(pathname, 'w') as file:
                    file.write(self.txtErrorOutput.GetValue())
            except IOError:
                # error in an error ... ;)
                errdlg = wx.MessageDialog(
                    self,
                    "Cannot save to file '%s'." % pathname,
                    "File save error",
                    wx.OK_DEFAULT | wx.ICON_ERROR | wx.CENTRE)
                errdlg.ShowModal()
                errdlg.Destroy()

        event.Skip()


def isErrorDialogVisible():
    """Check if the error dialog is open. This can be used to prevent background
    routines from running while the user deals with an error.

    Returns
    -------
    bool
        Error dialog is currently active.

    """
    return _error_dlg is not None


def exceptionCallback(exc_type, exc_value, exc_traceback):
    """Hook when an unhandled exception is raised within the current application
    thread. Gets the exception message and creates an error dialog box.

    When this function is patched into `sys.excepthook`, all unhandled
    exceptions will result in a dialog being displayed.

    """
    if psychopy.preferences.prefs.app['errorDialog'] is False:
        # have the error go out to stdout if dialogs are disabled
        traceback.print_exception(
            exc_type, exc_value, exc_traceback, file=sys.stdout)
        return

    global _error_dlg
    if not isErrorDialogVisible():
        # format the traceback text
        tbText = ''.join(traceback.format_exception(
            exc_type, exc_value, exc_traceback))
        _error_dlg = ErrorMsgDialog(None, tbText)

        # show the dialog
        _error_dlg.ShowModal()
        _error_dlg.Destroy()
        _error_dlg = None