PrEditor 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- preditor/__init__.py +315 -0
- preditor/__main__.py +13 -0
- preditor/about_module.py +165 -0
- preditor/cli.py +192 -0
- preditor/config.py +318 -0
- preditor/constants.py +13 -0
- preditor/contexts.py +210 -0
- preditor/cores/__init__.py +0 -0
- preditor/cores/core.py +20 -0
- preditor/dccs/.hab.json +10 -0
- preditor/dccs/maya/PrEditor_maya.mod +1 -0
- preditor/dccs/maya/README.md +22 -0
- preditor/dccs/maya/plug-ins/PrEditor_maya.py +141 -0
- preditor/dccs/studiomax/PackageContents.xml +32 -0
- preditor/dccs/studiomax/PrEditor-PrEditor_Show.mcr +8 -0
- preditor/dccs/studiomax/README.md +17 -0
- preditor/dccs/studiomax/preditor.ms +16 -0
- preditor/dccs/studiomax/preditor_menu.mnx +7 -0
- preditor/debug.py +149 -0
- preditor/delayable_engine/__init__.py +302 -0
- preditor/delayable_engine/delayables.py +85 -0
- preditor/enum.py +728 -0
- preditor/excepthooks.py +165 -0
- preditor/gui/__init__.py +56 -0
- preditor/gui/app.py +163 -0
- preditor/gui/codehighlighter.py +289 -0
- preditor/gui/completer.py +237 -0
- preditor/gui/console.py +605 -0
- preditor/gui/console_base.py +911 -0
- preditor/gui/dialog.py +181 -0
- preditor/gui/drag_tab_bar.py +625 -0
- preditor/gui/editor_chooser.py +57 -0
- preditor/gui/errordialog.py +69 -0
- preditor/gui/find_files.py +137 -0
- preditor/gui/fuzzy_search/__init__.py +0 -0
- preditor/gui/fuzzy_search/fuzzy_search.py +97 -0
- preditor/gui/group_tab_widget/__init__.py +0 -0
- preditor/gui/group_tab_widget/group_tab_widget.py +528 -0
- preditor/gui/group_tab_widget/grouped_tab_menu.py +35 -0
- preditor/gui/group_tab_widget/grouped_tab_models.py +107 -0
- preditor/gui/group_tab_widget/grouped_tab_widget.py +223 -0
- preditor/gui/group_tab_widget/one_tab_widget.py +96 -0
- preditor/gui/level_buttons.py +358 -0
- preditor/gui/logger_window_handler.py +77 -0
- preditor/gui/logger_window_plugin.py +35 -0
- preditor/gui/loggerwindow.py +2405 -0
- preditor/gui/newtabwidget.py +69 -0
- preditor/gui/output_console.py +11 -0
- preditor/gui/qtdesigner/__init__.py +21 -0
- preditor/gui/qtdesigner/_log_plugin.py +29 -0
- preditor/gui/qtdesigner/console_base_plugin.py +48 -0
- preditor/gui/qtdesigner/console_predit_plugin.py +48 -0
- preditor/gui/set_text_editor_path_dialog.py +61 -0
- preditor/gui/status_label.py +99 -0
- preditor/gui/suggest_path_quotes_dialog.py +50 -0
- preditor/gui/ui/editor_chooser.ui +93 -0
- preditor/gui/ui/errordialog.ui +74 -0
- preditor/gui/ui/find_files.ui +140 -0
- preditor/gui/ui/loggerwindow.ui +1909 -0
- preditor/gui/ui/set_text_editor_path_dialog.ui +189 -0
- preditor/gui/ui/suggest_path_quotes_dialog.ui +225 -0
- preditor/gui/window.py +161 -0
- preditor/gui/workbox_mixin.py +1139 -0
- preditor/gui/workbox_text_edit.py +136 -0
- preditor/gui/workboxwidget.py +315 -0
- preditor/logging_config.py +55 -0
- preditor/osystem.py +401 -0
- preditor/plugins.py +118 -0
- preditor/prefs.py +381 -0
- preditor/resource/environment_variables.html +26 -0
- preditor/resource/error_mail.html +85 -0
- preditor/resource/error_mail_inline.html +41 -0
- preditor/resource/img/README.md +17 -0
- preditor/resource/img/arrow_forward.png +0 -0
- preditor/resource/img/check-bold.png +0 -0
- preditor/resource/img/chevron-down.png +0 -0
- preditor/resource/img/chevron-up.png +0 -0
- preditor/resource/img/close-thick.png +0 -0
- preditor/resource/img/comment-edit.png +0 -0
- preditor/resource/img/content-copy.png +0 -0
- preditor/resource/img/content-cut.png +0 -0
- preditor/resource/img/content-duplicate.png +0 -0
- preditor/resource/img/content-paste.png +0 -0
- preditor/resource/img/content-save.png +0 -0
- preditor/resource/img/debug_disabled.png +0 -0
- preditor/resource/img/eye-check.png +0 -0
- preditor/resource/img/file-plus.png +0 -0
- preditor/resource/img/file-remove.png +0 -0
- preditor/resource/img/format-align-left.png +0 -0
- preditor/resource/img/format-letter-case-lower.png +0 -0
- preditor/resource/img/format-letter-case-upper.png +0 -0
- preditor/resource/img/format-letter-case.svg +1 -0
- preditor/resource/img/information.png +0 -0
- preditor/resource/img/logging_critical.png +0 -0
- preditor/resource/img/logging_custom.png +0 -0
- preditor/resource/img/logging_debug.png +0 -0
- preditor/resource/img/logging_error.png +0 -0
- preditor/resource/img/logging_info.png +0 -0
- preditor/resource/img/logging_not_set.png +0 -0
- preditor/resource/img/logging_warning.png +0 -0
- preditor/resource/img/marker.png +0 -0
- preditor/resource/img/play.png +0 -0
- preditor/resource/img/playlist-play.png +0 -0
- preditor/resource/img/plus-minus-variant.png +0 -0
- preditor/resource/img/preditor.ico +0 -0
- preditor/resource/img/preditor.png +0 -0
- preditor/resource/img/preditor.psd +0 -0
- preditor/resource/img/preditor.svg +44 -0
- preditor/resource/img/regex.svg +1 -0
- preditor/resource/img/restart.svg +1 -0
- preditor/resource/img/skip-forward-outline.png +0 -0
- preditor/resource/img/skip-next-outline.png +0 -0
- preditor/resource/img/skip-next.png +0 -0
- preditor/resource/img/skip-previous.png +0 -0
- preditor/resource/img/subdirectory-arrow-right.png +0 -0
- preditor/resource/img/text-search-variant.png +0 -0
- preditor/resource/img/warning-big.png +0 -0
- preditor/resource/lang/python.json +30 -0
- preditor/resource/pref_updates/pref_updates.json +17 -0
- preditor/resource/settings.ini +25 -0
- preditor/resource/stylesheet/Bright.css +76 -0
- preditor/resource/stylesheet/Dark.css +210 -0
- preditor/scintilla/__init__.py +40 -0
- preditor/scintilla/delayables/__init__.py +11 -0
- preditor/scintilla/delayables/smart_highlight.py +97 -0
- preditor/scintilla/delayables/spell_check.py +174 -0
- preditor/scintilla/documenteditor.py +1924 -0
- preditor/scintilla/finddialog.py +68 -0
- preditor/scintilla/lang/__init__.py +80 -0
- preditor/scintilla/lang/config/bash.ini +15 -0
- preditor/scintilla/lang/config/batch.ini +14 -0
- preditor/scintilla/lang/config/cpp.ini +19 -0
- preditor/scintilla/lang/config/css.ini +19 -0
- preditor/scintilla/lang/config/eyeonscript.ini +17 -0
- preditor/scintilla/lang/config/html.ini +21 -0
- preditor/scintilla/lang/config/javascript.ini +24 -0
- preditor/scintilla/lang/config/lua.ini +16 -0
- preditor/scintilla/lang/config/maxscript.ini +20 -0
- preditor/scintilla/lang/config/mel.ini +18 -0
- preditor/scintilla/lang/config/mu.ini +22 -0
- preditor/scintilla/lang/config/nsi.ini +19 -0
- preditor/scintilla/lang/config/perl.ini +19 -0
- preditor/scintilla/lang/config/puppet.ini +19 -0
- preditor/scintilla/lang/config/python.ini +28 -0
- preditor/scintilla/lang/config/ruby.ini +19 -0
- preditor/scintilla/lang/config/sql.ini +7 -0
- preditor/scintilla/lang/config/xml.ini +21 -0
- preditor/scintilla/lang/config/yaml.ini +18 -0
- preditor/scintilla/lang/language.py +240 -0
- preditor/scintilla/lexers/__init__.py +0 -0
- preditor/scintilla/lexers/cpplexer.py +22 -0
- preditor/scintilla/lexers/javascriptlexer.py +27 -0
- preditor/scintilla/lexers/maxscriptlexer.py +235 -0
- preditor/scintilla/lexers/mellexer.py +369 -0
- preditor/scintilla/lexers/mulexer.py +33 -0
- preditor/scintilla/lexers/pythonlexer.py +42 -0
- preditor/scintilla/ui/finddialog.ui +160 -0
- preditor/settings.py +71 -0
- preditor/stream/__init__.py +72 -0
- preditor/stream/console_handler.py +169 -0
- preditor/stream/director.py +144 -0
- preditor/stream/manager.py +97 -0
- preditor/streamhandler_helper.py +46 -0
- preditor/utils/__init__.py +191 -0
- preditor/utils/call_stack.py +86 -0
- preditor/utils/cute.py +106 -0
- preditor/utils/stylesheets.py +54 -0
- preditor/utils/text_search.py +338 -0
- preditor/version.py +34 -0
- preditor/weakref.py +363 -0
- preditor-2.1.0.dist-info/METADATA +308 -0
- preditor-2.1.0.dist-info/RECORD +179 -0
- preditor-2.1.0.dist-info/WHEEL +5 -0
- preditor-2.1.0.dist-info/entry_points.txt +19 -0
- preditor-2.1.0.dist-info/licenses/LICENSE +165 -0
- preditor-2.1.0.dist-info/top_level.txt +3 -0
- tests/encodings/test_ecoding.py +33 -0
- tests/find_files/test_find_files.py +74 -0
- tests/ide/test_delayable_engine.py +171 -0
|
@@ -0,0 +1,911 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import string
|
|
4
|
+
import subprocess
|
|
5
|
+
import time
|
|
6
|
+
import traceback
|
|
7
|
+
from fractions import Fraction
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from Qt import QtCompat
|
|
11
|
+
from Qt.QtCore import Property, Qt
|
|
12
|
+
from Qt.QtGui import (
|
|
13
|
+
QColor,
|
|
14
|
+
QFontMetrics,
|
|
15
|
+
QIcon,
|
|
16
|
+
QKeySequence,
|
|
17
|
+
QTextCharFormat,
|
|
18
|
+
QTextCursor,
|
|
19
|
+
)
|
|
20
|
+
from Qt.QtWidgets import QAction, QApplication, QTextEdit, QWidget
|
|
21
|
+
|
|
22
|
+
from .. import instance, resourcePath, stream
|
|
23
|
+
from ..constants import StreamType
|
|
24
|
+
from ..stream.console_handler import FormatterDescriptor, HandlerInfo
|
|
25
|
+
from ..utils.cute import QtPropertyInit
|
|
26
|
+
from .codehighlighter import CodeHighlighter
|
|
27
|
+
from .loggerwindow import LoggerWindow
|
|
28
|
+
from .suggest_path_quotes_dialog import SuggestPathQuotesDialog
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ConsoleBase(QTextEdit):
|
|
32
|
+
"""Base class for a text widget used to show stdout/stderr writes."""
|
|
33
|
+
|
|
34
|
+
_default_format = (
|
|
35
|
+
'%(levelname)s %(module)s.%(funcName)s line:%(lineno)d - %(message)s'
|
|
36
|
+
)
|
|
37
|
+
logging_formatter = FormatterDescriptor(default=_default_format)
|
|
38
|
+
"""Used to format logging messages if logging_handlers doesn't define it."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, parent: QWidget, controller: Optional[LoggerWindow] = None):
|
|
41
|
+
super().__init__(parent)
|
|
42
|
+
self._controller = None
|
|
43
|
+
self._stylesheet_changed_meta = None
|
|
44
|
+
self.controller = controller
|
|
45
|
+
|
|
46
|
+
self._first_show = True
|
|
47
|
+
# The last time a repaint call was made when writing. Used to limit the
|
|
48
|
+
# number of refreshes to once per X.X seconds.
|
|
49
|
+
self._last_repaint_time = 0
|
|
50
|
+
# The last time `QApplication.processEvents()` was called while writing.
|
|
51
|
+
# Optionally used to prevent the app from being marked as not responding
|
|
52
|
+
# while processing blocking code and ensure _last_repaint_time processes.
|
|
53
|
+
self._last_process_events_time = 0
|
|
54
|
+
self._write_error_self_destruct = False
|
|
55
|
+
"""If enabling tracebacks using `OverrideConsoleStreams`, this is set to True
|
|
56
|
+
so only if write_error is called by sys.excepthook it will remove itself.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
# Create the highlighter
|
|
60
|
+
highlight = CodeHighlighter(self, 'Python')
|
|
61
|
+
self.setCodeHighlighter(highlight)
|
|
62
|
+
|
|
63
|
+
self.addSepNewline = False
|
|
64
|
+
self.consoleLine = None
|
|
65
|
+
self.mousePressPos = None
|
|
66
|
+
self.logging_info = {}
|
|
67
|
+
|
|
68
|
+
self.init_actions()
|
|
69
|
+
|
|
70
|
+
def __repr__(self):
|
|
71
|
+
"""The repr for this object including its objectName if set."""
|
|
72
|
+
name = self.objectName()
|
|
73
|
+
if name:
|
|
74
|
+
name = f" named {name!r}"
|
|
75
|
+
module = type(self).__module__
|
|
76
|
+
class_ = type(self).__name__
|
|
77
|
+
|
|
78
|
+
return f"<{module}.{class_}{name} object at 0x{id(self):016X}>"
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def __defineRegexPatterns(cls):
|
|
82
|
+
"""Define various regex patterns to use to determine if the msg to write
|
|
83
|
+
is part of a traceback, and also to determine if it is from a workbox, or
|
|
84
|
+
from the console.
|
|
85
|
+
|
|
86
|
+
We construct various parts of the patterns, and combine them into the
|
|
87
|
+
final patterns. The workbox pattern and console pattern share the line
|
|
88
|
+
and in_str parts.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
# For Traceback workbox lines, use this regex pattern, so we can extract
|
|
92
|
+
# workboxName and lineNum. Note that Syntax errors present slightly
|
|
93
|
+
# differently than other Exceptions.
|
|
94
|
+
# SyntaxErrors:
|
|
95
|
+
# - Do NOT include the text ", in" followed by a module
|
|
96
|
+
# - DO include the offending line of code
|
|
97
|
+
# Other Exceptions
|
|
98
|
+
# - DO include the text ", in" followed by a module
|
|
99
|
+
# - Do NOT include the offending line of code if from stdIn (ie
|
|
100
|
+
# a workbox)
|
|
101
|
+
# So we will use the presence of the text ", in" to tell use whether to
|
|
102
|
+
# fake the offending code line or not.
|
|
103
|
+
|
|
104
|
+
# Define pattern pieces
|
|
105
|
+
console_pattern = r'File "<ConsolePrEdit>", '
|
|
106
|
+
workbox_pattern = r'File "<Workbox(?:Selection)?>:(?P<workboxName>.*)", '
|
|
107
|
+
line_pattern = r'line (?P<lineNum>\d{1,6})'
|
|
108
|
+
in_str_pattern = r'(?P<inStr>, in)?'
|
|
109
|
+
|
|
110
|
+
# Put the pattern pieces to together to make patterns
|
|
111
|
+
workbox_pattern = workbox_pattern + line_pattern + in_str_pattern
|
|
112
|
+
cls.workbox_pattern = re.compile(workbox_pattern)
|
|
113
|
+
|
|
114
|
+
console_pattern = console_pattern + line_pattern + in_str_pattern
|
|
115
|
+
cls.console_pattern = re.compile(console_pattern)
|
|
116
|
+
|
|
117
|
+
# Define a pattern to capture info from tracebacks. The newline/$ section
|
|
118
|
+
# handle SyntaxError output that does not include the `, in ...` portion.
|
|
119
|
+
pattern = r'File "(?P<filename>.*)", line (?P<lineNum>\d{1,10})(, in|\r\n|\n|$)'
|
|
120
|
+
cls.traceback_pattern = re.compile(pattern)
|
|
121
|
+
|
|
122
|
+
def add_separator(self):
|
|
123
|
+
"""Add a marker line for visual separation of console output."""
|
|
124
|
+
# Ensure the input is written to the end of the document on a new line
|
|
125
|
+
self.startPrompt("")
|
|
126
|
+
# Add a horizontal rule
|
|
127
|
+
self.insertHtml("<hr><br>")
|
|
128
|
+
|
|
129
|
+
def clear(self):
|
|
130
|
+
"""clears the text in the editor"""
|
|
131
|
+
super().clear()
|
|
132
|
+
# Ensure the console is refreshed in case the user is clearing the console
|
|
133
|
+
# as part of a blocking call.
|
|
134
|
+
self.maybeRepaint(force=True)
|
|
135
|
+
|
|
136
|
+
def contextMenuEvent(self, event):
|
|
137
|
+
"""Builds a custom right click menu to show."""
|
|
138
|
+
# Create the standard menu and allow subclasses to customize it
|
|
139
|
+
menu = self.createStandardContextMenu(event.pos())
|
|
140
|
+
menu = self.update_context_menu(menu)
|
|
141
|
+
if self.controller:
|
|
142
|
+
menu.setFont(self.controller.font())
|
|
143
|
+
menu.exec(self.mapToGlobal(event.pos()))
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def controller(self) -> Optional[LoggerWindow]:
|
|
147
|
+
"""Used to access workbox widgets and PrEditor settings that are needed.
|
|
148
|
+
|
|
149
|
+
This must be set to a LoggerWindow instance. If not set then uses
|
|
150
|
+
`self.window()`. If this instance isn't a child of a LoggerWindow you must
|
|
151
|
+
set controller to an instance of a LoggerWindow.
|
|
152
|
+
"""
|
|
153
|
+
if self._controller:
|
|
154
|
+
return self._controller
|
|
155
|
+
controller = self.window()
|
|
156
|
+
if not isinstance(controller, LoggerWindow):
|
|
157
|
+
controller = instance(create=False)
|
|
158
|
+
return controller
|
|
159
|
+
|
|
160
|
+
@controller.setter
|
|
161
|
+
def controller(self, value: LoggerWindow):
|
|
162
|
+
if self._stylesheet_changed_meta and self.controller:
|
|
163
|
+
# Remove the existing signals if connected
|
|
164
|
+
self.controller.styleSheetChanged.disconnect(self._stylesheet_changed_meta)
|
|
165
|
+
self._stylesheet_changed_meta = None
|
|
166
|
+
|
|
167
|
+
self._controller = value
|
|
168
|
+
# Ensure the stylesheet is up to date and stays up to date.
|
|
169
|
+
self.init_stylesheet()
|
|
170
|
+
|
|
171
|
+
def codeHighlighter(self):
|
|
172
|
+
"""Get the code highlighter for the console
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
_uiCodeHighlighter (CodeHighlighter): The instantiated CodeHighlighter
|
|
176
|
+
"""
|
|
177
|
+
return self._uiCodeHighlighter
|
|
178
|
+
|
|
179
|
+
def setCodeHighlighter(self, highlight):
|
|
180
|
+
"""Set the code highlighter for the console
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
highlight (CodeHighlighter): The instantiated CodeHighlighter
|
|
184
|
+
"""
|
|
185
|
+
self._uiCodeHighlighter = highlight
|
|
186
|
+
|
|
187
|
+
def errorHyperlink(self, anchor):
|
|
188
|
+
"""Determine if chosen line is an error traceback file-info line, if so, parse
|
|
189
|
+
the filepath and line number, and attempt to open the module file in the user's
|
|
190
|
+
chosen text editor at the relevant line, using specified Command Prompt pattern.
|
|
191
|
+
|
|
192
|
+
The text editor defaults to SublimeText3, in the normal install directory
|
|
193
|
+
"""
|
|
194
|
+
if not self.controller:
|
|
195
|
+
# Bail if there isn't a controller
|
|
196
|
+
return
|
|
197
|
+
# Bail if Error Hyperlinks setting is not turned on or we don't have an anchor.
|
|
198
|
+
doHyperlink = (
|
|
199
|
+
self.controller
|
|
200
|
+
and self.controller.uiErrorHyperlinksCHK.isChecked()
|
|
201
|
+
and anchor
|
|
202
|
+
)
|
|
203
|
+
if not doHyperlink:
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
# info is a comma separated string, in the form: "filename, workboxIdx, lineNum"
|
|
207
|
+
info = anchor.split(', ')
|
|
208
|
+
modulePath = info[0]
|
|
209
|
+
workboxName = info[1]
|
|
210
|
+
lineNum = info[2]
|
|
211
|
+
|
|
212
|
+
# fetch info from LoggerWindow
|
|
213
|
+
exePath = self.controller.textEditorPath
|
|
214
|
+
cmdTempl = self.controller.textEditorCmdTempl
|
|
215
|
+
|
|
216
|
+
# Bail if not setup properly
|
|
217
|
+
if not workboxName:
|
|
218
|
+
msg = (
|
|
219
|
+
"Cannot use traceback hyperlink (Correct the path with Options "
|
|
220
|
+
"> Set Preferred Text Editor Path).\n"
|
|
221
|
+
)
|
|
222
|
+
if not exePath:
|
|
223
|
+
msg += "No text editor path defined."
|
|
224
|
+
print(msg)
|
|
225
|
+
return
|
|
226
|
+
if not os.path.exists(exePath):
|
|
227
|
+
msg += "Text editor executable does not exist: {}".format(exePath)
|
|
228
|
+
print(msg)
|
|
229
|
+
return
|
|
230
|
+
if not cmdTempl:
|
|
231
|
+
msg += "No text editor Command Prompt command template defined."
|
|
232
|
+
print(msg)
|
|
233
|
+
return
|
|
234
|
+
if modulePath and not os.path.exists(modulePath):
|
|
235
|
+
msg += "Specified module path does not exist: {}".format(modulePath)
|
|
236
|
+
print(msg)
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
if modulePath:
|
|
240
|
+
# Check if cmdTempl filepaths aren't wrapped in double=quotes to handle
|
|
241
|
+
# spaces. If not, suggest to user to update the template, offering the
|
|
242
|
+
# suggested change.
|
|
243
|
+
pattern = r"(?<!\")({\w+Path})(?!\")"
|
|
244
|
+
repl = r'"\g<1>"'
|
|
245
|
+
quotedCmdTempl = re.sub(pattern, repl, cmdTempl)
|
|
246
|
+
if quotedCmdTempl != cmdTempl:
|
|
247
|
+
# Instantiate dialog to maybe show (unless user previously chose "Don't
|
|
248
|
+
# ask again")
|
|
249
|
+
dialog = SuggestPathQuotesDialog(
|
|
250
|
+
self.controller, cmdTempl, quotedCmdTempl
|
|
251
|
+
)
|
|
252
|
+
self.controller.maybeDisplayDialog(dialog)
|
|
253
|
+
|
|
254
|
+
# Refresh cmdTempl in case user just had it changed.
|
|
255
|
+
cmdTempl = self.controller.textEditorCmdTempl
|
|
256
|
+
|
|
257
|
+
# Attempt to create command from template and run the command
|
|
258
|
+
try:
|
|
259
|
+
command = cmdTempl.format(
|
|
260
|
+
exePath=exePath, modulePath=modulePath, lineNum=lineNum
|
|
261
|
+
)
|
|
262
|
+
subprocess.Popen(command)
|
|
263
|
+
except (ValueError, OSError):
|
|
264
|
+
msg = "The provided text editor command is not valid:\n {}"
|
|
265
|
+
msg = msg.format(cmdTempl)
|
|
266
|
+
print(msg)
|
|
267
|
+
elif workboxName is not None:
|
|
268
|
+
workbox = self.controller.workbox_for_name(workboxName, visible=True)
|
|
269
|
+
lineNum = int(lineNum)
|
|
270
|
+
# Make the controller visible and focus on the workbox. This is not
|
|
271
|
+
# using preditor.launch on the assumption that some widget has already
|
|
272
|
+
# initialized it to store in self.controller.
|
|
273
|
+
self.controller.launch(focus=False)
|
|
274
|
+
workbox.__goto_line__(lineNum)
|
|
275
|
+
workbox.setFocus()
|
|
276
|
+
|
|
277
|
+
@classmethod
|
|
278
|
+
def getIndentForCodeTracebackLine(cls, msg):
|
|
279
|
+
"""Determine the indentation to recreate traceback lines
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
msg (str): The traceback line
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
indent (str): A string of zero or more spaces used for indentation
|
|
286
|
+
"""
|
|
287
|
+
indent = ""
|
|
288
|
+
match = re.match(r"^ *", msg)
|
|
289
|
+
if match:
|
|
290
|
+
indent = match.group() * 2
|
|
291
|
+
return indent
|
|
292
|
+
|
|
293
|
+
def getWorkboxLine(self, name, lineNum):
|
|
294
|
+
"""Python 3 does not include in tracebacks the code line if it comes from
|
|
295
|
+
stdin, which is the case for PrEditor workboxes, so we fake it. This method
|
|
296
|
+
will return the line of code at lineNum, from the workbox with the provided
|
|
297
|
+
name.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
name (str): The name of the workbox from which to get a line of code
|
|
301
|
+
lineNum (int): The number of the line to return
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
txt (str): The line of text found
|
|
305
|
+
"""
|
|
306
|
+
if not self.controller:
|
|
307
|
+
return None
|
|
308
|
+
workbox = self.controller.workbox_for_name(name)
|
|
309
|
+
if not workbox:
|
|
310
|
+
return None
|
|
311
|
+
if lineNum > workbox.lines():
|
|
312
|
+
return None
|
|
313
|
+
txt = workbox.text(lineNum).strip() + "\n"
|
|
314
|
+
return txt
|
|
315
|
+
|
|
316
|
+
def init_actions(self):
|
|
317
|
+
self.uiClearACT = QAction("&Clear", self)
|
|
318
|
+
self.uiClearACT.setIcon(QIcon(resourcePath('img/close-thick.png')))
|
|
319
|
+
self.uiClearACT.setToolTip(
|
|
320
|
+
"Clears the top section of PrEditor. This does not clear the workbox."
|
|
321
|
+
)
|
|
322
|
+
self.uiClearACT.setShortcut(QKeySequence("Ctrl+Shift+Alt+D"))
|
|
323
|
+
self.uiClearACT.setShortcutContext(
|
|
324
|
+
Qt.ShortcutContext.WidgetWithChildrenShortcut
|
|
325
|
+
)
|
|
326
|
+
self.uiClearACT.triggered.connect(self.clear)
|
|
327
|
+
self.addAction(self.uiClearACT)
|
|
328
|
+
|
|
329
|
+
self.uiAddBreakACT = QAction("Add Separator")
|
|
330
|
+
self.uiAddBreakACT.triggered.connect(self.add_separator)
|
|
331
|
+
|
|
332
|
+
def init_logging_handlers(self, attrName=None, value=None):
|
|
333
|
+
# Ensure the old callbacks are removed so they don't keep writing.
|
|
334
|
+
# The stream Manager will deal with if this widget is closed, but not
|
|
335
|
+
# in the case of temporarily disabling a handler.
|
|
336
|
+
for hi in self.logging_info.values():
|
|
337
|
+
hi.uninstall(self.write_log)
|
|
338
|
+
|
|
339
|
+
# Reset and add new handlers to handle log statements
|
|
340
|
+
self.logging_info = {}
|
|
341
|
+
for h in self.logging_handlers:
|
|
342
|
+
hi = HandlerInfo(h)
|
|
343
|
+
hi.install(self.write_log)
|
|
344
|
+
self.logging_info[hi.name] = hi
|
|
345
|
+
|
|
346
|
+
def init_excepthook(self, attrName=None, value=None):
|
|
347
|
+
from preditor.excepthooks import PreditorExceptHook
|
|
348
|
+
|
|
349
|
+
if value:
|
|
350
|
+
if self.write_error not in PreditorExceptHook.callbacks:
|
|
351
|
+
PreditorExceptHook.callbacks.append(self.write_error)
|
|
352
|
+
else:
|
|
353
|
+
if self.write_error in PreditorExceptHook.callbacks:
|
|
354
|
+
PreditorExceptHook.callbacks.remove(self.write_error)
|
|
355
|
+
|
|
356
|
+
def init_stylesheet(self, attrName=None, value=None):
|
|
357
|
+
if not self.controller:
|
|
358
|
+
return
|
|
359
|
+
|
|
360
|
+
signal = self.controller.styleSheetChanged
|
|
361
|
+
if self.use_console_stylesheet:
|
|
362
|
+
# Apply the stylesheet and ensure that future updates are respected
|
|
363
|
+
self._stylesheet_changed_meta = signal.connect(self.update_stylesheet)
|
|
364
|
+
self.update_stylesheet()
|
|
365
|
+
|
|
366
|
+
elif self._stylesheet_changed_meta:
|
|
367
|
+
# if disabling use_console_stylesheet, then remove the existing
|
|
368
|
+
# connection if it was previously connected.
|
|
369
|
+
signal.disconnect(self._stylesheet_changed_meta)
|
|
370
|
+
self._stylesheet_changed_meta = None
|
|
371
|
+
|
|
372
|
+
def maybeRepaint(self, force=False):
|
|
373
|
+
"""Forces the console to repaint if enough time has elapsed from the
|
|
374
|
+
last repaint.
|
|
375
|
+
|
|
376
|
+
This method is called every time a print is written to the console. So if
|
|
377
|
+
more than `self.controller.repaintConsolesDelay` seconds has elapsed after
|
|
378
|
+
the last time maybeRepaint updated the display it will update the display
|
|
379
|
+
again, showing all new output.
|
|
380
|
+
|
|
381
|
+
This prefers calling `self.repaint()` so it doesn't add extra processing
|
|
382
|
+
of the Qt event loop. However if enabled it will call
|
|
383
|
+
`QApplication.processEvents` periodically. This ensures the repaint is
|
|
384
|
+
shown for writes if the Qt app looses focus. On windows this appears to
|
|
385
|
+
happen after ~5 seconds.
|
|
386
|
+
|
|
387
|
+
`self.controller.uiRepaintConsolesOnWriteCHK` can be used to disable
|
|
388
|
+
the entire `maybeRepaint` method. To disable only the processEvents calls
|
|
389
|
+
`self.controller.uiRepaintProcessEventsOccasionallyCHK` can be disabled.
|
|
390
|
+
The repaint delay is controlled by `self.controller.repaintConsolesDelay`.
|
|
391
|
+
"""
|
|
392
|
+
if not self.controller:
|
|
393
|
+
return
|
|
394
|
+
if not self.controller.uiRepaintConsolesOnWriteCHK.isChecked():
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
if force:
|
|
398
|
+
if self.controller.uiRepaintProcessEventsOccasionallyCHK.isChecked():
|
|
399
|
+
QApplication.processEvents()
|
|
400
|
+
self._last_process_events_time = time.time_ns()
|
|
401
|
+
self._last_repaint_time = self._last_process_events_time
|
|
402
|
+
else:
|
|
403
|
+
self.repaint()
|
|
404
|
+
self._last_repaint_time = time.time_ns()
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
# NOTE: All numbers here should remain int values. This method is can be
|
|
408
|
+
# called multiple times per write/print call so it should be optimized
|
|
409
|
+
# as much as possible
|
|
410
|
+
current_time = time.time_ns()
|
|
411
|
+
if (
|
|
412
|
+
self.controller.uiRepaintProcessEventsOccasionallyCHK.isChecked()
|
|
413
|
+
and current_time - self._last_process_events_time > 5 * 1e9 # seconds
|
|
414
|
+
):
|
|
415
|
+
# NOTE: On windows 10 this seems to only need called once after the
|
|
416
|
+
# app looses focus for the repaint to work on each call. However if
|
|
417
|
+
# the app regains focus while processEvents is called it will require
|
|
418
|
+
# another processEvents call. Calling this every X seconds also
|
|
419
|
+
# ensures the app doesn't stay (Not Responding for long).
|
|
420
|
+
QApplication.processEvents()
|
|
421
|
+
self._last_process_events_time = time.time_ns()
|
|
422
|
+
self._last_repaint_time = self._last_process_events_time
|
|
423
|
+
elif (
|
|
424
|
+
current_time - self._last_repaint_time
|
|
425
|
+
> self.controller.repaintConsolesDelay
|
|
426
|
+
):
|
|
427
|
+
# Enough time has elapsed, repaint the widget and reset the delay
|
|
428
|
+
self.repaint()
|
|
429
|
+
self._last_repaint_time = time.time_ns()
|
|
430
|
+
|
|
431
|
+
def mouseMoveEvent(self, event):
|
|
432
|
+
"""Overload of mousePressEvent to change mouse pointer to indicate it is
|
|
433
|
+
over a clickable error hyperlink.
|
|
434
|
+
"""
|
|
435
|
+
if self.anchorAt(event.pos()):
|
|
436
|
+
self.viewport().setCursor(Qt.CursorShape.PointingHandCursor)
|
|
437
|
+
else:
|
|
438
|
+
self.viewport().unsetCursor()
|
|
439
|
+
return super().mouseMoveEvent(event)
|
|
440
|
+
|
|
441
|
+
def mousePressEvent(self, event):
|
|
442
|
+
"""Overload of mousePressEvent to capture click position, so on release, we can
|
|
443
|
+
check release position. If it's the same (ie user clicked vs click-drag to
|
|
444
|
+
select text), we check if user clicked an error hyperlink.
|
|
445
|
+
"""
|
|
446
|
+
left = event.button() == Qt.MouseButton.LeftButton
|
|
447
|
+
anchor = self.anchorAt(event.pos())
|
|
448
|
+
self.mousePressPos = event.pos()
|
|
449
|
+
|
|
450
|
+
if left and anchor:
|
|
451
|
+
event.ignore()
|
|
452
|
+
return
|
|
453
|
+
|
|
454
|
+
return super().mousePressEvent(event)
|
|
455
|
+
|
|
456
|
+
def mouseReleaseEvent(self, event):
|
|
457
|
+
"""Overload of mouseReleaseEvent to capture if user has left clicked... Check if
|
|
458
|
+
click position is the same as release position, if so, call errorHyperlink.
|
|
459
|
+
"""
|
|
460
|
+
samePos = event.pos() == self.mousePressPos
|
|
461
|
+
left = event.button() == Qt.MouseButton.LeftButton
|
|
462
|
+
anchor = self.anchorAt(event.pos())
|
|
463
|
+
|
|
464
|
+
if samePos and left and anchor:
|
|
465
|
+
self.errorHyperlink(anchor)
|
|
466
|
+
self.mousePressPos = None
|
|
467
|
+
|
|
468
|
+
QApplication.restoreOverrideCursor()
|
|
469
|
+
ret = super().mouseReleaseEvent(event)
|
|
470
|
+
|
|
471
|
+
return ret
|
|
472
|
+
|
|
473
|
+
@classmethod
|
|
474
|
+
def parseErrorHyperLinkInfo(cls, txt):
|
|
475
|
+
"""Determine if txt is a File-info line from a traceback, and if so, return info
|
|
476
|
+
dict.
|
|
477
|
+
"""
|
|
478
|
+
|
|
479
|
+
ret = None
|
|
480
|
+
if not txt.lstrip().startswith("File "):
|
|
481
|
+
return ret
|
|
482
|
+
|
|
483
|
+
match = cls.traceback_pattern.search(txt)
|
|
484
|
+
if match:
|
|
485
|
+
filename = match.groupdict().get('filename')
|
|
486
|
+
lineNum = match.groupdict().get('lineNum')
|
|
487
|
+
fileStart = txt.find(filename)
|
|
488
|
+
fileEnd = fileStart + len(filename)
|
|
489
|
+
|
|
490
|
+
ret = {
|
|
491
|
+
'filename': filename,
|
|
492
|
+
'fileStart': fileStart,
|
|
493
|
+
'fileEnd': fileEnd,
|
|
494
|
+
'lineNum': lineNum,
|
|
495
|
+
}
|
|
496
|
+
return ret
|
|
497
|
+
|
|
498
|
+
def onFirstShow(self, event) -> bool:
|
|
499
|
+
"""Run extra code on the first showing of this widget.
|
|
500
|
+
|
|
501
|
+
Example override implementation:
|
|
502
|
+
|
|
503
|
+
class MyConsole(ConsoleBase):
|
|
504
|
+
def onFirstShow(self, event):
|
|
505
|
+
if not super().onFirstShow(event):
|
|
506
|
+
return False
|
|
507
|
+
self.doWork()
|
|
508
|
+
return True
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
bool: Returns True only if this is the first time this widget is
|
|
512
|
+
shown. All overrides of this method should return the same.
|
|
513
|
+
"""
|
|
514
|
+
if not self._first_show:
|
|
515
|
+
return False
|
|
516
|
+
|
|
517
|
+
# Configure the stream callbacks if enabled
|
|
518
|
+
self.update_streams()
|
|
519
|
+
|
|
520
|
+
# Redefine highlight variables now that stylesheet may have been updated
|
|
521
|
+
self.codeHighlighter().defineHighlightVariables()
|
|
522
|
+
|
|
523
|
+
self._first_show = False
|
|
524
|
+
return True
|
|
525
|
+
|
|
526
|
+
def setConsoleFont(self, font):
|
|
527
|
+
"""Set the console's font and adjust the tabStopWidth"""
|
|
528
|
+
|
|
529
|
+
# Capture the scroll bar's current position (by percentage of max)
|
|
530
|
+
origPercent = None
|
|
531
|
+
scroll = self.verticalScrollBar()
|
|
532
|
+
if scroll.maximum():
|
|
533
|
+
origPercent = Fraction(scroll.value(), scroll.maximum())
|
|
534
|
+
|
|
535
|
+
# Set console and completer popup fonts
|
|
536
|
+
self.setFont(font)
|
|
537
|
+
self.completer().popup().setFont(font)
|
|
538
|
+
|
|
539
|
+
# Set the setTabStopWidth for the console's font
|
|
540
|
+
tab_width = 4
|
|
541
|
+
# TODO: Make tab_width a general user setting
|
|
542
|
+
workbox = self.controller.current_workbox()
|
|
543
|
+
if workbox:
|
|
544
|
+
tab_width = workbox.__tab_width__()
|
|
545
|
+
fontPixelWidth = QFontMetrics(font).horizontalAdvance(" ")
|
|
546
|
+
self.setTabStopDistance(fontPixelWidth * tab_width)
|
|
547
|
+
|
|
548
|
+
# Scroll to same relative position where we started
|
|
549
|
+
if origPercent is not None:
|
|
550
|
+
self.doubleSingleShotSetScrollValue(origPercent)
|
|
551
|
+
|
|
552
|
+
def showEvent(self, event):
|
|
553
|
+
# Ensure the onFirstShow method is run.
|
|
554
|
+
self.onFirstShow(event)
|
|
555
|
+
super().showEvent(event)
|
|
556
|
+
|
|
557
|
+
def startPrompt(self, prompt):
|
|
558
|
+
"""create a new command prompt line with the given prompt
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
prompt(str): The prompt to start the line with. If this prompt
|
|
562
|
+
is already the only text on the last line this function does nothing.
|
|
563
|
+
"""
|
|
564
|
+
self.moveCursor(QTextCursor.MoveOperation.End)
|
|
565
|
+
|
|
566
|
+
# if this is not already a new line
|
|
567
|
+
if self.textCursor().block().text() != prompt:
|
|
568
|
+
charFormat = QTextCharFormat()
|
|
569
|
+
self.setCurrentCharFormat(charFormat)
|
|
570
|
+
|
|
571
|
+
inputstr = prompt
|
|
572
|
+
if self.textCursor().block().text():
|
|
573
|
+
inputstr = '\n' + inputstr
|
|
574
|
+
|
|
575
|
+
self.insertPlainText(inputstr)
|
|
576
|
+
|
|
577
|
+
scroll = self.verticalScrollBar()
|
|
578
|
+
maximum = scroll.maximum()
|
|
579
|
+
if maximum is not None:
|
|
580
|
+
scroll.setValue(maximum)
|
|
581
|
+
|
|
582
|
+
def update_context_menu(self, menu):
|
|
583
|
+
"""Returns the menu to use for right click context."""
|
|
584
|
+
# Note: this menu is built in reverse order for easy insertion
|
|
585
|
+
sep = menu.insertSeparator(menu.actions()[0])
|
|
586
|
+
menu.insertAction(sep, self.uiClearACT)
|
|
587
|
+
menu.insertAction(sep, self.uiAddBreakACT)
|
|
588
|
+
return menu
|
|
589
|
+
|
|
590
|
+
def update_streams(self, attrName=None, value=None):
|
|
591
|
+
# overload the sys logger and ensure the stream_manager is installed
|
|
592
|
+
self.stream_manager = stream.install_to_std()
|
|
593
|
+
|
|
594
|
+
needs_callback = self.stream_echo_stdout or self.stream_echo_stderr
|
|
595
|
+
if needs_callback:
|
|
596
|
+
# Redirect future writes directly to the console, add any previous
|
|
597
|
+
# writes to the console and possibly free up the memory consumed by
|
|
598
|
+
# previous writes. It's safe to call this repeatedly.
|
|
599
|
+
self.stream_manager.add_callback(
|
|
600
|
+
self.write,
|
|
601
|
+
replay=self.stream_replay,
|
|
602
|
+
disable_writes=self.stream_disable_writes,
|
|
603
|
+
clear=self.stream_clear,
|
|
604
|
+
)
|
|
605
|
+
else:
|
|
606
|
+
self.stream_manager.remove_callback(self.write)
|
|
607
|
+
|
|
608
|
+
def update_stylesheet(self):
|
|
609
|
+
sheet = None
|
|
610
|
+
if self.controller:
|
|
611
|
+
sheet = self.controller.styleSheet()
|
|
612
|
+
self.setStyleSheet(sheet)
|
|
613
|
+
|
|
614
|
+
def get_logging_info(self, name):
|
|
615
|
+
# Look for a specific rule to handle this logging message
|
|
616
|
+
parts = name.split(".")
|
|
617
|
+
for i in range(len(parts), 0, -1):
|
|
618
|
+
name = ".".join(parts[:i])
|
|
619
|
+
if name in self.logging_info:
|
|
620
|
+
return self.logging_info[name]
|
|
621
|
+
|
|
622
|
+
# If no logging handler matches the name but we are showing the root
|
|
623
|
+
# handler fall back to using the root handler to handle this logging call.
|
|
624
|
+
if "root" in self.logging_info:
|
|
625
|
+
return self.logging_info["root"]
|
|
626
|
+
|
|
627
|
+
# Otherwise ignore it
|
|
628
|
+
return None
|
|
629
|
+
|
|
630
|
+
def write_error(self, *exc_info):
|
|
631
|
+
text = traceback.format_exception(*exc_info)
|
|
632
|
+
for line in text:
|
|
633
|
+
self.write(line, stream_type=StreamType.CONSOLE | StreamType.STDERR)
|
|
634
|
+
|
|
635
|
+
if self._write_error_self_destruct:
|
|
636
|
+
# This should only be enabled by `OverrideConsoleStreams`. This callback
|
|
637
|
+
# was installed temporarily and once its called by sys.excepthook it
|
|
638
|
+
# should be removed so it doesn't get called again.
|
|
639
|
+
from preditor.excepthooks import PreditorExceptHook
|
|
640
|
+
|
|
641
|
+
self._write_error_self_destruct = False
|
|
642
|
+
PreditorExceptHook.callbacks.remove(self.write_error)
|
|
643
|
+
|
|
644
|
+
def write_log(self, log_data, stream_type=StreamType.CONSOLE):
|
|
645
|
+
"""Write a logging message to the console depending on filters."""
|
|
646
|
+
handler, record = log_data
|
|
647
|
+
# Find the console configuration that allows processing of this record
|
|
648
|
+
logging_info = self.get_logging_info(record.name)
|
|
649
|
+
if logging_info is None:
|
|
650
|
+
return
|
|
651
|
+
|
|
652
|
+
# Only log the record if it matches the logging level requirements
|
|
653
|
+
if logging_info.level > record.levelno:
|
|
654
|
+
return
|
|
655
|
+
|
|
656
|
+
formatter = handler
|
|
657
|
+
if logging_info.formatter:
|
|
658
|
+
formatter = logging_info.formatter
|
|
659
|
+
elif self.logging_formatter:
|
|
660
|
+
formatter = self.logging_formatter
|
|
661
|
+
msg = formatter.format(record)
|
|
662
|
+
self.write(f'{msg}\n', stream_type=stream_type)
|
|
663
|
+
|
|
664
|
+
def write(self, msg, stream_type=StreamType.STDOUT):
|
|
665
|
+
"""Write a message to the logger.
|
|
666
|
+
|
|
667
|
+
Args:
|
|
668
|
+
msg (str): The message to write.
|
|
669
|
+
stream_type (bool, optional): Treat this write as as stderr output.
|
|
670
|
+
|
|
671
|
+
In order to make a stack-trace provide clickable hyperlinks, it must be sent
|
|
672
|
+
to self._write line-by-line, like a actual exception traceback is. So, we check
|
|
673
|
+
if msg has the stack marker str, if so, send it line by line, otherwise, just
|
|
674
|
+
pass msg on to self._write.
|
|
675
|
+
"""
|
|
676
|
+
stack_marker = "Stack (most recent call last)"
|
|
677
|
+
index = msg.find(stack_marker)
|
|
678
|
+
has_stack_marker = index > -1
|
|
679
|
+
|
|
680
|
+
if has_stack_marker:
|
|
681
|
+
lines = msg.split("\n")
|
|
682
|
+
for line in lines:
|
|
683
|
+
line = "{}\n".format(line)
|
|
684
|
+
self._write(line, stream_type=stream_type)
|
|
685
|
+
else:
|
|
686
|
+
self._write(msg, stream_type=stream_type)
|
|
687
|
+
|
|
688
|
+
def _write(self, msg, stream_type=StreamType.STDOUT):
|
|
689
|
+
"""write the message to the logger"""
|
|
690
|
+
if not msg:
|
|
691
|
+
return
|
|
692
|
+
|
|
693
|
+
# Convert the stream_manager's stream to the boolean value this function expects
|
|
694
|
+
to_error = stream_type & StreamType.STDERR == StreamType.STDERR
|
|
695
|
+
to_console = stream_type & StreamType.CONSOLE == StreamType.CONSOLE
|
|
696
|
+
to_result = stream_type & StreamType.RESULT == StreamType.RESULT
|
|
697
|
+
|
|
698
|
+
# Check that we haven't been garbage collected before trying to write.
|
|
699
|
+
# This can happen while shutting down a QApplication like Nuke.
|
|
700
|
+
if not QtCompat.isValid(self):
|
|
701
|
+
return
|
|
702
|
+
|
|
703
|
+
if to_result and not self.stream_echo_result:
|
|
704
|
+
return
|
|
705
|
+
|
|
706
|
+
# If stream_type is Console, then always show the output
|
|
707
|
+
if not to_console:
|
|
708
|
+
# Otherwise only show the message
|
|
709
|
+
if to_error and not self.stream_echo_stderr:
|
|
710
|
+
return
|
|
711
|
+
if not to_error and not self.stream_echo_stdout:
|
|
712
|
+
return
|
|
713
|
+
|
|
714
|
+
if self.controller:
|
|
715
|
+
doHyperlink = self.controller.uiErrorHyperlinksCHK.isChecked()
|
|
716
|
+
sepPreditorTrace = self.controller.uiSeparateTracebackCHK.isChecked()
|
|
717
|
+
else:
|
|
718
|
+
doHyperlink = False
|
|
719
|
+
sepPreditorTrace = False
|
|
720
|
+
self.moveCursor(QTextCursor.MoveOperation.End)
|
|
721
|
+
|
|
722
|
+
charFormat = QTextCharFormat()
|
|
723
|
+
if not to_error:
|
|
724
|
+
charFormat.setForeground(self.stdoutColor)
|
|
725
|
+
else:
|
|
726
|
+
charFormat.setForeground(self.errorMessageColor)
|
|
727
|
+
self.setCurrentCharFormat(charFormat)
|
|
728
|
+
|
|
729
|
+
# If showing Error Hyperlinks... Sometimes (when a syntax error, at least),
|
|
730
|
+
# the last File-Info line of a traceback is issued in multiple messages
|
|
731
|
+
# starting with unicode paragraph separator (r"\u2029") and followed by a
|
|
732
|
+
# newline, so our normal string checks search won't work. Instead, we'll
|
|
733
|
+
# manually reconstruct the line. If msg is a newline, grab that current line
|
|
734
|
+
# and check it. If it matches,proceed using that line as msg
|
|
735
|
+
cursor = self.textCursor()
|
|
736
|
+
info = None
|
|
737
|
+
|
|
738
|
+
if doHyperlink and msg == '\n':
|
|
739
|
+
cursor.select(QTextCursor.SelectionType.BlockUnderCursor)
|
|
740
|
+
line = cursor.selectedText()
|
|
741
|
+
|
|
742
|
+
# Remove possible leading unicode paragraph separator, which really
|
|
743
|
+
# messes up the works
|
|
744
|
+
if line and line[0] not in string.printable:
|
|
745
|
+
line = line[1:]
|
|
746
|
+
|
|
747
|
+
info = self.parseErrorHyperLinkInfo(line)
|
|
748
|
+
if info:
|
|
749
|
+
cursor.insertText("\n")
|
|
750
|
+
msg = "{}\n".format(line)
|
|
751
|
+
|
|
752
|
+
# If showing Error Hyperlinks, display underline output, otherwise
|
|
753
|
+
# display normal output. Exclude ConsolePrEdits
|
|
754
|
+
info = info if info else self.parseErrorHyperLinkInfo(msg)
|
|
755
|
+
filename = info.get("filename", "") if info else ""
|
|
756
|
+
|
|
757
|
+
# Determine if this is a workbox line of code, or code run directly
|
|
758
|
+
# in the console
|
|
759
|
+
isWorkbox = '<WorkboxSelection>' in filename or '<Workbox>' in filename
|
|
760
|
+
isConsolePrEdit = '<ConsolePrEdit>' in filename
|
|
761
|
+
|
|
762
|
+
# Starting in Python 3, tracebacks don't include the code executed
|
|
763
|
+
# for stdin, so workbox code won't appear. This attempts to include
|
|
764
|
+
# it. There is an exception for SyntaxErrors, which DO include the
|
|
765
|
+
# offending line of code, so in those cases (indicated by lack of
|
|
766
|
+
# inStr from the regex search) we skip faking the code line.
|
|
767
|
+
if isWorkbox:
|
|
768
|
+
match = self.workbox_pattern.search(msg)
|
|
769
|
+
workboxName = match.groupdict().get("workboxName")
|
|
770
|
+
lineNum = int(match.groupdict().get("lineNum")) - 1
|
|
771
|
+
inStr = match.groupdict().get("inStr", "")
|
|
772
|
+
|
|
773
|
+
workboxLine = self.getWorkboxLine(workboxName, lineNum)
|
|
774
|
+
if workboxLine and inStr:
|
|
775
|
+
indent = self.getIndentForCodeTracebackLine(msg)
|
|
776
|
+
msg = "{}{}{}".format(msg, indent, workboxLine)
|
|
777
|
+
|
|
778
|
+
elif isConsolePrEdit:
|
|
779
|
+
# Syntax error tracebacks are different than other Exception.
|
|
780
|
+
# They don't include ", in ..." and are issued differently than
|
|
781
|
+
# other Exceptions, in that they will issue the final piece of
|
|
782
|
+
# offending code, whereas other Exceptions do not, for some
|
|
783
|
+
# reason. They do not need, and shouldn't, be handled here.
|
|
784
|
+
match = self.console_pattern.search(msg)
|
|
785
|
+
inStr = match.groupdict().get("inStr", "")
|
|
786
|
+
if inStr:
|
|
787
|
+
consoleLine = self.consoleLine
|
|
788
|
+
indent = self.getIndentForCodeTracebackLine(msg)
|
|
789
|
+
msg = "{}{}{}\n".format(msg, indent, consoleLine)
|
|
790
|
+
|
|
791
|
+
# To make it easier to see relevant lines of a traceback, optionally insert
|
|
792
|
+
# a newline separating internal PrEditor code from the code run by user.
|
|
793
|
+
if self.addSepNewline:
|
|
794
|
+
if sepPreditorTrace:
|
|
795
|
+
msg = "\n" + msg
|
|
796
|
+
self.addSepNewline = False
|
|
797
|
+
|
|
798
|
+
preditorCalls = ("cmdresult = e", "exec(compiled,")
|
|
799
|
+
if msg.strip().startswith(preditorCalls):
|
|
800
|
+
self.addSepNewline = True
|
|
801
|
+
|
|
802
|
+
# Error tracebacks and logging.stack_info supply msg's differently,
|
|
803
|
+
# so modify it here, so we get consistent results.
|
|
804
|
+
msg = msg.replace("\n\n", "\n")
|
|
805
|
+
|
|
806
|
+
if info and doHyperlink and not isConsolePrEdit:
|
|
807
|
+
fileStart = info.get("fileStart")
|
|
808
|
+
fileEnd = info.get("fileEnd")
|
|
809
|
+
lineNum = info.get("lineNum")
|
|
810
|
+
|
|
811
|
+
toolTip = 'Open "{}" at line number {}'.format(filename, lineNum)
|
|
812
|
+
if isWorkbox:
|
|
813
|
+
split = filename.split(':')
|
|
814
|
+
workboxIdx = split[-1]
|
|
815
|
+
filename = ''
|
|
816
|
+
else:
|
|
817
|
+
filename = filename
|
|
818
|
+
workboxIdx = ''
|
|
819
|
+
href = '{}, {}, {}'.format(filename, workboxIdx, lineNum)
|
|
820
|
+
|
|
821
|
+
# Insert initial, non-underlined text
|
|
822
|
+
cursor.insertText(msg[:fileStart])
|
|
823
|
+
|
|
824
|
+
# Insert hyperlink
|
|
825
|
+
fmt = cursor.charFormat()
|
|
826
|
+
fmt.setAnchor(True)
|
|
827
|
+
fmt.setAnchorHref(href)
|
|
828
|
+
fmt.setFontUnderline(True)
|
|
829
|
+
fmt.setToolTip(toolTip)
|
|
830
|
+
cursor.insertText(msg[fileStart:fileEnd], fmt)
|
|
831
|
+
|
|
832
|
+
# Insert the rest of the msg
|
|
833
|
+
fmt.setAnchor(False)
|
|
834
|
+
fmt.setAnchorHref('')
|
|
835
|
+
fmt.setFontUnderline(False)
|
|
836
|
+
fmt.setToolTip('')
|
|
837
|
+
cursor.insertText(msg[fileEnd:], fmt)
|
|
838
|
+
else:
|
|
839
|
+
# Non-hyperlink output
|
|
840
|
+
self.insertPlainText(msg)
|
|
841
|
+
|
|
842
|
+
# Update the display of the console if enough time has passed and enabled
|
|
843
|
+
self.maybeRepaint()
|
|
844
|
+
|
|
845
|
+
# These Qt Properties can be customized using style sheets.
|
|
846
|
+
commentColor = QtPropertyInit('_commentColor', QColor(0, 206, 52))
|
|
847
|
+
errorMessageColor = QtPropertyInit('_errorMessageColor', QColor(Qt.GlobalColor.red))
|
|
848
|
+
keywordColor = QtPropertyInit('_keywordColor', QColor(17, 154, 255))
|
|
849
|
+
resultColor = QtPropertyInit('_resultColor', QColor(128, 128, 128))
|
|
850
|
+
stdoutColor = QtPropertyInit('_stdoutColor', QColor(17, 154, 255))
|
|
851
|
+
stringColor = QtPropertyInit('_stringColor', QColor(255, 128, 0))
|
|
852
|
+
|
|
853
|
+
@Property(str)
|
|
854
|
+
def logging_formatter_str(self):
|
|
855
|
+
"""QtProperty exposing logging_formatter as a string for QtDesigner."""
|
|
856
|
+
try:
|
|
857
|
+
return self.logging_formatter._fmt
|
|
858
|
+
except AttributeError:
|
|
859
|
+
return ""
|
|
860
|
+
|
|
861
|
+
@logging_formatter_str.setter # type: ignore[no-redef]
|
|
862
|
+
def logging_formatter_str(self, value):
|
|
863
|
+
self.logging_formatter = value
|
|
864
|
+
|
|
865
|
+
logging_handlers = QtPropertyInit(
|
|
866
|
+
'_logging_handlers', list, callback=init_logging_handlers, typ="QStringList"
|
|
867
|
+
)
|
|
868
|
+
"""Used to install LoggerWindowHandler's for this console. Each item should be a
|
|
869
|
+
`handler.name,level`. Level can be the int value (50) or level name (DEBUG).
|
|
870
|
+
"""
|
|
871
|
+
|
|
872
|
+
# Configure stdout/error redirection options
|
|
873
|
+
stream_clear = QtPropertyInit('_stream_clear', False)
|
|
874
|
+
"""When first shown, should this instance clear the stream manager's stored
|
|
875
|
+
history?"""
|
|
876
|
+
stream_disable_writes = QtPropertyInit('_stream_disable_writes', False)
|
|
877
|
+
"""When first shown, should this instance disable writes on the stream?"""
|
|
878
|
+
stream_replay = QtPropertyInit('_stream_replay', False)
|
|
879
|
+
"""When first shown, should this instance replay the streams stored history?"""
|
|
880
|
+
stream_echo_stderr = QtPropertyInit(
|
|
881
|
+
'_stream_echo_stderr', False, callback=update_streams
|
|
882
|
+
)
|
|
883
|
+
"""Should this console print stderr writes?"""
|
|
884
|
+
stream_echo_stdout = QtPropertyInit(
|
|
885
|
+
'_stream_echo_stdout', False, callback=update_streams
|
|
886
|
+
)
|
|
887
|
+
"""Should this console print stdout writes?"""
|
|
888
|
+
stream_echo_result = False
|
|
889
|
+
"""Reserved for ConsolePrEdit to enable StreamType.RESULT output. There is
|
|
890
|
+
no reason for the baseclass to use QtPropertyInit, but this property is
|
|
891
|
+
checked used by write so it needs defined."""
|
|
892
|
+
stream_echo_tracebacks = QtPropertyInit(
|
|
893
|
+
"_stream_echo_tracebacks", False, callback=init_excepthook
|
|
894
|
+
)
|
|
895
|
+
"""Should this console print captured exceptions? Only use this if
|
|
896
|
+
stream_echo_stderr is disabled or you likely will get duplicate output.
|
|
897
|
+
"""
|
|
898
|
+
|
|
899
|
+
use_console_stylesheet = QtPropertyInit(
|
|
900
|
+
"_use_console_stylesheet", False, callback=init_stylesheet
|
|
901
|
+
)
|
|
902
|
+
"""Set this widgets stylesheet to the PrEditor instance's style sheet.
|
|
903
|
+
|
|
904
|
+
This ensures that the style of random OutputConsoles match even when not
|
|
905
|
+
parented to PrEditor. Enabling this will update the widgets style sheet, but
|
|
906
|
+
disabling it will not update the style sheet.
|
|
907
|
+
"""
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
# Build and add the class properties for regex patterns so subclasses can use them.
|
|
911
|
+
ConsoleBase._ConsoleBase__defineRegexPatterns()
|