PrEditor 1.0.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.

Potentially problematic release.


This version of PrEditor might be problematic. Click here for more details.

Files changed (158) hide show
  1. preditor/__init__.py +322 -0
  2. preditor/__main__.py +13 -0
  3. preditor/about_module.py +161 -0
  4. preditor/cli.py +192 -0
  5. preditor/config.py +302 -0
  6. preditor/contexts.py +119 -0
  7. preditor/cores/__init__.py +0 -0
  8. preditor/cores/core.py +20 -0
  9. preditor/dccs/maya/PrEditor_maya.mod +2 -0
  10. preditor/dccs/maya/plug-ins/PrEditor_maya.py +110 -0
  11. preditor/debug.py +144 -0
  12. preditor/delayable_engine/__init__.py +302 -0
  13. preditor/delayable_engine/delayables.py +85 -0
  14. preditor/enum.py +728 -0
  15. preditor/excepthooks.py +131 -0
  16. preditor/gui/__init__.py +93 -0
  17. preditor/gui/app.py +160 -0
  18. preditor/gui/codehighlighter.py +209 -0
  19. preditor/gui/completer.py +226 -0
  20. preditor/gui/console.py +867 -0
  21. preditor/gui/dialog.py +178 -0
  22. preditor/gui/drag_tab_bar.py +190 -0
  23. preditor/gui/editor_chooser.py +57 -0
  24. preditor/gui/errordialog.py +68 -0
  25. preditor/gui/find_files.py +125 -0
  26. preditor/gui/fuzzy_search/__init__.py +0 -0
  27. preditor/gui/fuzzy_search/fuzzy_search.py +93 -0
  28. preditor/gui/group_tab_widget/__init__.py +325 -0
  29. preditor/gui/group_tab_widget/grouped_tab_menu.py +35 -0
  30. preditor/gui/group_tab_widget/grouped_tab_models.py +108 -0
  31. preditor/gui/group_tab_widget/grouped_tab_widget.py +78 -0
  32. preditor/gui/group_tab_widget/one_tab_widget.py +54 -0
  33. preditor/gui/level_buttons.py +343 -0
  34. preditor/gui/logger_window_handler.py +48 -0
  35. preditor/gui/logger_window_plugin.py +32 -0
  36. preditor/gui/loggerwindow.py +1385 -0
  37. preditor/gui/newtabwidget.py +69 -0
  38. preditor/gui/set_text_editor_path_dialog.py +59 -0
  39. preditor/gui/status_label.py +99 -0
  40. preditor/gui/suggest_path_quotes_dialog.py +50 -0
  41. preditor/gui/ui/editor_chooser.ui +93 -0
  42. preditor/gui/ui/errordialog.ui +74 -0
  43. preditor/gui/ui/find_files.ui +140 -0
  44. preditor/gui/ui/loggerwindow.ui +1105 -0
  45. preditor/gui/ui/set_text_editor_path_dialog.ui +189 -0
  46. preditor/gui/ui/suggest_path_quotes_dialog.ui +225 -0
  47. preditor/gui/window.py +161 -0
  48. preditor/gui/workbox_mixin.py +389 -0
  49. preditor/gui/workbox_text_edit.py +137 -0
  50. preditor/gui/workboxwidget.py +298 -0
  51. preditor/logging_config.py +52 -0
  52. preditor/osystem.py +401 -0
  53. preditor/plugins.py +118 -0
  54. preditor/prefs.py +74 -0
  55. preditor/resource/environment_variables.html +26 -0
  56. preditor/resource/error_mail.html +85 -0
  57. preditor/resource/error_mail_inline.html +41 -0
  58. preditor/resource/img/README.md +17 -0
  59. preditor/resource/img/arrow_forward.png +0 -0
  60. preditor/resource/img/check-bold.png +0 -0
  61. preditor/resource/img/chevron-down.png +0 -0
  62. preditor/resource/img/chevron-up.png +0 -0
  63. preditor/resource/img/close-thick.png +0 -0
  64. preditor/resource/img/comment-edit.png +0 -0
  65. preditor/resource/img/content-copy.png +0 -0
  66. preditor/resource/img/content-cut.png +0 -0
  67. preditor/resource/img/content-duplicate.png +0 -0
  68. preditor/resource/img/content-paste.png +0 -0
  69. preditor/resource/img/content-save.png +0 -0
  70. preditor/resource/img/debug_disabled.png +0 -0
  71. preditor/resource/img/eye-check.png +0 -0
  72. preditor/resource/img/file-plus.png +0 -0
  73. preditor/resource/img/file-remove.png +0 -0
  74. preditor/resource/img/format-align-left.png +0 -0
  75. preditor/resource/img/format-letter-case-lower.png +0 -0
  76. preditor/resource/img/format-letter-case-upper.png +0 -0
  77. preditor/resource/img/format-letter-case.svg +1 -0
  78. preditor/resource/img/information.png +0 -0
  79. preditor/resource/img/logging_critical.png +0 -0
  80. preditor/resource/img/logging_custom.png +0 -0
  81. preditor/resource/img/logging_debug.png +0 -0
  82. preditor/resource/img/logging_error.png +0 -0
  83. preditor/resource/img/logging_info.png +0 -0
  84. preditor/resource/img/logging_not_set.png +0 -0
  85. preditor/resource/img/logging_warning.png +0 -0
  86. preditor/resource/img/marker.png +0 -0
  87. preditor/resource/img/play.png +0 -0
  88. preditor/resource/img/playlist-play.png +0 -0
  89. preditor/resource/img/plus-minus-variant.png +0 -0
  90. preditor/resource/img/preditor.ico +0 -0
  91. preditor/resource/img/preditor.png +0 -0
  92. preditor/resource/img/preditor.psd +0 -0
  93. preditor/resource/img/preditor.svg +44 -0
  94. preditor/resource/img/regex.svg +1 -0
  95. preditor/resource/img/restart.svg +1 -0
  96. preditor/resource/img/skip-forward-outline.png +0 -0
  97. preditor/resource/img/skip-next-outline.png +0 -0
  98. preditor/resource/img/skip-next.png +0 -0
  99. preditor/resource/img/skip-previous.png +0 -0
  100. preditor/resource/img/subdirectory-arrow-right.png +0 -0
  101. preditor/resource/img/text-search-variant.png +0 -0
  102. preditor/resource/img/warning-big.png +0 -0
  103. preditor/resource/lang/python.json +30 -0
  104. preditor/resource/settings.ini +25 -0
  105. preditor/resource/stylesheet/Bright.css +65 -0
  106. preditor/resource/stylesheet/Dark.css +199 -0
  107. preditor/scintilla/__init__.py +22 -0
  108. preditor/scintilla/delayables/__init__.py +11 -0
  109. preditor/scintilla/delayables/smart_highlight.py +94 -0
  110. preditor/scintilla/delayables/spell_check.py +173 -0
  111. preditor/scintilla/documenteditor.py +2038 -0
  112. preditor/scintilla/finddialog.py +68 -0
  113. preditor/scintilla/lang/__init__.py +80 -0
  114. preditor/scintilla/lang/config/bash.ini +15 -0
  115. preditor/scintilla/lang/config/batch.ini +14 -0
  116. preditor/scintilla/lang/config/cpp.ini +19 -0
  117. preditor/scintilla/lang/config/css.ini +19 -0
  118. preditor/scintilla/lang/config/eyeonscript.ini +17 -0
  119. preditor/scintilla/lang/config/html.ini +21 -0
  120. preditor/scintilla/lang/config/javascript.ini +24 -0
  121. preditor/scintilla/lang/config/lua.ini +16 -0
  122. preditor/scintilla/lang/config/maxscript.ini +20 -0
  123. preditor/scintilla/lang/config/mel.ini +18 -0
  124. preditor/scintilla/lang/config/mu.ini +22 -0
  125. preditor/scintilla/lang/config/nsi.ini +19 -0
  126. preditor/scintilla/lang/config/perl.ini +19 -0
  127. preditor/scintilla/lang/config/puppet.ini +19 -0
  128. preditor/scintilla/lang/config/python.ini +28 -0
  129. preditor/scintilla/lang/config/ruby.ini +19 -0
  130. preditor/scintilla/lang/config/sql.ini +7 -0
  131. preditor/scintilla/lang/config/xml.ini +21 -0
  132. preditor/scintilla/lang/config/yaml.ini +18 -0
  133. preditor/scintilla/lang/language.py +240 -0
  134. preditor/scintilla/lexers/__init__.py +0 -0
  135. preditor/scintilla/lexers/cpplexer.py +21 -0
  136. preditor/scintilla/lexers/javascriptlexer.py +25 -0
  137. preditor/scintilla/lexers/maxscriptlexer.py +234 -0
  138. preditor/scintilla/lexers/mellexer.py +368 -0
  139. preditor/scintilla/lexers/mulexer.py +32 -0
  140. preditor/scintilla/lexers/pythonlexer.py +41 -0
  141. preditor/scintilla/ui/finddialog.ui +160 -0
  142. preditor/settings.py +71 -0
  143. preditor/stream/__init__.py +80 -0
  144. preditor/stream/director.py +73 -0
  145. preditor/stream/manager.py +74 -0
  146. preditor/streamhandler_helper.py +46 -0
  147. preditor/utils/__init__.py +0 -0
  148. preditor/utils/cute.py +30 -0
  149. preditor/utils/stylesheets.py +54 -0
  150. preditor/utils/text_search.py +342 -0
  151. preditor/version.py +21 -0
  152. preditor/weakref.py +363 -0
  153. preditor-1.0.0.dist-info/METADATA +224 -0
  154. preditor-1.0.0.dist-info/RECORD +158 -0
  155. preditor-1.0.0.dist-info/WHEEL +5 -0
  156. preditor-1.0.0.dist-info/entry_points.txt +18 -0
  157. preditor-1.0.0.dist-info/licenses/LICENSE +165 -0
  158. preditor-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,867 @@
