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,1385 @@
1
+ from __future__ import absolute_import, print_function
2
+
3
+ import itertools
4
+ import json
5
+ import os
6
+ import re
7
+ import sys
8
+ import warnings
9
+ from builtins import bytes
10
+ from datetime import datetime, timedelta
11
+ from functools import partial
12
+
13
+ import __main__
14
+ from Qt import QtCompat, QtCore, QtWidgets
15
+ from Qt.QtCore import QByteArray, Qt, QTimer, Signal, Slot
16
+ from Qt.QtGui import QCursor, QFont, QIcon, QTextCursor
17
+ from Qt.QtWidgets import (
18
+ QApplication,
19
+ QFontDialog,
20
+ QInputDialog,
21
+ QMessageBox,
22
+ QTextBrowser,
23
+ QToolTip,
24
+ QVBoxLayout,
25
+ )
26
+
27
+ from .. import (
28
+ DEFAULT_CORE_NAME,
29
+ about_preditor,
30
+ core,
31
+ debug,
32
+ get_core_name,
33
+ osystem,
34
+ plugins,
35
+ prefs,
36
+ resourcePath,
37
+ )
38
+ from ..delayable_engine import DelayableEngine
39
+ from ..gui import Dialog, Window, loadUi, tab_widget_for_tab
40
+ from ..gui.fuzzy_search.fuzzy_search import FuzzySearch
41
+ from ..gui.group_tab_widget.grouped_tab_models import GroupTabListItemModel
42
+ from ..logging_config import LoggingConfig
43
+ from ..utils import stylesheets
44
+ from .completer import CompleterMode
45
+ from .level_buttons import LoggingLevelButton
46
+ from .set_text_editor_path_dialog import SetTextEditorPathDialog
47
+ from .status_label import StatusLabel
48
+
49
+
50
+ class WorkboxPages:
51
+ """Nice names for the uiWorkboxSTACK indexes."""
52
+
53
+ Options = 0
54
+ Workboxes = 1
55
+
56
+
57
+ class WorkboxName(str):
58
+ """The joined name of a workbox `group/workbox` with access to its parts.
59
+
60
+ This subclass provides properties for the group and workbox values separately.
61
+ """
62
+
63
+ def __new__(cls, group, workbox):
64
+ txt = "/".join((group, workbox))
65
+ ret = super().__new__(cls, txt)
66
+ # Preserve the imitable nature of str's by using properties without setters.
67
+ ret._group = group
68
+ ret._workbox = workbox
69
+ return ret
70
+
71
+ @property
72
+ def group(self):
73
+ """The tab name of the group tab that contains the workbox."""
74
+ return self._group
75
+
76
+ @property
77
+ def workbox(self):
78
+ """The workbox of the tab for this workbox inside of the group."""
79
+ return self._workbox
80
+
81
+
82
+ class LoggerWindow(Window):
83
+ _instance = None
84
+ styleSheetChanged = Signal(str)
85
+
86
+ def __init__(self, parent, name=None, run_workbox=False, standalone=False):
87
+ super(LoggerWindow, self).__init__(parent=parent)
88
+ self.name = name if name else get_core_name()
89
+ self._stylesheet = 'Bright'
90
+
91
+ # Create timer to autohide status messages
92
+ self.statusTimer = QTimer()
93
+ self.statusTimer.setSingleShot(True)
94
+
95
+ # Store the previous time a font-resize wheel event was triggered to prevent
96
+ # rapid-fire WheelEvents. Initialize to the current time.
97
+ self.previousFontResizeTime = datetime.now()
98
+
99
+ self.setWindowIcon(QIcon(resourcePath('img/preditor.png')))
100
+ loadUi(__file__, self)
101
+
102
+ self.uiConsoleTXT.flash_window = self
103
+ self.uiConsoleTXT.clearExecutionTime = self.clearExecutionTime
104
+ self.uiConsoleTXT.reportExecutionTime = self.reportExecutionTime
105
+ self.uiClearToLastPromptACT.triggered.connect(
106
+ self.uiConsoleTXT.clearToLastPrompt
107
+ )
108
+ # If we don't disable this shortcut Qt won't respond to this classes or
109
+ # the ConsolePrEdit's
110
+ self.uiConsoleTXT.uiClearToLastPromptACT.setShortcut('')
111
+
112
+ # create the status reporting label
113
+ self.uiStatusLBL = StatusLabel(self)
114
+ self.uiMenuBar.setCornerWidget(self.uiStatusLBL)
115
+
116
+ # create the workbox tabs
117
+ self._currentTab = -1
118
+ self._reloadRequested = set()
119
+ # Setup delayable system
120
+ self.delayable_engine = DelayableEngine.instance('logger', self)
121
+
122
+ self.uiWorkboxTAB.editor_kwargs = dict(
123
+ console=self.uiConsoleTXT, delayable_engine=self.delayable_engine.name
124
+ )
125
+
126
+ # Create additional buttons in toolbar.
127
+ self.uiLoggingLevelBTN = LoggingLevelButton(self)
128
+ self.uiConsoleTOOLBAR.insertWidget(
129
+ self.uiRunSelectedACT,
130
+ self.uiLoggingLevelBTN,
131
+ )
132
+ self.uiConsoleTOOLBAR.insertSeparator(self.uiRunSelectedACT)
133
+
134
+ # Configure Find in Workboxes
135
+ self.uiFindInWorkboxesWGT.hide()
136
+ self.uiFindInWorkboxesWGT.managers.append(self.uiWorkboxTAB)
137
+ self.uiFindInWorkboxesWGT.console = self.console()
138
+
139
+ # Initial configuration of the logToFile feature
140
+ self._logToFilePath = None
141
+ self._stds = None
142
+ self.uiLogToFileClearACT.setVisible(False)
143
+
144
+ self.uiRestartACT.triggered.connect(self.restartLogger)
145
+ self.uiCloseLoggerACT.triggered.connect(self.closeLogger)
146
+
147
+ self.uiRunAllACT.triggered.connect(self.execAll)
148
+ # Even though the RunSelected actions (with shortcuts) are connected
149
+ # here, this only affects if the action is chosen from the menu. The
150
+ # shortcuts are always intercepted by the workbox document editor. To
151
+ # handle this, the workbox.keyPressEvent method will perceive the
152
+ # shortcut press, and call .execSelected, which will then ultimately call
153
+ # workbox.__exec_selected__
154
+ self.uiRunSelectedACT.triggered.connect(
155
+ partial(self.execSelected, truncate=True)
156
+ )
157
+ self.uiRunSelectedDontTruncateACT.triggered.connect(
158
+ partial(self.execSelected, truncate=False)
159
+ )
160
+
161
+ self.uiConsoleAutoCompleteEnabledACT.toggled.connect(
162
+ partial(self.setAutoCompleteEnabled, console=True)
163
+ )
164
+ self.uiWorkboxAutoCompleteEnabledACT.toggled.connect(
165
+ partial(self.setAutoCompleteEnabled, console=False)
166
+ )
167
+
168
+ self.uiAutoCompleteCaseSensitiveACT.toggled.connect(self.setCaseSensitive)
169
+
170
+ self.uiSelectMonospaceFontACT.triggered.connect(
171
+ partial(self.selectFont, monospace=True)
172
+ )
173
+ self.uiSelectProportionalFontACT.triggered.connect(
174
+ partial(self.selectFont, proportional=True)
175
+ )
176
+ self.uiSelectAllFontACT.triggered.connect(
177
+ partial(self.selectFont, monospace=True, proportional=True)
178
+ )
179
+
180
+ # Setup ability to cycle completer mode, and create action for each mode
181
+ self.completerModeCycle = itertools.cycle(CompleterMode)
182
+ # create CompleterMode submenu
183
+ defaultMode = next(self.completerModeCycle)
184
+ for mode in CompleterMode:
185
+ modeName = mode.displayName()
186
+ action = self.uiCompleterModeMENU.addAction(modeName)
187
+ action.setObjectName('ui{}ModeACT'.format(modeName))
188
+ action.setData(mode)
189
+ action.setCheckable(True)
190
+ action.setChecked(mode == defaultMode)
191
+ completerMode = CompleterMode(mode)
192
+ action.setToolTip(completerMode.toolTip())
193
+ action.triggered.connect(partial(self.selectCompleterMode, action))
194
+
195
+ self.uiCompleterModeMENU.addSeparator()
196
+ action = self.uiCompleterModeMENU.addAction('Cycle mode')
197
+ action.setObjectName('uiCycleModeACT')
198
+ action.setShortcut(Qt.CTRL | Qt.Key_M)
199
+ action.triggered.connect(self.cycleCompleterMode)
200
+ self.uiCompleterModeMENU.hovered.connect(self.handleMenuHovered)
201
+
202
+ # Workbox add/remove
203
+ self.uiNewWorkboxACT.triggered.connect(
204
+ lambda: self.uiWorkboxTAB.add_new_tab(group=True)
205
+ )
206
+ self.uiCloseWorkboxACT.triggered.connect(self.uiWorkboxTAB.close_current_tab)
207
+
208
+ # Browse previous commands
209
+ self.uiGetPrevCmdACT.triggered.connect(self.getPrevCommand)
210
+ self.uiGetNextCmdACT.triggered.connect(self.getNextCommand)
211
+
212
+ # Focus to console or to workbox, optionally copy seleciton or line
213
+ self.uiFocusToConsoleACT.triggered.connect(self.focusToConsole)
214
+ self.uiCopyToConsoleACT.triggered.connect(self.copyToConsole)
215
+ self.uiFocusToWorkboxACT.triggered.connect(self.focusToWorkbox)
216
+ self.uiCopyToWorkboxACT.triggered.connect(self.copyToWorkbox)
217
+
218
+ # Navigate workbox tabs
219
+ self.uiNextTabACT.triggered.connect(self.nextTab)
220
+ self.uiPrevTabACT.triggered.connect(self.prevTab)
221
+
222
+ self.uiTab1ACT.triggered.connect(partial(self.gotoTabByIndex, 1))
223
+ self.uiTab2ACT.triggered.connect(partial(self.gotoTabByIndex, 2))
224
+ self.uiTab3ACT.triggered.connect(partial(self.gotoTabByIndex, 3))
225
+ self.uiTab4ACT.triggered.connect(partial(self.gotoTabByIndex, 4))
226
+ self.uiTab5ACT.triggered.connect(partial(self.gotoTabByIndex, 5))
227
+ self.uiTab6ACT.triggered.connect(partial(self.gotoTabByIndex, 6))
228
+ self.uiTab7ACT.triggered.connect(partial(self.gotoTabByIndex, 7))
229
+ self.uiTab8ACT.triggered.connect(partial(self.gotoTabByIndex, 8))
230
+ self.uiTabLastACT.triggered.connect(partial(self.gotoTabByIndex, -1))
231
+
232
+ self.uiGroup1ACT.triggered.connect(partial(self.gotoGroupByIndex, 1))
233
+ self.uiGroup2ACT.triggered.connect(partial(self.gotoGroupByIndex, 2))
234
+ self.uiGroup3ACT.triggered.connect(partial(self.gotoGroupByIndex, 3))
235
+ self.uiGroup4ACT.triggered.connect(partial(self.gotoGroupByIndex, 4))
236
+ self.uiGroup5ACT.triggered.connect(partial(self.gotoGroupByIndex, 5))
237
+ self.uiGroup6ACT.triggered.connect(partial(self.gotoGroupByIndex, 6))
238
+ self.uiGroup7ACT.triggered.connect(partial(self.gotoGroupByIndex, 7))
239
+ self.uiGroup8ACT.triggered.connect(partial(self.gotoGroupByIndex, 8))
240
+ self.uiGroupLastACT.triggered.connect(partial(self.gotoGroupByIndex, -1))
241
+
242
+ self.uiFocusNameACT.triggered.connect(self.show_focus_name)
243
+
244
+ self.uiCommentToggleACT.triggered.connect(self.comment_toggle)
245
+
246
+ self.uiSpellCheckEnabledACT.toggled.connect(self.setSpellCheckEnabled)
247
+ self.uiIndentationsTabsACT.toggled.connect(self.updateIndentationsUseTabs)
248
+ self.uiCopyTabsToSpacesACT.toggled.connect(self.updateCopyIndentsAsSpaces)
249
+ self.uiWordWrapACT.toggled.connect(self.setWordWrap)
250
+ self.uiResetWarningFiltersACT.triggered.connect(warnings.resetwarnings)
251
+ self.uiLogToFileACT.triggered.connect(self.installLogToFile)
252
+ self.uiLogToFileClearACT.triggered.connect(self.clearLogToFile)
253
+ self.uiClearLogACT.triggered.connect(self.clearLog)
254
+ self.uiSaveConsoleSettingsACT.triggered.connect(
255
+ lambda: self.recordPrefs(manual=True)
256
+ )
257
+ self.uiClearBeforeRunningACT.triggered.connect(self.setClearBeforeRunning)
258
+ self.uiEditorVerticalACT.toggled.connect(self.adjustWorkboxOrientation)
259
+ self.uiEnvironmentVarsACT.triggered.connect(self.showEnvironmentVars)
260
+ self.uiBackupPreferencesACT.triggered.connect(self.backupPreferences)
261
+ self.uiBrowsePreferencesACT.triggered.connect(self.browsePreferences)
262
+ self.uiAboutPreditorACT.triggered.connect(self.show_about)
263
+ self.uiSetFlashWindowIntervalACT.triggered.connect(self.setFlashWindowInterval)
264
+
265
+ self.uiSetPreferredTextEditorPathACT.triggered.connect(
266
+ self.openSetPreferredTextEditorDialog
267
+ )
268
+
269
+ # Tooltips - Qt4 doesn't have a ToolTipsVisible method, so we fake it
270
+ regEx = ".*"
271
+ menus = self.findChildren(QtWidgets.QMenu, QtCore.QRegExp(regEx))
272
+ for menu in menus:
273
+ menu.hovered.connect(self.handleMenuHovered)
274
+
275
+ self.uiClearLogACT.setIcon(QIcon(resourcePath('img/close-thick.png')))
276
+ self.uiNewWorkboxACT.setIcon(QIcon(resourcePath('img/file-plus.png')))
277
+ self.uiCloseWorkboxACT.setIcon(QIcon(resourcePath('img/file-remove.png')))
278
+ self.uiSaveConsoleSettingsACT.setIcon(
279
+ QIcon(resourcePath('img/content-save.png'))
280
+ )
281
+ self.uiAboutPreditorACT.setIcon(QIcon(resourcePath('img/information.png')))
282
+ self.uiRestartACT.setIcon(QIcon(resourcePath('img/restart.svg')))
283
+ self.uiCloseLoggerACT.setIcon(QIcon(resourcePath('img/close-thick.png')))
284
+
285
+ # Make action shortcuts available anywhere in the Logger
286
+ self.addAction(self.uiClearLogACT)
287
+
288
+ self.dont_ask_again = []
289
+
290
+ # Load any plugins that modify the LoggerWindow
291
+ self.plugins = {}
292
+ for name, plugin in plugins.loggerwindow():
293
+ self.plugins[name] = plugin(self)
294
+
295
+ self.restorePrefs()
296
+
297
+ # add stylesheet menu options.
298
+ for style_name in stylesheets.stylesheets():
299
+ action = self.uiStyleMENU.addAction(style_name)
300
+ action.setObjectName('ui{}ACT'.format(style_name))
301
+ action.setCheckable(True)
302
+ action.setChecked(self._stylesheet == style_name)
303
+ action.triggered.connect(partial(self.setStyleSheet, style_name))
304
+
305
+ self.uiConsoleTOOLBAR.show()
306
+ loggerName = QApplication.instance().translate(
307
+ 'PrEditorWindow', DEFAULT_CORE_NAME
308
+ )
309
+ self.setWindowTitle(
310
+ '{} - {} - {} {}-bit'.format(
311
+ loggerName,
312
+ self.name,
313
+ '{}.{}.{}'.format(*sys.version_info[:3]),
314
+ osystem.getPointerSize(),
315
+ )
316
+ )
317
+
318
+ self.setWorkboxFontBasedOnConsole()
319
+ self.setEditorChooserFontBasedOnConsole()
320
+
321
+ self.setup_run_workbox()
322
+
323
+ if not standalone:
324
+ # This action only is valid when running in standalone mode
325
+ self.uiRestartACT.setVisible(False)
326
+
327
+ # Run the current workbox after the LoggerWindow is shown.
328
+ if run_workbox:
329
+ # By using two singleShot timers, we can show and draw the LoggerWindow,
330
+ # then call execAll. This makes it easier to see what code you are running
331
+ # before it has finished running completely.
332
+ # QTimer.singleShot(0, lambda: QTimer.singleShot(0, self.execAll))
333
+ QTimer.singleShot(
334
+ 0, lambda: QTimer.singleShot(0, lambda: self.run_workbox(run_workbox))
335
+ )
336
+
337
+ @Slot()
338
+ def apply_options(self):
339
+ """Apply editor options the user chose on the WorkboxPage.Options page."""
340
+ editor_cls_name, editor_cls = plugins.editor(
341
+ self.uiEditorChooserWGT.editor_name()
342
+ )
343
+ if editor_cls_name is None:
344
+ return
345
+ if editor_cls_name != self.editor_cls_name:
346
+ self.editor_cls_name = editor_cls_name
347
+ self.uiWorkboxTAB.editor_cls = editor_cls
348
+ # We need to change the editor, save all prefs
349
+ self.recordPrefs()
350
+ # Clear the uiWorkboxTAB
351
+ self.uiWorkboxTAB.clear()
352
+ # Restore prefs to populate the tabs
353
+ self.restorePrefs()
354
+
355
+ self.update_workbox_stack()
356
+
357
+ def comment_toggle(self):
358
+ self.current_workbox().__comment_toggle__()
359
+
360
+ def current_workbox(self):
361
+ """Returns the current workbox for the current tab group."""
362
+ return self.uiWorkboxTAB.current_groups_widget()
363
+
364
+ @classmethod
365
+ def name_for_workbox(cls, workbox):
366
+ """Returns the name for a given workbox or None if not valid.
367
+
368
+ The name is a `WorkboxName` object showing the group and name joined by
369
+ a `/`.
370
+
371
+ Args:
372
+ workbox: The workbox to get the name of. If None is passed then it
373
+ will return the name of the current workbox.
374
+
375
+ Returns:
376
+ The name of the widget as a `WorkboxName` object showing the group
377
+ and name joined by a `/`. If workbox is not valid for the LoggerWindow
378
+ instance then None is returned.
379
+ """
380
+
381
+ if workbox is None:
382
+ # if the workbox was not provided use the current workbox
383
+ logger = cls.instance()
384
+ index = logger.uiWorkboxTAB.currentIndex()
385
+ group = logger.uiWorkboxTAB.tabText(index)
386
+ group_widget = logger.uiWorkboxTAB.currentWidget()
387
+ index = group_widget.currentIndex()
388
+ name = group_widget.tabText(index)
389
+ return WorkboxName(group, name)
390
+
391
+ # Otherwise resolve from the parent widgets.
392
+ # Get the parent QTabWidget of the workbox
393
+ workbox_tab_widget = tab_widget_for_tab(workbox)
394
+ if not workbox_tab_widget:
395
+ return None
396
+ # Get the group QTabWidget of the parent QTabWidget of the workbox
397
+ group_widget = tab_widget_for_tab(workbox_tab_widget)
398
+ if not group_widget:
399
+ return None
400
+
401
+ # Get the group name
402
+ index = group_widget.indexOf(workbox_tab_widget)
403
+ group = group_widget.tabText(index)
404
+
405
+ index = workbox_tab_widget.indexOf(workbox)
406
+ name = workbox_tab_widget.tabText(index)
407
+ return WorkboxName(group, name)
408
+
409
+ @classmethod
410
+ def workbox_for_name(cls, name, show=False, visible=False):
411
+ """Used to find a workbox for a given name. It accepts a string matching
412
+ the "{group}/{workbox}" format, or if True, the current workbox.
413
+
414
+ Args:
415
+ name(str, boolean): Used to define which workbox to run.
416
+ show (bool, optional): If a workbox is found, call `__show__` on it
417
+ to ensure that it is initialized and its text is loaded.
418
+ visible (bool, optional): Make the this workbox visible if found.
419
+ """
420
+ logger = cls.instance()
421
+
422
+ workbox = None
423
+
424
+ # If name is True, run the current workbox
425
+ if isinstance(name, bool):
426
+ if name:
427
+ workbox = logger.current_workbox()
428
+
429
+ # If name is a string, find first tab with that name
430
+ elif isinstance(name, str):
431
+ split = name.split('/', 1)
432
+ if len(split) < 2:
433
+ return None
434
+ group, editor = split
435
+ group_index = logger.uiWorkboxTAB.index_for_text(group)
436
+ if group_index != -1:
437
+ tab_widget = logger.uiWorkboxTAB.widget(group_index)
438
+ index = tab_widget.index_for_text(editor)
439
+ if index != -1:
440
+ workbox = tab_widget.widget(index)
441
+ if visible:
442
+ tab_widget.setCurrentIndex(index)
443
+ logger.uiWorkboxTAB.setCurrentIndex(group_index)
444
+
445
+ if show and workbox:
446
+ workbox.__show__()
447
+
448
+ return workbox
449
+
450
+ @classmethod
451
+ def run_workbox(cls, name):
452
+ """This is a function which will be added to __main__, and therefore
453
+ available to PythonLogger users. It will accept a string matching the
454
+ "{group}/{workbox}" format, or a boolean that will run the current tab
455
+ to support the command line launching functionality which auto-runs the
456
+ current workbox on launch.
457
+
458
+ Args:
459
+ name(str, boolean): Used to define which workbox to run.
460
+
461
+ Raises:
462
+ Exception: "Cannot call current workbox."
463
+
464
+ Example Usages:
465
+ run_workbox('group_a/test')
466
+ run_workbox('some/stuff.py')
467
+ (from command line): blurdev launch Python_Logger --run_workbox
468
+ """
469
+ workbox = cls.workbox_for_name(name)
470
+
471
+ if workbox is not None:
472
+ # if name is True, its ok to run the workbox, this option
473
+ # is passed by the cli to run the current tab
474
+ if workbox.hasFocus() and name is not True:
475
+ raise Exception("Cannot call current workbox.")
476
+ else:
477
+ # Make sure the workbox text is loaded as it likely has not
478
+ # been shown yet and each tab is now loaded only on demand.
479
+ workbox.__show__()
480
+ workbox.__exec_all__()
481
+
482
+ def setup_run_workbox(self):
483
+ """We will bind the runWordbox function on __main__, which makes is available to
484
+ code running within PythonLogger.
485
+ """
486
+ __main__.run_workbox = self.run_workbox
487
+
488
+ def openSetPreferredTextEditorDialog(self):
489
+ dlg = SetTextEditorPathDialog(parent=self)
490
+ dlg.exec_()
491
+
492
+ def focusToConsole(self):
493
+ """Move focus to the console"""
494
+ self.console().setFocus()
495
+
496
+ def focusToWorkbox(self):
497
+ """Move focus to the current workbox"""
498
+ self.current_workbox().setFocus()
499
+
500
+ def copyToConsole(self):
501
+ """Copy current selection or line from workbox to console"""
502
+ workbox = self.current_workbox()
503
+ if not workbox.hasFocus():
504
+ return
505
+
506
+ text, _line = workbox.__selected_text__(selectText=True)
507
+ if not text:
508
+ line, index = workbox.__cursor_position__()
509
+ text = workbox.__text__(line)
510
+ text = text.rstrip('\r\n')
511
+ if not text:
512
+ return
513
+
514
+ cursor = self.console().textCursor()
515
+ if cursor.hasSelection():
516
+ cursor.removeSelectedText()
517
+
518
+ self.console().insertPlainText(text)
519
+ self.focusToConsole()
520
+
521
+ def copyToWorkbox(self):
522
+ """Copy current selection or line from console to workbox"""
523
+ console = self.console()
524
+ if not console.hasFocus():
525
+ return
526
+
527
+ cursor = console.textCursor()
528
+ if not cursor.hasSelection():
529
+ cursor.select(QTextCursor.LineUnderCursor)
530
+ text = cursor.selectedText()
531
+ prompt = console.prompt()
532
+ if text.startswith(prompt):
533
+ text = text[len(prompt) :]
534
+ text = text.lstrip()
535
+
536
+ outputPrompt = console.outputPrompt()
537
+ outputPrompt = outputPrompt.rstrip()
538
+ if text.startswith(outputPrompt):
539
+ text = text[len(outputPrompt) :]
540
+ text = text.lstrip()
541
+
542
+ if not text:
543
+ return
544
+
545
+ workbox = self.current_workbox()
546
+ workbox.__remove_selected_text__()
547
+ workbox.__insert_text__(text)
548
+
549
+ line, index = workbox.__cursor_position__()
550
+ index += len(text)
551
+ workbox.__set_cursor_position__(line, index)
552
+
553
+ self.focusToWorkbox()
554
+
555
+ def getNextCommand(self):
556
+ if hasattr(self.console(), 'getNextCommand'):
557
+ self.console().getNextCommand()
558
+
559
+ def getPrevCommand(self):
560
+ if hasattr(self.console(), 'getPrevCommand'):
561
+ self.console().getPrevCommand()
562
+
563
+ def wheelEvent(self, event):
564
+ """adjust font size on ctrl+scrollWheel"""
565
+ if event.modifiers() == Qt.ControlModifier:
566
+ # WheelEvents can be emitted in a cluster, but we only want one at a time
567
+ # (ie to change font size by 1, rather than 2 or 3). Let's bail if previous
568
+ # font-resize wheel event was within a certain threshhold.
569
+ now = datetime.now()
570
+ elapsed = now - self.previousFontResizeTime
571
+ tolerance = timedelta(microseconds=100000)
572
+ if elapsed < tolerance:
573
+ return
574
+ self.previousFontResizeTime = now
575
+
576
+ # QT4 presents QWheelEvent.delta(), QT5 has QWheelEvent.angleDelta().y()
577
+ if hasattr(event, 'delta'): # Qt4
578
+ delta = event.delta()
579
+ else: # QT5
580
+ delta = event.angleDelta().y()
581
+
582
+ # convert delta to +1 or -1, depending
583
+ delta = delta // abs(delta)
584
+ minSize = 5
585
+ maxSize = 50
586
+ font = self.console().font()
587
+ newSize = font.pointSize() + delta
588
+ newSize = max(min(newSize, maxSize), minSize)
589
+
590
+ self.setFontSize(newSize)
591
+ else:
592
+ Window.wheelEvent(self, event)
593
+
594
+ def handleMenuHovered(self, action):
595
+ """Qt4 doesn't have a ToolTipsVisible method, so we fake it"""
596
+ # Don't show if it's just the text of the action
597
+ text = re.sub(r"(?<!&)&(?!&)", "", action.text())
598
+ text = text.replace('...', '')
599
+
600
+ if text == action.toolTip():
601
+ text = ''
602
+ else:
603
+ text = action.toolTip()
604
+
605
+ menu = action.parentWidget()
606
+ QToolTip.showText(QCursor.pos(), text, menu)
607
+
608
+ def selectFont(self, monospace=False, proportional=False):
609
+ """Present a QFontChooser dialog, offering, monospace, proportional, or all
610
+ fonts, based on user choice. If a font is chosen, set it on the console and
611
+ workboxes.
612
+
613
+ Args:
614
+ action (QAction): menu action associated with chosen font
615
+ """
616
+ origFont = self.console().font()
617
+ curFontFamily = origFont.family()
618
+
619
+ if monospace and proportional:
620
+ options = QFontDialog.MonospacedFonts | QFontDialog.ProportionalFonts
621
+ kind = "monospace or proportional "
622
+ elif monospace:
623
+ options = QFontDialog.MonospacedFonts
624
+ kind = "monospace "
625
+ elif proportional:
626
+ options = QFontDialog.ProportionalFonts
627
+ kind = "proportional "
628
+
629
+ # Present a QFontDialog for user to choose a font
630
+ title = "Pick a {} font. Current font is: {}".format(kind, curFontFamily)
631
+ newFont, okClicked = QFontDialog.getFont(origFont, self, title, options=options)
632
+
633
+ if okClicked:
634
+ self.console().setConsoleFont(newFont)
635
+ self.setWorkboxFontBasedOnConsole()
636
+ self.setEditorChooserFontBasedOnConsole()
637
+
638
+ def setFontSize(self, newSize):
639
+ """Update the font size in the console and current workbox.
640
+
641
+ Args:
642
+ newSize (int): The new size to set the font
643
+ """
644
+ font = self.console().font()
645
+ font.setPointSize(newSize)
646
+ self.console().setConsoleFont(font)
647
+
648
+ self.setWorkboxFontBasedOnConsole()
649
+ self.setEditorChooserFontBasedOnConsole()
650
+
651
+ def setWorkboxFontBasedOnConsole(self):
652
+ """If the current workbox's font is different to the console's font, set it to
653
+ match.
654
+ """
655
+ font = self.console().font()
656
+
657
+ workboxGroup = self.uiWorkboxTAB.currentWidget()
658
+ if workboxGroup is None:
659
+ return
660
+
661
+ workbox = workboxGroup.currentWidget()
662
+ if workbox is None:
663
+ return
664
+
665
+ if workbox.__font__() != font:
666
+ workbox.__set_margins_font__(font)
667
+ workbox.__set_font__(font)
668
+
669
+ def setEditorChooserFontBasedOnConsole(self):
670
+ """Set the EditorChooser font to match console. This helps with legibility when
671
+ using EditorChooser.
672
+ """
673
+ font = self.console().font()
674
+ for child in self.uiEditorChooserWGT.children():
675
+ if hasattr(child, "font"):
676
+ child.setFont(font)
677
+
678
+ @classmethod
679
+ def _genPrefName(cls, baseName, index):
680
+ if index:
681
+ baseName = '{name}{index}'.format(name=baseName, index=index)
682
+ return baseName
683
+
684
+ def adjustWorkboxOrientation(self, state):
685
+ if state:
686
+ self.uiSplitterSPLIT.setOrientation(Qt.Horizontal)
687
+ else:
688
+ self.uiSplitterSPLIT.setOrientation(Qt.Vertical)
689
+
690
+ def backupPreferences(self):
691
+ """Saves a copy of the current preferences to a zip archive."""
692
+ zip_path = prefs.backup()
693
+ print('PrEditor Preferences backed up to "{}"'.format(zip_path))
694
+ return zip_path
695
+
696
+ def browsePreferences(self):
697
+ prefs.browse(core_name=self.name)
698
+
699
+ def console(self):
700
+ return self.uiConsoleTXT
701
+
702
+ def clearLog(self):
703
+ self.uiConsoleTXT.clear()
704
+
705
+ def clearLogToFile(self):
706
+ """If installLogToFile has been called, clear the stdout."""
707
+ if self._stds:
708
+ self._stds[0].clear(stamp=True)
709
+
710
+ def closeEvent(self, event):
711
+ self.recordPrefs()
712
+ # Save the logger configuration
713
+ lcfg = LoggingConfig(core_name=self.name)
714
+ lcfg.build()
715
+ lcfg.save()
716
+
717
+ super(LoggerWindow, self).closeEvent(event)
718
+ if self.uiConsoleTOOLBAR.isFloating():
719
+ self.uiConsoleTOOLBAR.hide()
720
+
721
+ # Handle any cleanup each workbox tab may need to do before closing
722
+ for editor, _, _, _, _ in self.uiWorkboxTAB.all_widgets():
723
+ editor.__close__()
724
+
725
+ def closeLogger(self):
726
+ self.close()
727
+
728
+ def execAll(self):
729
+ """Clears the console before executing all workbox code"""
730
+ if self.uiClearBeforeRunningACT.isChecked():
731
+ self.clearLog()
732
+ self.current_workbox().__exec_all__()
733
+
734
+ if self.uiAutoPromptACT.isChecked():
735
+ console = self.console()
736
+ prompt = console.prompt()
737
+ console.startPrompt(prompt)
738
+
739
+ def execSelected(self, truncate=True):
740
+ """Clears the console before executing selected workbox code.
741
+
742
+ NOTE! This method is not called when the uiRunSelectedACT is triggered,
743
+ because the workbox will always intercept it. So instead, the workbox's
744
+ keyPressEvent will notice the shortcut and call this method.
745
+ """
746
+
747
+ if self.uiClearBeforeRunningACT.isChecked():
748
+ self.clearLog()
749
+
750
+ self.current_workbox().__exec_selected__(truncate=truncate)
751
+
752
+ if self.uiAutoPromptACT.isChecked():
753
+ self.console().startInputLine()
754
+
755
+ def keyPressEvent(self, event):
756
+ # Fix 'Maya : Qt tools lose focus' https://redmine.blur.com/issues/34430
757
+ if event.modifiers() & (Qt.AltModifier | Qt.ControlModifier | Qt.ShiftModifier):
758
+ pass
759
+ else:
760
+ super(LoggerWindow, self).keyPressEvent(event)
761
+
762
+ def clearExecutionTime(self):
763
+ """Update status text with hyphens to indicate execution has begun."""
764
+ self.setStatusText('Exec: -.- Seconds')
765
+ QApplication.instance().processEvents()
766
+
767
+ def reportExecutionTime(self, seconds):
768
+ """Update status text with seconds passed in."""
769
+ self.uiStatusLBL.showSeconds(seconds)
770
+ self.uiMenuBar.adjustSize()
771
+
772
+ def recordPrefs(self, manual=False):
773
+ if not manual and not self.uiAutoSaveSettingssACT.isChecked():
774
+ return
775
+
776
+ pref = self.load_prefs()
777
+ geo = self.geometry()
778
+ pref.update(
779
+ {
780
+ 'loggergeom': [geo.x(), geo.y(), geo.width(), geo.height()],
781
+ 'windowState': int(self.windowState()),
782
+ 'SplitterVertical': self.uiEditorVerticalACT.isChecked(),
783
+ 'SplitterSize': self.uiSplitterSPLIT.sizes(),
784
+ 'tabIndent': self.uiIndentationsTabsACT.isChecked(),
785
+ 'copyIndentsAsSpaces': self.uiCopyTabsToSpacesACT.isChecked(),
786
+ 'hintingEnabled': self.uiConsoleAutoCompleteEnabledACT.isChecked(),
787
+ 'workboxHintingEnabled': (
788
+ self.uiWorkboxAutoCompleteEnabledACT.isChecked()
789
+ ),
790
+ 'spellCheckEnabled': self.uiSpellCheckEnabledACT.isChecked(),
791
+ 'wordWrap': self.uiWordWrapACT.isChecked(),
792
+ 'clearBeforeRunning': self.uiClearBeforeRunningACT.isChecked(),
793
+ 'uiSelectTextACT': self.uiSelectTextACT.isChecked(),
794
+ 'toolbarStates': str(self.saveState().toHex(), 'utf-8'),
795
+ 'consoleFont': self.console().font().toString(),
796
+ 'uiAutoSaveSettingssACT': self.uiAutoSaveSettingssACT.isChecked(),
797
+ 'uiAutoPromptACT': self.uiAutoPromptACT.isChecked(),
798
+ 'uiLinesInNewWorkboxACT': self.uiLinesInNewWorkboxACT.isChecked(),
799
+ 'uiErrorHyperlinksACT': self.uiErrorHyperlinksACT.isChecked(),
800
+ 'uiStatusLbl_limit': self.uiStatusLBL.limit(),
801
+ 'textEditorPath': self.textEditorPath,
802
+ 'textEditorCmdTempl': self.textEditorCmdTempl,
803
+ 'currentStyleSheet': self._stylesheet,
804
+ 'flash_time': self.uiConsoleTXT.flash_time,
805
+ 'find_files_regex': self.uiFindInWorkboxesWGT.uiRegexBTN.isChecked(),
806
+ 'find_files_cs': (
807
+ self.uiFindInWorkboxesWGT.uiCaseSensitiveBTN.isChecked()
808
+ ),
809
+ 'find_files_context': self.uiFindInWorkboxesWGT.uiContextSPN.value(),
810
+ 'find_files_text': self.uiFindInWorkboxesWGT.uiFindTXT.text(),
811
+ 'uiHighlightExactCompletionACT': (
812
+ self.uiHighlightExactCompletionACT.isChecked()
813
+ ),
814
+ 'dont_ask_again': self.dont_ask_again,
815
+ }
816
+ )
817
+
818
+ # completer settings
819
+ completer = self.console().completer()
820
+ pref["caseSensitive"] = completer.caseSensitive()
821
+ pref["completerMode"] = completer.completerMode().value
822
+
823
+ if self._stylesheet == 'Custom':
824
+ pref['styleSheet'] = self.styleSheet()
825
+
826
+ workbox_prefs = self.uiWorkboxTAB.save_prefs()
827
+ pref['workbox_prefs'] = workbox_prefs
828
+
829
+ pref['editor_cls'] = self.editor_cls_name
830
+
831
+ # Allow any plugins to add their own preferences dictionary
832
+ pref["plugins"] = {}
833
+ for name, plugin in self.plugins.items():
834
+ plugin_pref = plugin.record_prefs(name)
835
+ if plugin_pref:
836
+ pref["plugins"][name] = plugin_pref
837
+
838
+ self.save_prefs(pref)
839
+
840
+ def load_prefs(self):
841
+ filename = prefs.prefs_path('preditor_pref.json', core_name=self.name)
842
+ if os.path.exists(filename):
843
+ with open(filename) as fp:
844
+ return json.load(fp)
845
+ return {}
846
+
847
+ def save_prefs(self, pref):
848
+ # Save preferences to disk
849
+ filename = prefs.prefs_path('preditor_pref.json', core_name=self.name)
850
+ dirname = os.path.dirname(filename)
851
+ if not os.path.exists(dirname):
852
+ os.makedirs(dirname)
853
+ with open(filename, 'w') as fp:
854
+ json.dump(pref, fp, indent=4)
855
+
856
+ def maybeDisplayDialog(self, dialog):
857
+ """If user hasn't previously opted to not show this particular dialog again,
858
+ show it.
859
+ """
860
+ if dialog.objectName() in self.dont_ask_again:
861
+ return
862
+
863
+ dialog.exec_()
864
+
865
+ def restartLogger(self):
866
+ """Closes this PrEditor instance and starts a new process with the same
867
+ cli arguments.
868
+
869
+ Note: This only works if PrEditor is running in standalone mode. It doesn't
870
+ quit the QApplication or other host process. It simply closes this instance
871
+ of PrEditor, saving its preferences, which should allow Qt to exit if no
872
+ other windows are open.
873
+ """
874
+ self.close()
875
+
876
+ # Get the current command and launch it as a new process. This handles
877
+ # use of the preditor/preditor executable launchers.
878
+ cmd = sys.argv[0]
879
+ args = sys.argv[1:]
880
+
881
+ if os.path.basename(cmd) == "__main__.py":
882
+ # Handles using `python -m preditor` style launch.
883
+ cmd = sys.executable
884
+ args = ["-m", "preditor"] + args
885
+ QtCore.QProcess.startDetached(cmd, args)
886
+
887
+ def restorePrefs(self):
888
+ pref = self.load_prefs()
889
+
890
+ # Editor selection
891
+ self.editor_cls_name = pref.get('editor_cls')
892
+ if self.editor_cls_name:
893
+ self.editor_cls_name, editor_cls = plugins.editor(self.editor_cls_name)
894
+ self.uiWorkboxTAB.editor_cls = editor_cls
895
+ else:
896
+ self.uiWorkboxTAB.editor_cls = None
897
+ # Set the workbox core_name so it reads/writes its tabs content into the
898
+ # same core_name preference folder.
899
+ self.uiWorkboxTAB.core_name = self.name
900
+ self.uiEditorChooserWGT.set_editor_name(self.editor_cls_name)
901
+
902
+ # Geometry
903
+ if 'loggergeom' in pref:
904
+ self.setGeometry(*pref['loggergeom'])
905
+ self.uiEditorVerticalACT.setChecked(pref.get('SplitterVertical', False))
906
+ self.adjustWorkboxOrientation(self.uiEditorVerticalACT.isChecked())
907
+
908
+ sizes = pref.get('SplitterSize')
909
+ if sizes:
910
+ self.uiSplitterSPLIT.setSizes(sizes)
911
+ self.setWindowState(Qt.WindowStates(pref.get('windowState', 0)))
912
+ self.uiIndentationsTabsACT.setChecked(pref.get('tabIndent', True))
913
+ self.uiCopyTabsToSpacesACT.setChecked(pref.get('copyIndentsAsSpaces', False))
914
+
915
+ # completer settings
916
+ self.setCaseSensitive(pref.get('caseSensitive', True))
917
+ completerMode = CompleterMode(pref.get('completerMode', 0))
918
+ self.cycleToCompleterMode(completerMode)
919
+ self.setCompleterMode(completerMode)
920
+ self.uiHighlightExactCompletionACT.setChecked(
921
+ pref.get('uiHighlightExactCompletionACT', False)
922
+ )
923
+
924
+ self.setSpellCheckEnabled(self.uiSpellCheckEnabledACT.isChecked())
925
+ self.uiSpellCheckEnabledACT.setChecked(pref.get('spellCheckEnabled', False))
926
+ self.uiSpellCheckEnabledACT.setDisabled(False)
927
+
928
+ self.uiAutoSaveSettingssACT.setChecked(pref.get('uiAutoSaveSettingssACT', True))
929
+
930
+ self.uiAutoPromptACT.setChecked(pref.get('uiAutoPromptACT', False))
931
+ self.uiLinesInNewWorkboxACT.setChecked(
932
+ pref.get('uiLinesInNewWorkboxACT', False)
933
+ )
934
+ self.uiErrorHyperlinksACT.setChecked(pref.get('uiErrorHyperlinksACT', True))
935
+ self.uiStatusLBL.setLimit(pref.get('uiStatusLbl_limit', 5))
936
+
937
+ # Find Files settings
938
+ self.uiFindInWorkboxesWGT.uiRegexBTN.setChecked(
939
+ pref.get('find_files_regex', False)
940
+ )
941
+ self.uiFindInWorkboxesWGT.uiCaseSensitiveBTN.setChecked(
942
+ pref.get('find_files_cs', False)
943
+ )
944
+ self.uiFindInWorkboxesWGT.uiContextSPN.setValue(
945
+ pref.get('find_files_context', 3)
946
+ )
947
+ self.uiFindInWorkboxesWGT.uiFindTXT.setText(pref.get('find_files_text', ''))
948
+
949
+ # External text editor filepath and command template
950
+ defaultExePath = r"C:\Program Files\Sublime Text 3\sublime_text.exe"
951
+ defaultCmd = r'"{exePath}" "{modulePath}":{lineNum}'
952
+ self.textEditorPath = pref.get('textEditorPath', defaultExePath)
953
+ self.textEditorCmdTempl = pref.get('textEditorCmdTempl', defaultCmd)
954
+
955
+ self.uiWordWrapACT.setChecked(pref.get('wordWrap', True))
956
+ self.setWordWrap(self.uiWordWrapACT.isChecked())
957
+ self.uiClearBeforeRunningACT.setChecked(pref.get('clearBeforeRunning', False))
958
+ self.setClearBeforeRunning(self.uiClearBeforeRunningACT.isChecked())
959
+ self.uiSelectTextACT.setChecked(pref.get('uiSelectTextACT', True))
960
+
961
+ self._stylesheet = pref.get('currentStyleSheet', 'Bright')
962
+ if self._stylesheet == 'Custom':
963
+ self.setStyleSheet(pref.get('styleSheet', ''))
964
+ else:
965
+ self.setStyleSheet(self._stylesheet)
966
+ self.uiConsoleTXT.flash_time = pref.get('flash_time', 1.0)
967
+
968
+ self.uiWorkboxTAB.restore_prefs(pref.get('workbox_prefs', {}))
969
+
970
+ hintingEnabled = pref.get('hintingEnabled', True)
971
+ self.uiConsoleAutoCompleteEnabledACT.setChecked(hintingEnabled)
972
+ self.setAutoCompleteEnabled(hintingEnabled, console=True)
973
+ workboxHintingEnabled = pref.get('workboxHintingEnabled', True)
974
+ self.uiWorkboxAutoCompleteEnabledACT.setChecked(workboxHintingEnabled)
975
+ self.setAutoCompleteEnabled(workboxHintingEnabled, console=False)
976
+
977
+ # Ensure the correct workbox stack page is shown
978
+ self.update_workbox_stack()
979
+
980
+ _font = pref.get('consoleFont', None)
981
+ if _font:
982
+ font = QFont()
983
+ if font.fromString(_font):
984
+ self.console().setConsoleFont(font)
985
+
986
+ self.dont_ask_again = pref.get('dont_ask_again', [])
987
+
988
+ # Allow any plugins to restore their own preferences
989
+ for name, plugin in self.plugins.items():
990
+ plugin.restore_prefs(name, pref.get("plugins", {}).get(name))
991
+
992
+ def restoreToolbars(self, pref=None):
993
+ if pref is None:
994
+ pref = self.load_prefs()
995
+
996
+ state = pref.get('toolbarStates', None)
997
+ if state:
998
+ state = QByteArray.fromHex(bytes(state, 'utf-8'))
999
+ self.restoreState(state)
1000
+
1001
+ def setAutoCompleteEnabled(self, state, console=True):
1002
+ if console:
1003
+ self.uiConsoleTXT.completer().setEnabled(state)
1004
+ else:
1005
+ for workbox, _, _, _, _ in self.uiWorkboxTAB.all_widgets():
1006
+ workbox.__set_auto_complete_enabled__(state)
1007
+
1008
+ def setSpellCheckEnabled(self, state):
1009
+ try:
1010
+ self.delayable_engine.set_delayable_enabled('spell_check', state)
1011
+ except KeyError:
1012
+ # Spell check can not be enabled
1013
+ if self.isVisible():
1014
+ # Only show warning if Logger is visible and also disable the action
1015
+ self.uiSpellCheckEnabledACT.setDisabled(True)
1016
+ QMessageBox.warning(
1017
+ self, "Spell-Check", 'Unable to activate spell check.'
1018
+ )
1019
+
1020
+ def setStatusText(self, txt):
1021
+ """Set the text shown in the menu corner of the menu bar.
1022
+
1023
+ Args:
1024
+ txt (str): The text to show in the status text label.
1025
+ """
1026
+ self.uiStatusLBL.setText(txt)
1027
+ self.uiMenuBar.adjustSize()
1028
+
1029
+ def clearStatusText(self):
1030
+ """Clear any displayed status text"""
1031
+ self.uiStatusLBL.clear()
1032
+ self.uiMenuBar.adjustSize()
1033
+
1034
+ def autoHideStatusText(self):
1035
+ """Set timer to automatically clear status text"""
1036
+ if self.statusTimer.isActive():
1037
+ self.statusTimer.stop()
1038
+ self.statusTimer.singleShot(2000, self.clearStatusText)
1039
+ self.statusTimer.start()
1040
+
1041
+ def setStyleSheet(self, stylesheet, recordPrefs=True):
1042
+ """Accepts the name of a stylesheet included with blurdev, or a full
1043
+ path to any stylesheet. If given None, it will default to Bright.
1044
+ """
1045
+ sheet = None
1046
+ if stylesheet is None:
1047
+ stylesheet = 'Bright'
1048
+ if os.path.isfile(stylesheet):
1049
+ # A path to a stylesheet was passed in
1050
+ with open(stylesheet) as f:
1051
+ sheet = f.read()
1052
+ self._stylesheet = stylesheet
1053
+ else:
1054
+ # Try to find an installed stylesheet with the given name
1055
+ sheet, valid = stylesheets.read_stylesheet(stylesheet)
1056
+ if valid:
1057
+ self._stylesheet = stylesheet
1058
+ else:
1059
+ # Assume the user passed the text of the stylesheet directly
1060
+ sheet = stylesheet
1061
+ self._stylesheet = 'Custom'
1062
+
1063
+ # Load the stylesheet
1064
+ if sheet is not None:
1065
+ super(LoggerWindow, self).setStyleSheet(sheet)
1066
+
1067
+ # Update the style menu
1068
+ for act in self.uiStyleMENU.actions():
1069
+ name = act.objectName()
1070
+ isCurrent = name == 'ui{}ACT'.format(self._stylesheet)
1071
+ act.setChecked(isCurrent)
1072
+
1073
+ # Notify widgets that the styleSheet has changed
1074
+ self.styleSheetChanged.emit(stylesheet)
1075
+
1076
+ def setCaseSensitive(self, state):
1077
+ """Set completer case-sensivity"""
1078
+ completer = self.console().completer()
1079
+ completer.setCaseSensitive(state)
1080
+ self.uiAutoCompleteCaseSensitiveACT.setChecked(state)
1081
+ self.reportCaseChange(state)
1082
+ completer.refreshList()
1083
+
1084
+ def toggleCaseSensitive(self):
1085
+ """Toggle completer case-sensitivity"""
1086
+ state = self.console().completer().caseSensitive()
1087
+ self.reportCaseChange(state)
1088
+ self.setCaseSensitive(not state)
1089
+
1090
+ # Completer Modes
1091
+ def cycleCompleterMode(self):
1092
+ """Cycle comleter mode"""
1093
+ completerMode = next(self.completerModeCycle)
1094
+ self.setCompleterMode(completerMode)
1095
+ self.reportCompleterModeChange(completerMode)
1096
+
1097
+ def cycleToCompleterMode(self, completerMode):
1098
+ """
1099
+ Syncs the completerModeCycle iterator to currently chosen completerMode
1100
+ Args:
1101
+ completerMode: Chosen CompleterMode ENUM member
1102
+ """
1103
+ for _ in range(len(CompleterMode)):
1104
+ tempMode = next(self.completerModeCycle)
1105
+ if tempMode == completerMode:
1106
+ break
1107
+
1108
+ def setCompleterMode(self, completerMode):
1109
+ """
1110
+ Set the completer mode to chosen mode
1111
+ Args:
1112
+ completerMode: Chosen CompleterMode ENUM member
1113
+ """
1114
+ completer = self.console().completer()
1115
+
1116
+ completer.setCompleterMode(completerMode)
1117
+ completer.buildCompleter()
1118
+
1119
+ for action in self.uiCompleterModeMENU.actions():
1120
+ action.setChecked(action.data() == completerMode)
1121
+
1122
+ def selectCompleterMode(self, action):
1123
+ if not action.isChecked():
1124
+ action.setChecked(True)
1125
+ return
1126
+ """
1127
+ Handle when completer mode is chosen via menu
1128
+ Will sync mode iterator and set the completion mode
1129
+ Args:
1130
+ action: the menu action associated with the chosen mode
1131
+ """
1132
+
1133
+ # update cycleToCompleterMode to current Mode
1134
+ mode = action.data()
1135
+ self.cycleToCompleterMode(mode)
1136
+ self.setCompleterMode(mode)
1137
+
1138
+ def reportCaseChange(self, state):
1139
+ """Update status text with current Case Sensitivity Mode"""
1140
+ text = "Case Sensitive " if state else "Case Insensitive "
1141
+ self.setStatusText(text)
1142
+ self.autoHideStatusText()
1143
+
1144
+ def reportCompleterModeChange(self, mode):
1145
+ """Update status text with current Completer Mode"""
1146
+ self.setStatusText('Completer Mode: {} '.format(mode.displayName()))
1147
+ self.autoHideStatusText()
1148
+
1149
+ def setClearBeforeRunning(self, state):
1150
+ self.uiRunSelectedACT.setIcon(QIcon(resourcePath('img/playlist-play.png')))
1151
+ self.uiRunAllACT.setIcon(QIcon(resourcePath('img/play.png')))
1152
+
1153
+ def setFlashWindowInterval(self):
1154
+ value = self.uiConsoleTXT.flash_time
1155
+ msg = (
1156
+ 'If running code in the logger takes X seconds or longer,\n'
1157
+ 'the window will flash if it is not in focus.\n'
1158
+ 'Setting the value to zero will disable flashing.'
1159
+ )
1160
+ value, success = QInputDialog.getDouble(self, 'Set flash window', msg, value)
1161
+ if success:
1162
+ self.uiConsoleTXT.flash_time = value
1163
+
1164
+ def setWordWrap(self, state):
1165
+ if state:
1166
+ self.uiConsoleTXT.setLineWrapMode(self.uiConsoleTXT.WidgetWidth)
1167
+ else:
1168
+ self.uiConsoleTXT.setLineWrapMode(self.uiConsoleTXT.NoWrap)
1169
+
1170
+ def show_about(self):
1171
+ """Shows `preditor.about_preditor()`'s output in a message box."""
1172
+ msg = about_preditor(instance=self)
1173
+ QMessageBox.information(self, 'About PrEditor', '<pre>{}</pre>'.format(msg))
1174
+
1175
+ def showEnvironmentVars(self):
1176
+ dlg = Dialog(self)
1177
+ lyt = QVBoxLayout(dlg)
1178
+ lbl = QTextBrowser(dlg)
1179
+ lyt.addWidget(lbl)
1180
+ dlg.setWindowTitle('Blurdev Environment Variable Help')
1181
+ with open(resourcePath('environment_variables.html')) as f:
1182
+ lbl.setText(f.read().replace('\n', ''))
1183
+ dlg.setMinimumSize(600, 400)
1184
+ dlg.show()
1185
+
1186
+ def showEvent(self, event):
1187
+ super(LoggerWindow, self).showEvent(event)
1188
+ self.restoreToolbars()
1189
+ self.updateIndentationsUseTabs()
1190
+ self.updateCopyIndentsAsSpaces()
1191
+
1192
+ # Adjust the minimum height of the label so it's text is the same as
1193
+ # the action menu text
1194
+ height = self.uiMenuBar.actionGeometry(self.uiFileMENU.menuAction()).height()
1195
+ self.uiStatusLBL.setMinimumHeight(height)
1196
+
1197
+ @Slot()
1198
+ def show_workbox_options(self):
1199
+ self.uiWorkboxSTACK.setCurrentIndex(WorkboxPages.Options)
1200
+
1201
+ @Slot()
1202
+ def show_find_in_workboxes(self):
1203
+ """Ensure the find workboxes widget is visible and has focus."""
1204
+ self.uiFindInWorkboxesWGT.activate()
1205
+
1206
+ @Slot()
1207
+ def show_focus_name(self):
1208
+ model = GroupTabListItemModel(manager=self.uiWorkboxTAB)
1209
+ model.process()
1210
+
1211
+ def update_tab(index):
1212
+ group, tab = model.workbox_indexes_from_model_index(index)
1213
+ if group is not None:
1214
+ self.uiWorkboxTAB.set_current_groups_from_index(group, tab)
1215
+
1216
+ w = FuzzySearch(model, parent=self)
1217
+ w.selected.connect(update_tab)
1218
+ w.canceled.connect(update_tab)
1219
+ w.highlighted.connect(update_tab)
1220
+ w.popup()
1221
+
1222
+ def updateCopyIndentsAsSpaces(self):
1223
+ for workbox, _, _, _, _ in self.uiWorkboxTAB.all_widgets():
1224
+ workbox.__set_copy_indents_as_spaces__(
1225
+ self.uiCopyTabsToSpacesACT.isChecked()
1226
+ )
1227
+
1228
+ def updateIndentationsUseTabs(self):
1229
+ for workbox, _, _, _, _ in self.uiWorkboxTAB.all_widgets():
1230
+ workbox.__set_indentations_use_tabs__(
1231
+ self.uiIndentationsTabsACT.isChecked()
1232
+ )
1233
+
1234
+ @Slot()
1235
+ def update_workbox_stack(self):
1236
+ if self.uiWorkboxTAB.editor_cls:
1237
+ index = WorkboxPages.Workboxes
1238
+ else:
1239
+ index = WorkboxPages.Options
1240
+
1241
+ self.uiWorkboxSTACK.setCurrentIndex(index)
1242
+
1243
+ def shutdown(self):
1244
+ # close out of the ide system
1245
+
1246
+ # if this is the global instance, then allow it to be deleted on close
1247
+ if self == LoggerWindow._instance:
1248
+ self.setAttribute(Qt.WA_DeleteOnClose, True)
1249
+ LoggerWindow._instance = None
1250
+
1251
+ # clear out the system
1252
+ self.close()
1253
+
1254
+ def nextTab(self):
1255
+ """Move focus to next workbox tab"""
1256
+ tabWidget = self.uiWorkboxTAB.currentWidget()
1257
+ if not tabWidget.currentWidget().hasFocus():
1258
+ tabWidget.currentWidget().setFocus()
1259
+
1260
+ index = tabWidget.currentIndex()
1261
+ if index == tabWidget.count() - 1:
1262
+ tabWidget.setCurrentIndex(0)
1263
+ else:
1264
+ tabWidget.setCurrentIndex(index + 1)
1265
+
1266
+ def prevTab(self):
1267
+ """Move focus to previous workbox tab"""
1268
+ tabWidget = self.uiWorkboxTAB.currentWidget()
1269
+ if not tabWidget.currentWidget().hasFocus():
1270
+ tabWidget.currentWidget().setFocus()
1271
+
1272
+ index = tabWidget.currentIndex()
1273
+ if index == 0:
1274
+ tabWidget.setCurrentIndex(tabWidget.count() - 1)
1275
+ else:
1276
+ tabWidget.setCurrentIndex(index - 1)
1277
+
1278
+ def gotoGroupByIndex(self, index):
1279
+ """Generally to be used in conjunction with the Ctrl+Alt+<num> keyboard
1280
+ shortcuts, which allow user to jump directly to another tab, mimicking
1281
+ web browser functionality.
1282
+ """
1283
+ if index == -1:
1284
+ index = self.uiWorkboxTAB.count() - 1
1285
+ else:
1286
+ count = self.uiWorkboxTAB.count()
1287
+ index = min(index, count)
1288
+ index -= 1
1289
+
1290
+ self.uiWorkboxTAB.setCurrentIndex(index)
1291
+
1292
+ def gotoTabByIndex(self, index):
1293
+ """Generally to be used in conjunction with the Ctrl+<num> keyboard
1294
+ shortcuts, which allow user to jump directly to another tab, mimicking
1295
+ web browser functionality.
1296
+ """
1297
+ group_tab = self.uiWorkboxTAB.currentWidget()
1298
+ if index == -1:
1299
+ index = group_tab.count() - 1
1300
+ else:
1301
+ count = group_tab.count()
1302
+ index = min(index, count)
1303
+ index -= 1
1304
+
1305
+ group_tab.setCurrentIndex(index)
1306
+
1307
+ @staticmethod
1308
+ def instance(
1309
+ parent=None, name=None, run_workbox=False, create=True, standalone=False
1310
+ ):
1311
+ """Returns the existing instance of the PrEditor gui creating it on first call.
1312
+
1313
+ Args:
1314
+ parent (QWidget, optional): If the instance hasn't been created yet, create
1315
+ it and parent it to this object.
1316
+ run_workbox (bool, optional): If the instance hasn't been created yet, this
1317
+ will execute the active workbox's code once fully initialized.
1318
+ create (bool, optional): Returns None if the instance has not been created.
1319
+ standalone (bool, optional): Launch PrEditor in standalone mode. This
1320
+ enables extra options that only make sense when it is running as
1321
+ its own app, not inside of another app.
1322
+
1323
+ Returns:
1324
+ Returns a fully initialized instance of the PrEditor gui. If called more
1325
+ than once, the same instance will be returned. If create is False, it may
1326
+ return None.
1327
+ """
1328
+ # create the instance for the logger
1329
+ if not LoggerWindow._instance:
1330
+ if not create:
1331
+ return None
1332
+
1333
+ # create the logger instance
1334
+ inst = LoggerWindow(
1335
+ parent, name=name, run_workbox=run_workbox, standalone=standalone
1336
+ )
1337
+
1338
+ # RV has a Unique window structure. It makes more sense to not parent a
1339
+ # singleton window than to parent it to a specific top level window.
1340
+ if core.objectName() == 'rv':
1341
+ inst.setParent(None)
1342
+ inst.setAttribute(Qt.WA_QuitOnClose, False)
1343
+
1344
+ # protect the memory
1345
+ inst.setAttribute(Qt.WA_DeleteOnClose, False)
1346
+
1347
+ # cache the instance
1348
+ LoggerWindow._instance = inst
1349
+
1350
+ return LoggerWindow._instance
1351
+
1352
+ def installLogToFile(self):
1353
+ """All stdout/stderr output is also appended to this file.
1354
+
1355
+ This uses preditor.debug.logToFile(path, useOldStd=True).
1356
+ """
1357
+ if self._logToFilePath is None:
1358
+ path = osystem.defaultLogFile()
1359
+ path, _ = QtCompat.QFileDialog.getSaveFileName(
1360
+ self, "Log Output to File", path
1361
+ )
1362
+ if not path:
1363
+ return
1364
+ path = os.path.normpath(path)
1365
+ print('Output logged to: "{}"'.format(path))
1366
+ debug.logToFile(path, useOldStd=True)
1367
+ # Store the std's so we can clear them later
1368
+ self._stds = (sys.stdout, sys.stderr)
1369
+ self.uiLogToFileACT.setText('Output Logged to File')
1370
+ self.uiLogToFileClearACT.setVisible(True)
1371
+ self._logToFilePath = path
1372
+ else:
1373
+ print('Output logged to: "{}"'.format(self._logToFilePath))
1374
+
1375
+ @classmethod
1376
+ def instance_shutdown(cls):
1377
+ """Call shutdown the LoggerWindow instance only if it was instantiated.
1378
+
1379
+ Returns:
1380
+ bool: If a shutdown was required
1381
+ """
1382
+ if cls._instance:
1383
+ cls._instance.shutdown()
1384
+ return True
1385
+ return False