1
+ """ LoggerWindow class is an overloaded python interpreter for preditor"""
2
+ from __future__ import absolute_import, print_function
3
+
4
+ import os
5
+ import re
6
+ import string
7
+ import subprocess
8
+ import sys
9
+ import time
10
+ import traceback
11
+ from builtins import str as text
12
+ from fractions import Fraction
13
+ from functools import partial
14
+
15
+ import __main__
16
+ from Qt import QtCompat
17
+ from Qt.QtCore import QPoint, Qt, QTimer
18
+ from Qt.QtGui import QColor, QFontMetrics, QTextCharFormat, QTextCursor, QTextDocument
19
+ from Qt.QtWidgets import QAbstractItemView, QAction, QApplication, QTextEdit
20
+
21
+ from .. import settings, stream
22
+ from ..streamhandler_helper import StreamHandlerHelper
23
+ from . import QtPropertyInit
24
+ from .codehighlighter import CodeHighlighter
25
+ from .completer import PythonCompleter
26
+ from .suggest_path_quotes_dialog import SuggestPathQuotesDialog
27
+
28
+
29
+ class ConsolePrEdit(QTextEdit):
30
+ # Ensure the error prompt only shows up once.
31
+ _errorPrompted = False
32
+
33
+ # These Qt Properties can be customized using style sheets.
34
+ commentColor = QtPropertyInit('_commentColor', QColor(0, 206, 52))
35
+ errorMessageColor = QtPropertyInit('_errorMessageColor', QColor(Qt.red))
36
+ keywordColor = QtPropertyInit('_keywordColor', QColor(17, 154, 255))
37
+ resultColor = QtPropertyInit('_resultColor', QColor(128, 128, 128))
38
+ stdoutColor = QtPropertyInit('_stdoutColor', QColor(17, 154, 255))
39
+ stringColor = QtPropertyInit('_stringColor', QColor(255, 128, 0))
40
+
41
+ def __init__(self, parent):
42
+ super(ConsolePrEdit, self).__init__(parent)
43
+ # store the error buffer
44
+ self._completer = None
45
+
46
+ # If populated, also write to this interface
47
+ self.outputPipe = None
48
+
49
+ self._consolePrompt = '>>> '
50
+ # Note: Changing _outputPrompt may require updating resource\lang\python.xml
51
+ # If still using a #
52
+ self._outputPrompt = '#Result: '
53
+ # Method used to update the gui when code is executed
54
+ self.clearExecutionTime = None
55
+ self.reportExecutionTime = None
56
+
57
+ self._firstShow = True
58
+
59
+ # When executing code, that takes longer than this seconds, flash the window
60
+ self.flash_time = 1.0
61
+ self.flash_window = None
62
+
63
+ # Store previous commands to retrieve easily
64
+ self._prevCommands = []
65
+ self._prevCommandIndex = 0
66
+ self._prevCommandsMax = 100
67
+
68
+ # create the completer
69
+ self.setCompleter(PythonCompleter(self))
70
+
71
+ # sys.__stdout__ doesn't work if some third party has implemented their own
72
+ # override. Use these to backup the current logger so the logger displays
73
+ # output, but application specific consoles also get the info.
74
+ self.stdout = None
75
+ self.stderr = None
76
+ self._errorLog = None
77
+
78
+ # overload the sys logger
79
+ self.stream_manager = stream.install_to_std()
80
+ # Redirect future writes directly to the console, add any previous writes
81
+ # to the console and free up the memory consumed by previous writes as we
82
+ # assume this is likely to be the only callback added to the manager.
83
+ self.stream_manager.add_callback(
84
+ self.write, replay=True, disable_writes=True, clear=True
85
+ )
86
+ # Store the current outputs
87
+ self.stdout = sys.stdout
88
+ self.stderr = sys.stderr
89
+ self._errorLog = sys.stderr
90
+
91
+ # Update any StreamHandler's that were setup using the old stdout/err
92
+ StreamHandlerHelper.replace_stream(self.stdout, sys.stdout)
93
+ StreamHandlerHelper.replace_stream(self.stderr, sys.stderr)
94
+
95
+ # create the highlighter
96
+ highlight = CodeHighlighter(self)
97
+ highlight.setLanguage('Python')
98
+ self.uiCodeHighlighter = highlight
99
+
100
+ self.uiClearToLastPromptACT = QAction('Clear to Last', self)
101
+ self.uiClearToLastPromptACT.triggered.connect(self.clearToLastPrompt)
102
+ self.uiClearToLastPromptACT.setShortcut(Qt.CTRL | Qt.SHIFT | Qt.Key_Backspace)
103
+ self.addAction(self.uiClearToLastPromptACT)
104
+
105
+ self.x = 0
106
+ self.clickPos = None
107
+ self.anchor = None
108
+
109
+ # Make sure console cursor is visible. It can get it's width set to 0 with
110
+ # unusual(ie not 100%) os display scaling.
111
+ if not self.cursorWidth():
112
+ self.setCursorWidth(1)
113
+
114
+ def doubleSingleShotSetScrollValue(self, origPercent):
115
+ """This double QTimer.singleShot monkey business seems to be the only way
116
+ to get scroll.maximum() to update properly so that we calc newValue
117
+ correctly. It's quite silly. Apparently, the important part is that
118
+ calling scroll.maximum() has had a pause since the font had been set.
119
+ """
120
+
121
+ def singleShotSetScrollValue(self, origPercent):
122
+ scroll = self.verticalScrollBar()
123
+ maximum = scroll.maximum()
124
+ if maximum is not None:
125
+ newValue = round(origPercent * maximum)
126
+ QTimer.singleShot(1, partial(scroll.setValue, newValue))
127
+
128
+ # The 100 ms timer amount is somewhat arbitrary. It must be more than
129
+ # some value to work, but what that value is is unknown, and may change
130
+ # under various circumstances. Briefly disable updates for smoother transition.
131
+ self.setUpdatesEnabled(False)
132
+ try:
133
+ QTimer.singleShot(100, partial(singleShotSetScrollValue, self, origPercent))
134
+ finally:
135
+ self.setUpdatesEnabled(True)
136
+
137
+ def setConsoleFont(self, font):
138
+ """Set the console's font and adjust the tabStopWidth"""
139
+
140
+ # Capture the scroll bar's current position (by percentage of max)
141
+ origPercent = None
142
+ scroll = self.verticalScrollBar()
143
+ if scroll.maximum():
144
+ origPercent = Fraction(scroll.value(), scroll.maximum())
145
+
146
+ # Set console and completer popup fonts
147
+ self.setFont(font)
148
+ self.completer().popup().setFont(font)
149
+
150
+ # Set the setTabStopWidth for the console's font
151
+ tab_width = 4
152
+ # TODO: Make tab_width a general user setting
153
+ if hasattr(self, "window") and "LoggerWindow" in str(type(self.window())):
154
+ # If parented to a LoggerWindow, get the tab_width from it's workboxes
155
+ workbox = self.window().current_workbox()
156
+ if workbox:
157
+ tab_width = workbox.__tab_width__()
158
+ fontPixelWidth = QFontMetrics(font).width(" ")
159
+ self.setTabStopWidth(fontPixelWidth * tab_width)
160
+
161
+ # Scroll to same relative position where we started
162
+ if origPercent is not None:
163
+ self.doubleSingleShotSetScrollValue(origPercent)
164
+
165
+ def mousePressEvent(self, event):
166
+ """Overload of mousePressEvent to capture click position, so on release, we can
167
+ check release position. If it's the same (ie user clicked vs click-drag to
168
+ select text), we check if user clicked an error hyperlink.
169
+ """
170
+ self.clickPos = event.pos()
171
+ self.anchor = self.anchorAt(event.pos())
172
+ if self.anchor:
173
+ QApplication.setOverrideCursor(Qt.PointingHandCursor)
174
+ return super(ConsolePrEdit, self).mousePressEvent(event)
175
+
176
+ def mouseReleaseEvent(self, event):
177
+ """Overload of mouseReleaseEvent to capture if user has left clicked... Check if
178
+ click position is the same as release position, if so, call errorHyperlink.
179
+ """
180
+ samePos = event.pos() == self.clickPos
181
+ left = event.button() == Qt.LeftButton
182
+ if samePos and left and self.anchor:
183
+ self.errorHyperlink()
184
+
185
+ self.clickPos = None
186
+ self.anchor = None
187
+ QApplication.restoreOverrideCursor()
188
+ return super(ConsolePrEdit, self).mouseReleaseEvent(event)
189
+
190
+ def wheelEvent(self, event):
191
+ """Override of wheelEvent to allow for font resizing by holding ctrl while"""
192
+ # scrolling. If used in LoggerWindow, use that wheel event
193
+ # May not want to import LoggerWindow, so perhaps
194
+ # check by str(type())
195
+ ctrlPressed = event.modifiers() == Qt.ControlModifier
196
+ if ctrlPressed and "LoggerWindow" in str(type(self.window())):
197
+ self.window().wheelEvent(event)
198
+ else:
199
+ QTextEdit.wheelEvent(self, event)
200
+
201
+ def keyReleaseEvent(self, event):
202
+ """Override of keyReleaseEvent to determine when to end navigation of
203
+ previous commands
204
+ """
205
+ if event.key() == Qt.Key_Alt:
206
+ self._prevCommandIndex = 0
207
+ else:
208
+ event.ignore()
209
+
210
+ def errorHyperlink(self):
211
+ """Determine if chosen line is an error traceback file-info line, if so, parse
212
+ the filepath and line number, and attempt to open the module file in the user's
213
+ chosen text editor at the relevant line, using specified Command Prompt pattern.
214
+
215
+ The text editor defaults to SublimeText3, in the normal install directory
216
+ """
217
+ window = self.window()
218
+
219
+ # Bail if Error Hyperlinks setting is not turned on or we don't have an anchor.
220
+ doHyperlink = (
221
+ hasattr(window, 'uiErrorHyperlinksACT')
222
+ and window.uiErrorHyperlinksACT.isChecked()
223
+ and self.anchor
224
+ )
225
+ if not doHyperlink:
226
+ return
227
+
228
+ # info is a comma separated string, in the form: "filename, workboxIdx, lineNum"
229
+ info = self.anchor.split(', ')
230
+ modulePath = info[0]
231
+ workboxIndex = info[1]
232
+ lineNum = info[2]
233
+
234
+ # fetch info from LoggerWindow
235
+ exePath = ''
236
+ cmdTempl = ''
237
+ if hasattr(window, 'textEditorPath'):
238
+ exePath = window.textEditorPath
239
+ cmdTempl = window.textEditorCmdTempl
240
+
241
+ # Bail if not setup properly
242
+ msg = "Cannot use traceback hyperlink. "
243
+ if not exePath:
244
+ msg += "No text editor path defined."
245
+ print(msg)
246
+ return
247
+ if not os.path.exists(exePath):
248
+ msg += "Text editor executable does not exist: {}".format(exePath)
249
+ print(msg)
250
+ return
251
+ if not cmdTempl:
252
+ msg += "No text editor Command Prompt command template defined."
253
+ print(msg)
254
+ return
255
+ if modulePath and not os.path.exists(modulePath):
256
+ msg += "Specified module path does not exist: {}".format(modulePath)
257
+ print(msg)
258
+ return
259
+
260
+ if modulePath:
261
+ # Check if cmdTempl filepaths aren't wrapped in double=quotes to handle
262
+ # spaces. If not, suggest to user to update the template, offering the
263
+ # suggested change.
264
+ pattern = r"(?<!\")({\w+Path})(?!\")"
265
+ repl = r'"\g<1>"'
266
+ quotedCmdTempl = re.sub(pattern, repl, cmdTempl)
267
+ if quotedCmdTempl != cmdTempl:
268
+ # Instantiate dialog to maybe show (unless user previously chose "Don't
269
+ # ask again")
270
+ dialog = SuggestPathQuotesDialog(
271
+ self.window(), cmdTempl, quotedCmdTempl
272
+ )
273
+ self.window().maybeDisplayDialog(dialog)
274
+
275
+ # Refresh cmdTempl in case user just had it changed.
276
+ cmdTempl = window.textEditorCmdTempl
277
+
278
+ # Attempt to create command from template and run the command
279
+ try:
280
+ command = cmdTempl.format(
281
+ exePath=exePath, modulePath=modulePath, lineNum=lineNum
282
+ )
283
+ subprocess.Popen(command)
284
+ except (ValueError, OSError):
285
+ msg = "The provided text editor command template is not valid:\n {}"
286
+ msg = msg.format(cmdTempl)
287
+ print(msg)
288
+ elif workboxIndex is not None:
289
+ group, editor = workboxIndex.split(',')
290
+ lineNum = int(lineNum)
291
+ workbox = window.uiWorkboxTAB.set_current_groups_from_index(
292
+ int(group), int(editor)
293
+ )
294
+ workbox.__goto_line__(lineNum)
295
+ workbox.setFocus()
296
+
297
+ def getPrevCommand(self):
298
+ """Find and display the previous command in stack"""
299
+ self._prevCommandIndex -= 1
300
+
301
+ if abs(self._prevCommandIndex) > len(self._prevCommands):
302
+ self._prevCommandIndex += 1
303
+
304
+ if self._prevCommands:
305
+ self.setCommand()
306
+
307
+ def getNextCommand(self):
308
+ """Find and display the next command in stack"""
309
+ self._prevCommandIndex += 1
310
+ self._prevCommandIndex = min(self._prevCommandIndex, 0)
311
+
312
+ if self._prevCommands:
313
+ self.setCommand()
314
+
315
+ def setCommand(self):
316
+ """Do the displaying of currently chosen command"""
317
+ prevCommand = ''
318
+ if self._prevCommandIndex:
319
+ prevCommand = self._prevCommands[self._prevCommandIndex]
320
+
321
+ cursor = self.textCursor()
322
+ cursor.select(QTextCursor.LineUnderCursor)
323
+ if cursor.selectedText().startswith(self._consolePrompt):
324
+ prevCommand = "{}{}".format(self._consolePrompt, prevCommand)
325
+ cursor.insertText(prevCommand)
326
+ self.setTextCursor(cursor)
327
+
328
+ def clear(self):
329
+ """clears the text in the editor"""
330
+ QTextEdit.clear(self)
331
+ self.startInputLine()
332
+
333
+ def clearToLastPrompt(self):
334
+ # store the current cursor position so we can restore when we are done
335
+ currentCursor = self.textCursor()
336
+ # move to the end of the document so we can search backwards
337
+ cursor = self.textCursor()
338
+ cursor.movePosition(cursor.End)
339
+ self.setTextCursor(cursor)
340
+ # Check if the last line is a empty prompt. If so, then preform two finds so we
341
+ # find the prompt we are looking for instead of this empty prompt
342
+ findCount = (
343
+ 2 if self.toPlainText()[-len(self.prompt()) :] == self.prompt() else 1
344
+ )
345
+ for _ in range(findCount):
346
+ self.find(self.prompt(), QTextDocument.FindBackward)
347
+ # move to the end of the found line, select the rest of the text and remove it
348
+ # preserving history if there is anything to remove.
349
+ cursor = self.textCursor()
350
+ cursor.movePosition(cursor.EndOfLine)
351
+ cursor.movePosition(cursor.End, cursor.KeepAnchor)
352
+ txt = cursor.selectedText()
353
+ if txt:
354
+ self.setTextCursor(cursor)
355
+ self.insertPlainText('')
356
+ # Restore the cursor position to its original location
357
+ self.setTextCursor(currentCursor)
358
+
359
+ def completer(self):
360
+ """returns the completer instance that is associated with this editor"""
361
+ return self._completer
362
+
363
+ def executeString(self, commandText, filename='<ConsolePrEdit>', extraPrint=True):
364
+ if self.clearExecutionTime is not None:
365
+ self.clearExecutionTime()
366
+ cursor = self.textCursor()
367
+ cursor.select(QTextCursor.BlockUnderCursor)
368
+ line = cursor.selectedText()
369
+ if line and line[0] not in string.printable:
370
+ line = line[1:]
371
+
372
+ if line.startswith(self.prompt()) and extraPrint:
373
+ print("")
374
+
375
+ cmdresult = None
376
+ # https://stackoverflow.com/a/29456463
377
+ # If you want to get the result of the code, you have to call eval
378
+ # however eval does not accept multiple statements. For that you need
379
+ # exec which has no Return.
380
+ wasEval = False
381
+ startTime = time.time()
382
+
383
+ try:
384
+ compiled = compile(commandText, filename, 'eval')
385
+ wasEval = True
386
+ except Exception:
387
+ compiled = compile(commandText, filename, 'exec')
388
+
389
+ # We wrap in try / finally so that elapsed time gets updated, even when an
390
+ # exception is raised.
391
+ try:
392
+ if wasEval:
393
+ cmdresult = eval(compiled, __main__.__dict__, __main__.__dict__)
394
+ else:
395
+ exec(compiled, __main__.__dict__, __main__.__dict__)
396
+ finally:
397
+ # Report the total time it took to execute this code.
398
+ if self.reportExecutionTime is not None:
399
+ delta = time.time() - startTime
400
+ self.reportExecutionTime((delta, commandText))
401
+
402
+ # Provide user feedback when running long code execution.
403
+ if self.flash_window and self.flash_time and delta >= self.flash_time:
404
+ if settings.OS_TYPE == "Windows":
405
+ try:
406
+ from casement import utils
407
+ except ImportError:
408
+ # If casement is not installed, flash window is disabled
409
+ pass
410
+ else:
411
+ hwnd = int(self.flash_window.winId())
412
+ utils.flash_window(hwnd)
413
+
414
+ return cmdresult, wasEval
415
+
416
+ def executeCommand(self):
417
+ """executes the current line of code"""
418
+ # grab the command from the line
419
+ block = self.textCursor().block().text()
420
+ p = '{prompt}(.*)'.format(prompt=re.escape(self.prompt()))
421
+ results = re.search(p, block)
422
+ if results:
423
+ commandText = results.groups()[0]
424
+ # if the cursor position is at the end of the line
425
+ if self.textCursor().atEnd():
426
+ # insert a new line
427
+ self.insertPlainText('\n')
428
+
429
+ # update prevCommands list, but only if commandText is not the most
430
+ # recent prevCommand, or there are no previous commands
431
+ hasText = len(commandText) > 0
432
+ prevCmds = self._prevCommands
433
+ notPrevCmd = not prevCmds or prevCmds[-1] != commandText
434
+ if hasText and notPrevCmd:
435
+ self._prevCommands.append(commandText)
436
+ # limit length of prevCommand list to max number of prev commands
437
+ self._prevCommands = self._prevCommands[-1 * self._prevCommandsMax :]
438
+
439
+ # evaluate the command
440
+ cmdresult, wasEval = self.executeString(commandText)
441
+
442
+ # print the resulting commands
443
+ if cmdresult is not None:
444
+ # When writing to additional stdout's not including a new line
445
+ # makes the output not match the formatting you get inside the
446
+ # console.
447
+ self.write(u'{}\n'.format(cmdresult))
448
+ # NOTE: I am using u'' above so unicode strings in python 2
449
+ # don't get converted to str objects.
450
+
451
+ self.startInputLine()
452
+
453
+ # otherwise, move the command to the end of the line
454
+ else:
455
+ self.startInputLine()
456
+ self.insertPlainText(commandText)
457
+
458
+ # if no command, then start a new line
459
+ else:
460
+ self.startInputLine()
461
+
462
+ def flush(self):
463
+ pass
464
+
465
+ def focusInEvent(self, event):
466
+ """overload the focus in event to ensure the completer has the proper widget"""
467
+ if self.completer():
468
+ self.completer().setWidget(self)
469
+ QTextEdit.focusInEvent(self, event)
470
+
471
+ def insertCompletion(self, completion):
472
+ """inserts the completion text into the editor"""
473
+ if self.completer().widget() == self:
474
+ cursor = self.textCursor()
475
+ cursor.select(QTextCursor.WordUnderCursor)
476
+ cursor.insertText(completion)
477
+ self.setTextCursor(cursor)
478
+
479
+ def insertFromMimeData(self, mimeData):
480
+ html = False
481
+ if mimeData.hasHtml():
482
+ txt = mimeData.html()
483
+ html = True
484
+ else:
485
+ txt = mimeData.text()
486
+
487
+ doc = QTextDocument()
488
+
489
+ if html:
490
+ doc.setHtml(txt)
491
+ else:
492
+ doc.setPlainText(txt)
493
+
494
+ txt = doc.toPlainText()
495
+
496
+ exp = re.compile(
497
+ (
498
+ r'[^A-Za-z0-9\~\!\@\#\$\%\^\&\*\(\)\_\+\{\}\|\:'
499
+ r'\"\<\>\?\`\-\=\[\]\\\;\'\,\.\/ \t\n]'
500
+ )
501
+ )
502
+ newText = text(txt)
503
+ for each in exp.findall(newText):
504
+ newText = newText.replace(each, '?')
505
+
506
+ self.insertPlainText(newText)
507
+
508
+ def isatty(self):
509
+ """Return True if the stream is interactive (i.e., connected to a terminal/tty
510
+ device).
511
+ """
512
+ # This method is required for pytest to run in a DCC. Returns False so the
513
+ # output does not contain cursor control characters that disrupt the visual
514
+ # display of the output.
515
+ return False
516
+
517
+ def lastError(self):
518
+ try:
519
+ return ''.join(
520
+ traceback.format_exception(
521
+ sys.last_type, sys.last_value, sys.last_traceback
522
+ )
523
+ )
524
+ except AttributeError:
525
+ # last_traceback, last_type and last_value do not always exist
526
+ return ''
527
+
528
+ def keyPressEvent(self, event):
529
+ """overload the key press event to handle custom events"""
530
+
531
+ completer = self.completer()
532
+
533
+ # Define prefix so we can determine if the exact prefix is in
534
+ # completions and highlight it. We must manually add the currently typed
535
+ # character, or remove it if backspace or delete has just been pressed.
536
+ key = event.text()
537
+ _, prefix = completer.currentObject(scope=__main__.__dict__)
538
+ isBackspaceOrDel = event.key() in (Qt.Key_Backspace, Qt.Key_Delete)
539
+ if key.isalnum() or key in ("-", "_"):
540
+ prefix += str(key)
541
+ elif isBackspaceOrDel and prefix:
542
+ prefix = prefix[:-1]
543
+
544
+ if completer and event.key() in (
545
+ Qt.Key_Backspace,
546
+ Qt.Key_Delete,
547
+ Qt.Key_Escape,
548
+ ):
549
+ completer.hideDocumentation()
550
+
551
+ # enter || return keys will execute the command
552
+ if event.key() in (Qt.Key_Return, Qt.Key_Enter):
553
+ if completer.popup().isVisible():
554
+ completer.clear()
555
+ event.ignore()
556
+ else:
557
+ self.executeCommand()
558
+
559
+ # home key will move the cursor to home
560
+ elif event.key() == Qt.Key_Home:
561
+ self.moveToHome()
562
+
563
+ # otherwise, ignore the event for completion events
564
+ elif event.key() in (Qt.Key_Tab, Qt.Key_Backtab):
565
+ if not completer.popup().isVisible():
566
+ # The completer does not get updated if its not visible while typing.
567
+ # We are about to complete the text using it so ensure its updated.
568
+ completer.refreshList(scope=__main__.__dict__)
569
+ completer.popup().setCurrentIndex(
570
+ completer.completionModel().index(0, 0)
571
+ )
572
+ # Insert the correct text and clear the completion model
573
+ index = completer.popup().currentIndex()
574
+ self.insertCompletion(index.data(Qt.DisplayRole))
575
+ completer.clear()
576
+
577
+ elif event.key() == Qt.Key_Escape and completer.popup().isVisible():
578
+ completer.clear()
579
+
580
+ # other wise handle the keypress
581
+ else:
582
+ # define special key sequences
583
+ modifiers = QApplication.instance().keyboardModifiers()
584
+ ctrlSpace = event.key() == Qt.Key_Space and modifiers == Qt.ControlModifier
585
+ ctrlM = event.key() == Qt.Key_M and modifiers == Qt.ControlModifier
586
+ ctrlI = event.key() == Qt.Key_I and modifiers == Qt.ControlModifier
587
+
588
+ # Process all events we do not want to override
589
+ if not (ctrlSpace or ctrlM or ctrlI):
590
+ QTextEdit.keyPressEvent(self, event)
591
+
592
+ window = self.window()
593
+ if ctrlI:
594
+ hasToggleCase = hasattr(window, 'toggleCaseSensitive')
595
+ if hasToggleCase:
596
+ window.toggleCaseSensitive()
597
+ if ctrlM:
598
+ hasCycleMode = hasattr(window, 'cycleCompleterMode')
599
+ if hasCycleMode:
600
+ window.cycleCompleterMode()
601
+
602
+ # check for particular events for the completion
603
+ if completer:
604
+ # look for documentation popups
605
+ if event.key() == Qt.Key_ParenLeft:
606
+ rect = self.cursorRect()
607
+ point = self.mapToGlobal(QPoint(rect.x(), rect.y()))
608
+ completer.showDocumentation(pos=point, scope=__main__.__dict__)
609
+
610
+ # hide documentation popups
611
+ elif event.key() == Qt.Key_ParenRight:
612
+ completer.hideDocumentation()
613
+
614
+ # determine if we need to show the popup or if it already is visible, we
615
+ # need to update it
616
+ elif (
617
+ event.key() == Qt.Key_Period
618
+ or event.key() == Qt.Key_Escape
619
+ or completer.popup().isVisible()
620
+ or ctrlSpace
621
+ or ctrlI
622
+ or ctrlM
623
+ or completer.wasCompletingCounter
624
+ ):
625
+ completer.refreshList(scope=__main__.__dict__)
626
+
627
+ model = completer.completionModel()
628
+ index = model.index(0, 0)
629
+
630
+ # If option chosen, if the exact prefix exists in the
631
+ # possible completions, highlight it, even if it's not the
632
+ # topmost completion.
633
+ if self.window().uiHighlightExactCompletionACT.isChecked():
634
+ for i in range(completer.completionCount()):
635
+ completer.setCurrentRow(i)
636
+ curCompletion = completer.currentCompletion()
637
+ if prefix == curCompletion:
638
+ index = model.index(i, 0)
639
+ break
640
+ elif prefix == curCompletion.lower():
641
+ index = model.index(i, 0)
642
+ break
643
+
644
+ # Set completer current Row, so finishing the completer will use
645
+ # correct text
646
+ completer.setCurrentRow(index.row())
647
+
648
+ # Make sure that current selection is visible, ie scroll to it
649
+ completer.popup().scrollTo(index, QAbstractItemView.EnsureVisible)
650
+
651
+ # show the completer for the rect
652
+ rect = self.cursorRect()
653
+ rect.setWidth(
654
+ completer.popup().sizeHintForColumn(0)
655
+ + completer.popup().verticalScrollBar().sizeHint().width()
656
+ )
657
+ completer.complete(rect)
658
+
659
+ if completer.popup().isVisible():
660
+ completer.wasCompleting = True
661
+ completer.wasCompletingCounter = 0
662
+
663
+ if completer.wasCompleting and not completer.popup().isVisible():
664
+ wasCompletingCounterMax = completer.wasCompletingCounterMax
665
+ if completer.wasCompletingCounter <= wasCompletingCounterMax:
666
+ if event.key() not in (Qt.Key_Backspace, Qt.Key_Left):
667
+ completer.wasCompletingCounter += 1
668
+ else:
669
+ completer.wasCompletingCounter = 0
670
+ completer.wasCompleting = False
671
+
672
+ def moveToHome(self):
673
+ """moves the cursor to the home location"""
674
+ mode = QTextCursor.MoveAnchor
675
+ # select the home
676
+ if QApplication.instance().keyboardModifiers() == Qt.ShiftModifier:
677
+ mode = QTextCursor.KeepAnchor
678
+ # grab the cursor
679
+ cursor = self.textCursor()
680
+ if QApplication.instance().keyboardModifiers() == Qt.ControlModifier:
681
+ # move to the top of the document if control is pressed
682
+ cursor.movePosition(QTextCursor.Start)
683
+ else:
684
+ # Otherwise just move it to the start of the line
685
+ cursor.movePosition(QTextCursor.StartOfBlock, mode)
686
+ # move the cursor to the end of the prompt.
687
+ cursor.movePosition(QTextCursor.Right, mode, len(self.prompt()))
688
+ self.setTextCursor(cursor)
689
+
690
+ def outputPrompt(self):
691
+ """The prompt used to output a result."""
692
+ return self._outputPrompt
693
+
694
+ def prompt(self):
695
+ return self._consolePrompt
696
+
697
+ def setCompleter(self, completer):
698
+ """sets the completer instance for this widget"""
699
+ if completer:
700
+ self._completer = completer
701
+ completer.setWidget(self)
702
+ completer.activated.connect(self.insertCompletion)
703
+
704
+ def showEvent(self, event):
705
+ # _firstShow is used to ensure the first imput prompt is styled by any active
706
+ # stylesheet
707
+ if self._firstShow:
708
+ self.startInputLine()
709
+ self._firstShow = False
710
+ super(ConsolePrEdit, self).showEvent(event)
711
+
712
+ def startInputLine(self):
713
+ """create a new command prompt line"""
714
+ self.startPrompt(self.prompt())
715
+ self._prevCommandIndex = 0
716
+
717
+ def startPrompt(self, prompt):
718
+ """create a new command prompt line with the given prompt
719
+
720
+ Args:
721
+ prompt(str): The prompt to start the line with. If this prompt
722
+ is already the only text on the last line this function does nothing.
723
+ """
724
+ self.moveCursor(QTextCursor.End)
725
+
726
+ # if this is not already a new line
727
+ if self.textCursor().block().text() != prompt:
728
+ charFormat = QTextCharFormat()
729
+ self.setCurrentCharFormat(charFormat)
730
+
731
+ inputstr = prompt
732
+ if self.textCursor().block().text():
733
+ inputstr = '\n' + inputstr
734
+
735
+ self.insertPlainText(inputstr)
736
+
737
+ scroll = self.verticalScrollBar()
738
+ maximum = scroll.maximum()
739
+ if maximum is not None:
740
+ scroll.setValue(maximum)
741
+
742
+ def startOutputLine(self):
743
+ """Create a new line to show output text."""
744
+ self.startPrompt(self._outputPrompt)
745
+
746
+ def removeCurrentLine(self):
747
+ self.moveCursor(QTextCursor.End, QTextCursor.MoveAnchor)
748
+ self.moveCursor(QTextCursor.StartOfLine, QTextCursor.MoveAnchor)
749
+ self.moveCursor(QTextCursor.End, QTextCursor.KeepAnchor)
750
+ self.textCursor().removeSelectedText()
751
+ self.textCursor().deletePreviousChar()
752
+ self.insertPlainText("\n")
753
+
754
+ def parseErrorHyperLinkInfo(self, txt):
755
+ """Determine if txt is a File-info line from a traceback, and if so, return info
756
+ dict.
757
+ """
758
+ lineMarker = '", line '
759
+ ret = None
760
+
761
+ filenameEnd = txt.find(lineMarker)
762
+ if txt[:8] == ' File "' and filenameEnd >= 0:
763
+ filename = txt[8:filenameEnd]
764
+ lineNumStart = filenameEnd + len(lineMarker)
765
+ lineNumEnd = txt.find(',', lineNumStart)
766
+ if lineNumEnd == -1:
767
+ lineNumEnd = len(txt)
768
+ lineNum = txt[lineNumStart:lineNumEnd]
769
+ ret = {
770
+ 'filename': filename,
771
+ 'fileStart': 8,
772
+ 'fileEnd': filenameEnd,
773
+ 'lineNum': lineNum,
774
+ }
775
+
776
+ return ret
777
+
778
+ def write(self, msg, error=False):
779
+ """write the message to the logger"""
780
+ # Convert the stream_manager's stream to the boolean value this function expects
781
+ error = error == stream.STDERR
782
+ # Check that we haven't been garbage collected before trying to write.
783
+ # This can happen while shutting down a QApplication like Nuke.
784
+ if QtCompat.isValid(self):
785
+ window = self.window()
786
+ doHyperlink = (
787
+ hasattr(window, 'uiErrorHyperlinksACT')
788
+ and window.uiErrorHyperlinksACT.isChecked()
789
+ )
790
+ self.moveCursor(QTextCursor.End)
791
+
792
+ charFormat = QTextCharFormat()
793
+ if not error:
794
+ charFormat.setForeground(self.stdoutColor)
795
+ else:
796
+ charFormat.setForeground(self.errorMessageColor)
797
+ self.setCurrentCharFormat(charFormat)
798
+
799
+ # If showing Error Hyperlinks... Sometimes (when a syntax error, at least),
800
+ # the last File-Info line of a traceback is issued in multiple messages
801
+ # starting with unicode paragraph separator (r"\u2029") and followed by a
802
+ # newline, so our normal string checks search won't work. Instead, we'll
803
+ # manually reconstruct the line. If msg is a newline, grab that current line
804
+ # and check it. If it matches,proceed using that line as msg
805
+ cursor = self.textCursor()
806
+ info = None
807
+
808
+ if doHyperlink and msg == '\n':
809
+ cursor.select(QTextCursor.BlockUnderCursor)
810
+ line = cursor.selectedText()
811
+
812
+ # Remove possible leading unicode paragraph separator, which really
813
+ # messes up the works
814
+ if line and line[0] not in string.printable:
815
+ line = line[1:]
816
+
817
+ info = self.parseErrorHyperLinkInfo(line)
818
+ if info:
819
+ cursor.insertText("\n")
820
+ msg = "{}\n".format(line)
821
+
822
+ # If showing Error Hyperlinks, display underline output, otherwise
823
+ # display normal output. Exclude ConsolePrEdits
824
+ info = info if info else self.parseErrorHyperLinkInfo(msg)
825
+ filename = info.get("filename", "") if info else ""
826
+ isConsolePrEdit = '<ConsolePrEdit>' in filename
827
+
828
+ if info and doHyperlink and not isConsolePrEdit:
829
+ fileStart = info.get("fileStart")
830
+ fileEnd = info.get("fileEnd")
831
+ lineNum = info.get("lineNum")
832
+
833
+ isWorkbox = '<WorkboxSelection>' in filename or '<Workbox>' in filename
834
+ if isWorkbox:
835
+ split = filename.split(':')
836
+ workboxIdx = split[-1]
837
+ filename = ''
838
+ else:
839
+ filename = filename
840
+ workboxIdx = ''
841
+ href = '{}, {}, {}'.format(filename, workboxIdx, lineNum)
842
+
843
+ # Insert initial, non-underlined text
844
+ cursor.insertText(msg[:fileStart])
845
+
846
+ # Insert hyperlink
847
+ fmt = cursor.charFormat()
848
+ fmt.setAnchor(True)
849
+ fmt.setAnchorHref(href)
850
+ fmt.setFontUnderline(True)
851
+ toolTip = "Open {} at line number {}".format(filename, lineNum)
852
+ fmt.setToolTip(toolTip)
853
+ cursor.insertText(msg[fileStart:fileEnd], fmt)
854
+
855
+ # Insert the rest of the msg
856
+ fmt.setAnchor(False)
857
+ fmt.setAnchorHref('')
858
+ fmt.setFontUnderline(False)
859
+ fmt.setToolTip('')
860
+ cursor.insertText(msg[fileEnd:], fmt)
861
+ else:
862
+ # Non-hyperlink output
863
+ self.insertPlainText(msg)
864
+
865
+ # if a outputPipe was provided, write the message to that pipe
866
+ if self.outputPipe:
867
+ self.outputPipe(msg, error=error)