PrEditor 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. preditor/__init__.py +315 -0
  2. preditor/__main__.py +13 -0
  3. preditor/about_module.py +165 -0
  4. preditor/cli.py +192 -0
  5. preditor/config.py +318 -0
  6. preditor/constants.py +13 -0
  7. preditor/contexts.py +210 -0
  8. preditor/cores/__init__.py +0 -0
  9. preditor/cores/core.py +20 -0
  10. preditor/dccs/.hab.json +10 -0
  11. preditor/dccs/maya/PrEditor_maya.mod +1 -0
  12. preditor/dccs/maya/README.md +22 -0
  13. preditor/dccs/maya/plug-ins/PrEditor_maya.py +141 -0
  14. preditor/dccs/studiomax/PackageContents.xml +32 -0
  15. preditor/dccs/studiomax/PrEditor-PrEditor_Show.mcr +8 -0
  16. preditor/dccs/studiomax/README.md +17 -0
  17. preditor/dccs/studiomax/preditor.ms +16 -0
  18. preditor/dccs/studiomax/preditor_menu.mnx +7 -0
  19. preditor/debug.py +149 -0
  20. preditor/delayable_engine/__init__.py +302 -0
  21. preditor/delayable_engine/delayables.py +85 -0
  22. preditor/enum.py +728 -0
  23. preditor/excepthooks.py +165 -0
  24. preditor/gui/__init__.py +56 -0
  25. preditor/gui/app.py +163 -0
  26. preditor/gui/codehighlighter.py +289 -0
  27. preditor/gui/completer.py +237 -0
  28. preditor/gui/console.py +605 -0
  29. preditor/gui/console_base.py +911 -0
  30. preditor/gui/dialog.py +181 -0
  31. preditor/gui/drag_tab_bar.py +625 -0
  32. preditor/gui/editor_chooser.py +57 -0
  33. preditor/gui/errordialog.py +69 -0
  34. preditor/gui/find_files.py +137 -0
  35. preditor/gui/fuzzy_search/__init__.py +0 -0
  36. preditor/gui/fuzzy_search/fuzzy_search.py +97 -0
  37. preditor/gui/group_tab_widget/__init__.py +0 -0
  38. preditor/gui/group_tab_widget/group_tab_widget.py +528 -0
  39. preditor/gui/group_tab_widget/grouped_tab_menu.py +35 -0
  40. preditor/gui/group_tab_widget/grouped_tab_models.py +107 -0
  41. preditor/gui/group_tab_widget/grouped_tab_widget.py +223 -0
  42. preditor/gui/group_tab_widget/one_tab_widget.py +96 -0
  43. preditor/gui/level_buttons.py +358 -0
  44. preditor/gui/logger_window_handler.py +77 -0
  45. preditor/gui/logger_window_plugin.py +35 -0
  46. preditor/gui/loggerwindow.py +2405 -0
  47. preditor/gui/newtabwidget.py +69 -0
  48. preditor/gui/output_console.py +11 -0
  49. preditor/gui/qtdesigner/__init__.py +21 -0
  50. preditor/gui/qtdesigner/_log_plugin.py +29 -0
  51. preditor/gui/qtdesigner/console_base_plugin.py +48 -0
  52. preditor/gui/qtdesigner/console_predit_plugin.py +48 -0
  53. preditor/gui/set_text_editor_path_dialog.py +61 -0
  54. preditor/gui/status_label.py +99 -0
  55. preditor/gui/suggest_path_quotes_dialog.py +50 -0
  56. preditor/gui/ui/editor_chooser.ui +93 -0
  57. preditor/gui/ui/errordialog.ui +74 -0
  58. preditor/gui/ui/find_files.ui +140 -0
  59. preditor/gui/ui/loggerwindow.ui +1909 -0
  60. preditor/gui/ui/set_text_editor_path_dialog.ui +189 -0
  61. preditor/gui/ui/suggest_path_quotes_dialog.ui +225 -0
  62. preditor/gui/window.py +161 -0
  63. preditor/gui/workbox_mixin.py +1139 -0
  64. preditor/gui/workbox_text_edit.py +136 -0
  65. preditor/gui/workboxwidget.py +315 -0
  66. preditor/logging_config.py +55 -0
  67. preditor/osystem.py +401 -0
  68. preditor/plugins.py +118 -0
  69. preditor/prefs.py +381 -0
  70. preditor/resource/environment_variables.html +26 -0
  71. preditor/resource/error_mail.html +85 -0
  72. preditor/resource/error_mail_inline.html +41 -0
  73. preditor/resource/img/README.md +17 -0
  74. preditor/resource/img/arrow_forward.png +0 -0
  75. preditor/resource/img/check-bold.png +0 -0
  76. preditor/resource/img/chevron-down.png +0 -0
  77. preditor/resource/img/chevron-up.png +0 -0
  78. preditor/resource/img/close-thick.png +0 -0
  79. preditor/resource/img/comment-edit.png +0 -0
  80. preditor/resource/img/content-copy.png +0 -0
  81. preditor/resource/img/content-cut.png +0 -0
  82. preditor/resource/img/content-duplicate.png +0 -0
  83. preditor/resource/img/content-paste.png +0 -0
  84. preditor/resource/img/content-save.png +0 -0
  85. preditor/resource/img/debug_disabled.png +0 -0
  86. preditor/resource/img/eye-check.png +0 -0
  87. preditor/resource/img/file-plus.png +0 -0
  88. preditor/resource/img/file-remove.png +0 -0
  89. preditor/resource/img/format-align-left.png +0 -0
  90. preditor/resource/img/format-letter-case-lower.png +0 -0
  91. preditor/resource/img/format-letter-case-upper.png +0 -0
  92. preditor/resource/img/format-letter-case.svg +1 -0
  93. preditor/resource/img/information.png +0 -0
  94. preditor/resource/img/logging_critical.png +0 -0
  95. preditor/resource/img/logging_custom.png +0 -0
  96. preditor/resource/img/logging_debug.png +0 -0
  97. preditor/resource/img/logging_error.png +0 -0
  98. preditor/resource/img/logging_info.png +0 -0
  99. preditor/resource/img/logging_not_set.png +0 -0
  100. preditor/resource/img/logging_warning.png +0 -0
  101. preditor/resource/img/marker.png +0 -0
  102. preditor/resource/img/play.png +0 -0
  103. preditor/resource/img/playlist-play.png +0 -0
  104. preditor/resource/img/plus-minus-variant.png +0 -0
  105. preditor/resource/img/preditor.ico +0 -0
  106. preditor/resource/img/preditor.png +0 -0
  107. preditor/resource/img/preditor.psd +0 -0
  108. preditor/resource/img/preditor.svg +44 -0
  109. preditor/resource/img/regex.svg +1 -0
  110. preditor/resource/img/restart.svg +1 -0
  111. preditor/resource/img/skip-forward-outline.png +0 -0
  112. preditor/resource/img/skip-next-outline.png +0 -0
  113. preditor/resource/img/skip-next.png +0 -0
  114. preditor/resource/img/skip-previous.png +0 -0
  115. preditor/resource/img/subdirectory-arrow-right.png +0 -0
  116. preditor/resource/img/text-search-variant.png +0 -0
  117. preditor/resource/img/warning-big.png +0 -0
  118. preditor/resource/lang/python.json +30 -0
  119. preditor/resource/pref_updates/pref_updates.json +17 -0
  120. preditor/resource/settings.ini +25 -0
  121. preditor/resource/stylesheet/Bright.css +76 -0
  122. preditor/resource/stylesheet/Dark.css +210 -0
  123. preditor/scintilla/__init__.py +40 -0
  124. preditor/scintilla/delayables/__init__.py +11 -0
  125. preditor/scintilla/delayables/smart_highlight.py +97 -0
  126. preditor/scintilla/delayables/spell_check.py +174 -0
  127. preditor/scintilla/documenteditor.py +1924 -0
  128. preditor/scintilla/finddialog.py +68 -0
  129. preditor/scintilla/lang/__init__.py +80 -0
  130. preditor/scintilla/lang/config/bash.ini +15 -0
  131. preditor/scintilla/lang/config/batch.ini +14 -0
  132. preditor/scintilla/lang/config/cpp.ini +19 -0
  133. preditor/scintilla/lang/config/css.ini +19 -0
  134. preditor/scintilla/lang/config/eyeonscript.ini +17 -0
  135. preditor/scintilla/lang/config/html.ini +21 -0
  136. preditor/scintilla/lang/config/javascript.ini +24 -0
  137. preditor/scintilla/lang/config/lua.ini +16 -0
  138. preditor/scintilla/lang/config/maxscript.ini +20 -0
  139. preditor/scintilla/lang/config/mel.ini +18 -0
  140. preditor/scintilla/lang/config/mu.ini +22 -0
  141. preditor/scintilla/lang/config/nsi.ini +19 -0
  142. preditor/scintilla/lang/config/perl.ini +19 -0
  143. preditor/scintilla/lang/config/puppet.ini +19 -0
  144. preditor/scintilla/lang/config/python.ini +28 -0
  145. preditor/scintilla/lang/config/ruby.ini +19 -0
  146. preditor/scintilla/lang/config/sql.ini +7 -0
  147. preditor/scintilla/lang/config/xml.ini +21 -0
  148. preditor/scintilla/lang/config/yaml.ini +18 -0
  149. preditor/scintilla/lang/language.py +240 -0
  150. preditor/scintilla/lexers/__init__.py +0 -0
  151. preditor/scintilla/lexers/cpplexer.py +22 -0
  152. preditor/scintilla/lexers/javascriptlexer.py +27 -0
  153. preditor/scintilla/lexers/maxscriptlexer.py +235 -0
  154. preditor/scintilla/lexers/mellexer.py +369 -0
  155. preditor/scintilla/lexers/mulexer.py +33 -0
  156. preditor/scintilla/lexers/pythonlexer.py +42 -0
  157. preditor/scintilla/ui/finddialog.ui +160 -0
  158. preditor/settings.py +71 -0
  159. preditor/stream/__init__.py +72 -0
  160. preditor/stream/console_handler.py +169 -0
  161. preditor/stream/director.py +144 -0
  162. preditor/stream/manager.py +97 -0
  163. preditor/streamhandler_helper.py +46 -0
  164. preditor/utils/__init__.py +191 -0
  165. preditor/utils/call_stack.py +86 -0
  166. preditor/utils/cute.py +106 -0
  167. preditor/utils/stylesheets.py +54 -0
  168. preditor/utils/text_search.py +338 -0
  169. preditor/version.py +34 -0
  170. preditor/weakref.py +363 -0
  171. preditor-2.1.0.dist-info/METADATA +308 -0
  172. preditor-2.1.0.dist-info/RECORD +179 -0
  173. preditor-2.1.0.dist-info/WHEEL +5 -0
  174. preditor-2.1.0.dist-info/entry_points.txt +19 -0
  175. preditor-2.1.0.dist-info/licenses/LICENSE +165 -0
  176. preditor-2.1.0.dist-info/top_level.txt +3 -0
  177. tests/encodings/test_ecoding.py +33 -0
  178. tests/find_files/test_find_files.py +74 -0
  179. tests/ide/test_delayable_engine.py +171 -0
@@ -0,0 +1,2405 @@
1
+ from __future__ import absolute_import, print_function
2
+
3
+ import copy
4
+ import itertools
5
+ import json
6
+ import logging
7
+ import os
8
+ import re
9
+ import shutil
10
+ import sys
11
+ import warnings
12
+ from builtins import bytes
13
+ from datetime import datetime, timedelta
14
+ from enum import IntEnum
15
+ from functools import partial
16
+ from pathlib import Path
17
+
18
+ import __main__
19
+ from Qt import QtCompat, QtCore, QtWidgets
20
+ from Qt.QtCore import QByteArray, QFileSystemWatcher, QObject, Qt, QTimer, Signal, Slot
21
+ from Qt.QtGui import QFont, QIcon, QKeySequence, QTextCursor
22
+ from Qt.QtWidgets import (
23
+ QApplication,
24
+ QFontDialog,
25
+ QInputDialog,
26
+ QMenu,
27
+ QMessageBox,
28
+ QTextBrowser,
29
+ QTextEdit,
30
+ QToolButton,
31
+ QToolTip,
32
+ QVBoxLayout,
33
+ )
34
+
35
+ from .. import (
36
+ DEFAULT_CORE_NAME,
37
+ about_preditor,
38
+ config,
39
+ debug,
40
+ get_core_name,
41
+ osystem,
42
+ plugins,
43
+ prefs,
44
+ resourcePath,
45
+ )
46
+ from ..delayable_engine import DelayableEngine
47
+ from ..gui import Dialog, Window, handleMenuHovered, loadUi, tab_widget_for_tab
48
+ from ..gui.fuzzy_search.fuzzy_search import FuzzySearch
49
+ from ..gui.group_tab_widget.grouped_tab_models import GroupTabListItemModel
50
+ from ..logging_config import LoggingConfig
51
+ from ..utils import Json, Truncate, stylesheets
52
+ from .completer import CompleterMode
53
+ from .level_buttons import LoggingLevelButton
54
+ from .set_text_editor_path_dialog import SetTextEditorPathDialog
55
+ from .status_label import StatusLabel
56
+ from .workbox_mixin import WorkboxName
57
+
58
+ logger = logging.getLogger(__name__)
59
+
60
+ PRUNE_PATTERN = r"(?P<name>\w*)-{}\.".format(prefs.DATETIME_PATTERN.pattern)
61
+ PRUNE_PATTERN = re.compile(PRUNE_PATTERN)
62
+
63
+
64
+ class WorkboxPages(IntEnum):
65
+ """Nice names for the uiWorkboxSTACK indexes."""
66
+
67
+ Options = 0
68
+ Workboxes = 1
69
+ Preferences = 2
70
+
71
+
72
+ class LoggerWindow(Window):
73
+ _instance = None
74
+ styleSheetChanged = Signal(str)
75
+
76
+ def __init__(self, parent, name=None, run_workbox=False, standalone=False):
77
+ super(LoggerWindow, self).__init__(parent=parent)
78
+ self.name = name if name else get_core_name()
79
+
80
+ self._logToFilePath = None
81
+
82
+ self._stylesheet = 'Bright'
83
+
84
+ self.setupStatusTimer()
85
+
86
+ # Define gui-resizing mods, which may need to be accessed by other modules.
87
+ ctrl = Qt.KeyboardModifier.ControlModifier
88
+ alt = Qt.KeyboardModifier.AltModifier
89
+ self.gui_font_mod = ctrl | alt
90
+
91
+ # Store the previous time a font-resize wheel event was triggered to prevent
92
+ # rapid-fire WheelEvents. Initialize to the current time.
93
+ self.previousFontResizeTime = datetime.now()
94
+
95
+ self.setWindowIcon(QIcon(resourcePath('img/preditor.png')))
96
+ loadUi(__file__, self)
97
+
98
+ self.uiConsoleTXT.flash_window = self
99
+ self.uiConsoleTXT.clearExecutionTime = self.clearExecutionTime
100
+ self.uiConsoleTXT.reportExecutionTime = self.reportExecutionTime
101
+ # If we don't disable this shortcut Qt won't respond to this classes or
102
+ # the ConsolePrEdit's
103
+ self.uiConsoleTXT.uiClearToLastPromptACT.setShortcut('')
104
+
105
+ # create the status reporting label
106
+ self.uiStatusLBL = StatusLabel(self)
107
+ self.uiMenuBar.setCornerWidget(self.uiStatusLBL)
108
+
109
+ # create the workbox tabs
110
+ self._currentTab = -1
111
+
112
+ # Setup delayable system
113
+ self.delayable_engine = DelayableEngine.instance('logger', self)
114
+
115
+ self.uiWorkboxTAB.editor_kwargs = dict(
116
+ console=self.uiConsoleTXT, delayable_engine=self.delayable_engine.name
117
+ )
118
+
119
+ # Create additional buttons in toolbar.
120
+ self.uiLoggingLevelBTN = LoggingLevelButton(self)
121
+ self.uiConsoleTOOLBAR.insertWidget(
122
+ self.uiRunSelectedACT,
123
+ self.uiLoggingLevelBTN,
124
+ )
125
+ self.uiConsoleTOOLBAR.insertSeparator(self.uiRunSelectedACT)
126
+ self.uiConsoleTOOLBAR.show()
127
+
128
+ # Configure Find in Workboxes
129
+ self.uiFindInWorkboxesWGT.hide()
130
+ self.uiFindInWorkboxesWGT.managers.append(self.uiWorkboxTAB)
131
+ self.uiFindInWorkboxesWGT.console = self.console()
132
+
133
+ # Initial configuration of the logToFile feature
134
+ self._logToFilePath = None
135
+ self._stds = None
136
+ self.uiLogToFileClearACT.setVisible(False)
137
+
138
+ # Call other setup methods
139
+ self.connectSignals()
140
+ self.createActions()
141
+ self.setIcons()
142
+ self.startFileSystemMonitor()
143
+
144
+ self.maxRecentClosedWorkboxes = 20
145
+ self.max_num_backups = 50
146
+ self.dont_ask_again = []
147
+
148
+ # Load any plugins, and set window title
149
+ self.loadPlugins()
150
+ self.setWindowTitle(self.defineWindowTitle())
151
+
152
+ self.handleChangedUiElements()
153
+
154
+ self.restorePrefs()
155
+
156
+ self.setWorkboxFontBasedOnConsole()
157
+ self.setEditorChooserFontBasedOnConsole()
158
+
159
+ self.setup_run_workbox()
160
+
161
+ if not standalone:
162
+ # This action only is valid when running in standalone mode
163
+ self.uiRestartACT.setVisible(False)
164
+
165
+ # Run the current workbox after the LoggerWindow is shown.
166
+ if run_workbox:
167
+ # By using two singleShot timers, we can show and draw the LoggerWindow,
168
+ # then call execAll. This makes it easier to see what code you are running
169
+ # before it has finished running completely.
170
+ # QTimer.singleShot(0, lambda: QTimer.singleShot(0, self.execAll))
171
+ QTimer.singleShot(
172
+ 0, lambda: QTimer.singleShot(0, lambda: self.run_workbox(run_workbox))
173
+ )
174
+
175
+ def connectSignals(self):
176
+ """Connect various signals"""
177
+ self.uiClearToLastPromptACT.triggered.connect(
178
+ self.uiConsoleTXT.clearToLastPrompt
179
+ )
180
+
181
+ self.uiRestartACT.triggered.connect(self.restartLogger)
182
+ self.uiCloseLoggerACT.triggered.connect(self.closeLoggerByAction)
183
+
184
+ self.uiRunAllACT.triggered.connect(self.execAll)
185
+ # Even though the RunSelected and Open Most Recently Closed Workbox
186
+ # actions (with shortcuts) are connected here, this only affects if the
187
+ # action is chosen from the menu. The shortcuts are always intercepted
188
+ # by the workbox document editor. To handle this, the
189
+ # workbox.keyPressEvent method will perceive the shortcut press, and
190
+ # call the correct method.
191
+ self.uiRunSelectedACT.triggered.connect(
192
+ partial(self.execSelected, truncate=True)
193
+ )
194
+ self.uiRunSelectedDontTruncateACT.triggered.connect(
195
+ partial(self.execSelected, truncate=False)
196
+ )
197
+ # Closed workboxes
198
+ self.uiOpenMostRecentWorkboxACT.triggered.connect(
199
+ self.openMostRecentlyClosedWorkbox
200
+ )
201
+
202
+ self.uiConsoleAutoCompleteEnabledCHK.toggled.connect(
203
+ partial(self.setAutoCompleteEnabled, console=True)
204
+ )
205
+ self.uiWorkboxAutoCompleteEnabledCHK.toggled.connect(
206
+ partial(self.setAutoCompleteEnabled, console=False)
207
+ )
208
+
209
+ self.uiAutoCompleteCaseSensitiveACT.toggled.connect(self.setCaseSensitive)
210
+
211
+ self.uiSelectMonospaceFontACT.triggered.connect(
212
+ partial(self.selectFont, origFont=None, monospace=True)
213
+ )
214
+ self.uiSelectProportionalFontACT.triggered.connect(
215
+ partial(self.selectFont, origFont=None, proportional=True)
216
+ )
217
+ self.uiSelectAllFontACT.triggered.connect(
218
+ partial(self.selectFont, origFont=None, monospace=True, proportional=True)
219
+ )
220
+ self.uiSelectGuiFontsMENU.triggered.connect(
221
+ partial(self.selectGuiFont, monospace=True, proportional=True)
222
+ )
223
+
224
+ self.uiDecreaseCodeFontSizeACT.triggered.connect(
225
+ partial(self.adjustFontSize, "Code", -1)
226
+ )
227
+ self.uiIncreaseCodeFontSizeACT.triggered.connect(
228
+ partial(self.adjustFontSize, "Code", 1)
229
+ )
230
+ self.uiDecreaseGuiFontSizeACT.triggered.connect(
231
+ partial(self.adjustFontSize, "Gui", -1)
232
+ )
233
+ self.uiIncreaseGuiFontSizeACT.triggered.connect(
234
+ partial(self.adjustFontSize, "Gui", 1)
235
+ )
236
+
237
+ # Workbox add/remove
238
+ self.uiNewWorkboxACT.triggered.connect(
239
+ lambda: self.uiWorkboxTAB.add_new_tab(group=True)
240
+ )
241
+ self.uiCloseWorkboxACT.triggered.connect(self.uiWorkboxTAB.close_current_tab)
242
+
243
+ # Old workbox housekeeping
244
+ self.uiEmptyWorkboxRecycleBinACT.triggered.connect(
245
+ self.empty_workbox_recycle_bin
246
+ )
247
+
248
+ # Browse previous commands
249
+ self.uiGetPrevCmdACT.triggered.connect(self.getPrevCommand)
250
+ self.uiGetNextCmdACT.triggered.connect(self.getNextCommand)
251
+
252
+ # Focus to console or to workbox, optionally copy seleciton or line
253
+ self.uiFocusToConsoleACT.triggered.connect(self.focusToConsole)
254
+ self.uiCopyToConsoleACT.triggered.connect(self.copyToConsole)
255
+ self.uiFocusToWorkboxACT.triggered.connect(self.focusToWorkbox)
256
+ self.uiCopyToWorkboxACT.triggered.connect(self.copyToWorkbox)
257
+
258
+ # Navigate workbox tabs
259
+ self.uiNextTabACT.triggered.connect(self.nextTab)
260
+ self.uiPrevTabACT.triggered.connect(self.prevTab)
261
+
262
+ # Navigate workbox versions
263
+ self.uiTab1ACT.triggered.connect(partial(self.gotoTabByIndex, 1))
264
+ self.uiTab2ACT.triggered.connect(partial(self.gotoTabByIndex, 2))
265
+ self.uiTab3ACT.triggered.connect(partial(self.gotoTabByIndex, 3))
266
+ self.uiTab4ACT.triggered.connect(partial(self.gotoTabByIndex, 4))
267
+ self.uiTab5ACT.triggered.connect(partial(self.gotoTabByIndex, 5))
268
+ self.uiTab6ACT.triggered.connect(partial(self.gotoTabByIndex, 6))
269
+ self.uiTab7ACT.triggered.connect(partial(self.gotoTabByIndex, 7))
270
+ self.uiTab8ACT.triggered.connect(partial(self.gotoTabByIndex, 8))
271
+ self.uiTabLastACT.triggered.connect(partial(self.gotoTabByIndex, -1))
272
+
273
+ self.uiGroup1ACT.triggered.connect(partial(self.gotoGroupByIndex, 1))
274
+ self.uiGroup2ACT.triggered.connect(partial(self.gotoGroupByIndex, 2))
275
+ self.uiGroup3ACT.triggered.connect(partial(self.gotoGroupByIndex, 3))
276
+ self.uiGroup4ACT.triggered.connect(partial(self.gotoGroupByIndex, 4))
277
+ self.uiGroup5ACT.triggered.connect(partial(self.gotoGroupByIndex, 5))
278
+ self.uiGroup6ACT.triggered.connect(partial(self.gotoGroupByIndex, 6))
279
+ self.uiGroup7ACT.triggered.connect(partial(self.gotoGroupByIndex, 7))
280
+ self.uiGroup8ACT.triggered.connect(partial(self.gotoGroupByIndex, 8))
281
+ self.uiGroupLastACT.triggered.connect(partial(self.gotoGroupByIndex, -1))
282
+
283
+ self.uiRunFirstWorkboxACT.triggered.connect(self.run_first_workbox)
284
+
285
+ self.latestTimeStrsForBoxesChangedViaInstance = {}
286
+ self.boxesOrphanedViaInstance = {}
287
+
288
+ self.uiFocusNameACT.triggered.connect(self.show_focus_name)
289
+
290
+ self.uiCommentToggleACT.triggered.connect(self.comment_toggle)
291
+
292
+ self.uiSpellCheckEnabledCHK.toggled.connect(self.setSpellCheckEnabled)
293
+ self.uiIndentationsTabsCHK.toggled.connect(self.updateIndentationsUseTabs)
294
+ self.uiCopyTabsToSpacesCHK.toggled.connect(self.updateCopyIndentsAsSpaces)
295
+ self.uiWordWrapCHK.toggled.connect(self.setWordWrap)
296
+ self.uiResetWarningFiltersACT.triggered.connect(warnings.resetwarnings)
297
+ self.uiLogToFileACT.triggered.connect(self.installLogToFile)
298
+ self.uiLogToFileClearACT.triggered.connect(self.clearLogToFile)
299
+ self.uiClearLogACT.triggered.connect(self.clearLog)
300
+ self.uiSaveConsoleSettingsACT.triggered.connect(
301
+ lambda: self.recordPrefs(manual=True)
302
+ )
303
+ self.uiClearBeforeRunningCHK.toggled.connect(self.setClearBeforeRunning)
304
+ self.uiEditorVerticalCHK.toggled.connect(self.adjustWorkboxOrientation)
305
+ self.uiEnvironmentVarsACT.triggered.connect(self.showEnvironmentVars)
306
+ self.uiAboutPreditorACT.triggered.connect(self.show_about)
307
+
308
+ # Prefs on disk
309
+ self.uiPrefsBrowseBTN.clicked.connect(self.browsePreferences)
310
+ self.uiPrefsBackupBTN.clicked.connect(self.backupPreferences)
311
+
312
+ self.uiSetPreferredTextEditorPathACT.triggered.connect(
313
+ self.openSetPreferredTextEditorDialog
314
+ )
315
+
316
+ # Tooltips - Qt4 doesn't have a ToolTipsVisible method, so we fake it
317
+ regEx = ".*"
318
+ menus = self.findChildren(QtWidgets.QMenu, QtCore.QRegExp(regEx))
319
+ for menu in menus:
320
+ menu.hovered.connect(handleMenuHovered)
321
+
322
+ # Scroll thru workbox versions
323
+ self.uiShowFirstWorkboxVersionACT.triggered.connect(
324
+ partial(self.change_to_workbox_version_text, prefs.VersionTypes.First)
325
+ )
326
+ self.uiShowPreviousWorkboxVersionACT.triggered.connect(
327
+ partial(self.change_to_workbox_version_text, prefs.VersionTypes.Previous)
328
+ )
329
+ self.uiShowNextWorkboxVersionACT.triggered.connect(
330
+ partial(self.change_to_workbox_version_text, prefs.VersionTypes.Next)
331
+ )
332
+ self.uiShowLastWorkboxVersionACT.triggered.connect(
333
+ partial(self.change_to_workbox_version_text, prefs.VersionTypes.Last)
334
+ )
335
+
336
+ # Preferences window
337
+ self.uiClosePreferencesBTN.clicked.connect(self.update_workbox_stack)
338
+ self.uiClosePreferencesBTN.clicked.connect(self.update_window_settings)
339
+
340
+ # Preferences
341
+ self.uiExtraTooltipInfoCHK.toggled.connect(self.updateTabColorsAndToolTips)
342
+
343
+ # Code Highlighting
344
+ self.uiConsoleHighlightEnabledCHK.toggled.connect(
345
+ self.setConsoleHighlightEnabled
346
+ )
347
+
348
+ # Pre-cache the refresh on Write value for speed when writing
349
+ self.uiRepaintConsolesPerSecondSPIN.valueChanged.connect(
350
+ self.updateRepaintDelay
351
+ )
352
+ self.uiRepaintConsolesOnWriteCHK.toggled.connect(
353
+ self.uiRepaintProcessEventsOccasionallyCHK.setEnabled
354
+ )
355
+
356
+ def setIcons(self):
357
+ """Set various icons"""
358
+ self.uiClearLogACT.setIcon(QIcon(resourcePath('img/close-thick.png')))
359
+ self.uiNewWorkboxACT.setIcon(QIcon(resourcePath('img/file-plus.png')))
360
+ self.uiCloseWorkboxACT.setIcon(QIcon(resourcePath('img/file-remove.png')))
361
+ self.uiSaveConsoleSettingsACT.setIcon(
362
+ QIcon(resourcePath('img/content-save.png'))
363
+ )
364
+ self.uiAboutPreditorACT.setIcon(QIcon(resourcePath('img/information.png')))
365
+ self.uiRestartACT.setIcon(QIcon(resourcePath('img/restart.svg')))
366
+ self.uiCloseLoggerACT.setIcon(QIcon(resourcePath('img/close-thick.png')))
367
+
368
+ def createActions(self):
369
+ """Create the necessary actions"""
370
+ self.addAction(self.uiClearLogACT)
371
+ self.uiConsoleTXT.removeAction(self.uiConsoleTXT.uiClearACT)
372
+
373
+ # Setup ability to cycle completer mode, and create action for each mode
374
+ self.completerModeCycle = itertools.cycle(CompleterMode)
375
+ # create CompleterMode submenu
376
+ defaultMode = next(self.completerModeCycle)
377
+ for mode in CompleterMode:
378
+ modeName = mode.displayName()
379
+ action = self.uiCompleterModeMENU.addAction(modeName)
380
+ action.setObjectName('ui{}ModeACT'.format(modeName))
381
+ action.setData(mode)
382
+ action.setCheckable(True)
383
+ action.setChecked(mode == defaultMode)
384
+ completerMode = CompleterMode(mode)
385
+ action.setToolTip(completerMode.toolTip())
386
+ action.triggered.connect(partial(self.selectCompleterMode, action))
387
+
388
+ # Completer mode actions
389
+ self.uiCompleterModeMENU.addSeparator()
390
+ action = self.uiCompleterModeMENU.addAction('Cycle mode')
391
+ action.setObjectName('uiCycleModeACT')
392
+ action.setShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_M))
393
+ action.triggered.connect(self.cycleCompleterMode)
394
+ self.uiCompleterModeMENU.hovered.connect(handleMenuHovered)
395
+
396
+ # add stylesheet menu options.
397
+ for style_name in stylesheets.stylesheets():
398
+ action = self.uiStyleMENU.addAction(style_name)
399
+ action.setObjectName('ui{}ACT'.format(style_name))
400
+ action.setCheckable(True)
401
+ action.setChecked(self._stylesheet == style_name)
402
+ action.triggered.connect(partial(self.setStyleSheet, style_name))
403
+
404
+ def startFileSystemMonitor(self):
405
+ """Start the file system monitor, and add this PrEditor's prefs path"""
406
+ self.openFileMonitor = QFileSystemWatcher(self)
407
+ self.openFileMonitor.fileChanged.connect(self.linkedFileChanged)
408
+ self.setFileMonitoringEnabled(self.prefsPath(), True)
409
+
410
+ @Slot()
411
+ def apply_options(self):
412
+ """Apply editor options the user chose on the WorkboxPage.Options page."""
413
+ editor_cls_name, editor_cls = plugins.editor(
414
+ self.uiEditorChooserWGT.editor_name()
415
+ )
416
+ if editor_cls_name is None:
417
+ self.update_workbox_stack()
418
+ return
419
+ if editor_cls_name != self.editor_cls_name:
420
+ self.editor_cls_name = editor_cls_name
421
+ self.uiWorkboxTAB.editor_cls = editor_cls
422
+ # We need to change the editor, save all prefs
423
+ self.recordPrefs(manual=True, disableFileMonitoring=True)
424
+ # Clear the uiWorkboxTAB
425
+ self.uiWorkboxTAB.clear()
426
+ # Restore prefs to populate the tabs
427
+ self.restorePrefs()
428
+
429
+ self.update_workbox_stack()
430
+
431
+ def autoSaveEnabled(self):
432
+ """Whether or not AutoSave option is set
433
+
434
+ Returns:
435
+ bool: Whether AutoSave option is checked or not
436
+ """
437
+ return self.uiAutoSaveSettingsCHK.isChecked()
438
+
439
+ def setAutoSaveEnabled(self, state):
440
+ """Set AutoSave option to state
441
+
442
+ Args:
443
+ state (bool): State to set AutoSave option
444
+ """
445
+ self.uiAutoSaveSettingsCHK.setChecked(state)
446
+
447
+ def promptOnLinkedChange(self):
448
+ """Whether or not Prompt On Linked Change option is set
449
+
450
+ Returns:
451
+ bool: Whether or not Prompt On Linked Change option is set
452
+ """
453
+ return self.uiPromptOnLinkedChangeCHK.isChecked()
454
+
455
+ def setPromptOnLinkedChange(self, state):
456
+ """Set Prompt On Linked Change option option to state
457
+
458
+ Args:
459
+ state (bool): State to set Prompt On Linked Change option
460
+ """
461
+ self.uiPromptOnLinkedChangeCHK.setChecked(state)
462
+
463
+ def launch(self, focus=True):
464
+ """Ensure this window is raised to the top and make it regain focus.
465
+
466
+ Args:
467
+ focus (bool, optional): If True then make sure the console has focus.
468
+ """
469
+ self.show()
470
+ self.activateWindow()
471
+ self.raise_()
472
+ self.setWindowState(
473
+ self.windowState() & ~Qt.WindowState.WindowMinimized
474
+ | Qt.WindowState.WindowActive
475
+ )
476
+ if focus:
477
+ self.focusToConsole()
478
+
479
+ def loadPlugins(self):
480
+ """Load any plugins that modify the LoggerWindow."""
481
+ self.plugins = {}
482
+ for name, plugin in plugins.loggerwindow():
483
+ if name not in self.plugins:
484
+ self.plugins[name] = plugin(self)
485
+
486
+ def handleChangedUiElements(self):
487
+ """To prevent errors if user has newer PrEditor, but older plugins,
488
+ we keep the ui elements until the ui and plugins have been loaded. Now
489
+ we can check if now deprecated can safely be deleted.
490
+ """
491
+
492
+ # Preferences are moved to a tab page, so this menu should be removed.
493
+ # But it may be used by some plugins, so only remove it if the plugins
494
+ # have also been updated.
495
+ if self.uiPreferencesMENU.isEmpty():
496
+ self.uiPreferencesMENU.deleteLater()
497
+
498
+ def defineWindowTitle(self):
499
+ """Define the window title, including and info plugins may add."""
500
+
501
+ # Define the title
502
+ loggerName = QApplication.instance().translate(
503
+ 'PrEditorWindow', DEFAULT_CORE_NAME
504
+ )
505
+ pyVersion = '{}.{}.{}'.format(*sys.version_info[:3])
506
+ size = osystem.getPointerSize()
507
+ title = f"{loggerName} - {self.name} - {pyVersion} {size}-bit"
508
+
509
+ # Add any info plugins may add to title
510
+ for _name, plugin in self.plugins.items():
511
+ title = plugin.updateWindowTitle(title)
512
+ return title
513
+
514
+ def comment_toggle(self):
515
+ self.current_workbox().__comment_toggle__()
516
+
517
+ def current_workbox(self):
518
+ """Returns the current workbox for the current tab group."""
519
+ return self.uiWorkboxTAB.current_groups_widget()
520
+
521
+ @classmethod
522
+ def name_for_workbox(cls, workbox):
523
+ """Returns the name for a given workbox or None if not valid.
524
+
525
+ The name is a `WorkboxName` object showing the group and name joined by
526
+ a `/`.
527
+
528
+ Args:
529
+ workbox: The workbox to get the name of. If None is passed then it
530
+ will return the name of the current workbox.
531
+
532
+ Returns:
533
+ The name of the widget as a `WorkboxName` object showing the group
534
+ and name joined by a `/`. If workbox is not valid for the LoggerWindow
535
+ instance then None is returned.
536
+ """
537
+
538
+ if workbox is None:
539
+ # if the workbox was not provided use the current workbox
540
+ logger = cls.instance()
541
+ index = logger.uiWorkboxTAB.currentIndex()
542
+ group = logger.uiWorkboxTAB.tabText(index)
543
+ group_widget = logger.uiWorkboxTAB.currentWidget()
544
+ index = group_widget.currentIndex()
545
+ name = group_widget.tabText(index)
546
+ return WorkboxName(group, name)
547
+
548
+ # Otherwise resolve from the parent widgets.
549
+ # Get the parent QTabWidget of the workbox
550
+ workbox_tab_widget = tab_widget_for_tab(workbox)
551
+ if not workbox_tab_widget:
552
+ return None
553
+ # Get the group QTabWidget of the parent QTabWidget of the workbox
554
+ group_widget = tab_widget_for_tab(workbox_tab_widget)
555
+ if not group_widget:
556
+ return None
557
+
558
+ # Get the group name
559
+ index = group_widget.indexOf(workbox_tab_widget)
560
+ group = group_widget.tabText(index)
561
+
562
+ index = workbox_tab_widget.indexOf(workbox)
563
+ name = workbox_tab_widget.tabText(index)
564
+ return WorkboxName(group, name)
565
+
566
+ @classmethod
567
+ def workbox_for_name(cls, name, show=False, visible=False):
568
+ """Used to find a workbox for a given name. It accepts a string matching
569
+ the "{group}/{workbox}" format, or if True, the current workbox.
570
+
571
+ Args:
572
+ name(str, boolean): Used to define which workbox to run.
573
+ show (bool, optional): If a workbox is found, call `__show__` on it
574
+ to ensure that it is initialized and its text is loaded.
575
+ visible (bool, optional): Make the this workbox visible if found.
576
+ """
577
+ logger = cls.instance()
578
+
579
+ workbox = None
580
+
581
+ # If name is True, run the current workbox
582
+ if isinstance(name, bool):
583
+ if name:
584
+ workbox = logger.current_workbox()
585
+
586
+ # If name is a string, find first tab with that name
587
+ elif isinstance(name, str):
588
+ split = name.split('/', 1)
589
+ if len(split) < 2:
590
+ return None
591
+ group, editor = split
592
+ group_index = logger.uiWorkboxTAB.index_for_text(group)
593
+ if group_index != -1:
594
+ tab_widget = logger.uiWorkboxTAB.widget(group_index)
595
+ index = tab_widget.index_for_text(editor)
596
+ if index != -1:
597
+ workbox = tab_widget.widget(index)
598
+ if visible:
599
+ tab_widget.setCurrentIndex(index)
600
+ logger.uiWorkboxTAB.setCurrentIndex(group_index)
601
+
602
+ if show and workbox:
603
+ workbox.__show__()
604
+
605
+ return workbox
606
+
607
+ def workbox_for_id(self, workbox_id, show=False, visible=False):
608
+ """Used to find a workbox for a given id.
609
+
610
+ Args:
611
+ workbox_id(str): The workbox id for which to match when searching
612
+ for the workbox
613
+ show (bool, optional): If a workbox is found, call `__show__` on it
614
+ to ensure that it is initialized and its text is loaded.
615
+ visible (bool, optional): Make the this workbox visible if found.
616
+ """
617
+ workbox = None
618
+ for box_info in self.uiWorkboxTAB.all_widgets():
619
+ temp_box = box_info[0]
620
+ if temp_box.__workbox_id__() == workbox_id:
621
+ workbox = temp_box
622
+ break
623
+
624
+ if workbox:
625
+ if show:
626
+ workbox.__show__()
627
+ if visible:
628
+ grp_idx, tab_idx = workbox.__group_tab_index__()
629
+ self.uiWorkboxTAB.setCurrentIndex(grp_idx)
630
+ group = self.uiWorkboxTAB.widget(grp_idx)
631
+ group.setCurrentIndex(tab_idx)
632
+
633
+ return workbox
634
+
635
+ def run_first_workbox(self):
636
+ workbox = self.uiWorkboxTAB.widget(0).widget(0)
637
+ self.run_workbox("", workbox=workbox)
638
+
639
+ @classmethod
640
+ def run_workbox(cls, name, workbox=None):
641
+ """This is a function which will be added to __main__, and therefore
642
+ available to PythonLogger users. It will accept a string matching the
643
+ "{group}/{workbox}" format, or a boolean that will run the current tab
644
+ to support the command line launching functionality which auto-runs the
645
+ current workbox on launch.
646
+
647
+ Args:
648
+ name(str, boolean): Used to define which workbox to run.
649
+
650
+ Raises:
651
+ Exception: "Cannot call current workbox."
652
+
653
+ Example Usages:
654
+ run_workbox('group_a/test')
655
+ run_workbox('some/stuff.py')
656
+ (from command line): blurdev launch Python_Logger --run_workbox
657
+ """
658
+ if workbox is None:
659
+ workbox = cls.workbox_for_name(name)
660
+
661
+ if workbox is not None:
662
+ # if name is True, its ok to run the workbox, this option
663
+ # is passed by the cli to run the current tab
664
+ if workbox.hasFocus() and name is not True:
665
+ raise Exception("Cannot call current workbox.")
666
+ else:
667
+ # Make sure the workbox text is loaded as it likely has not
668
+ # been shown yet and each tab is now loaded only on demand.
669
+ workbox.__show__()
670
+ workbox.__exec_all__()
671
+
672
+ def setup_run_workbox(self):
673
+ """We will bind the runWordbox function on __main__, which makes is available to
674
+ code running within PythonLogger.
675
+ """
676
+ __main__.run_workbox = self.run_workbox
677
+
678
+ def change_to_workbox_version_text(self, versionType):
679
+ """Change the current workbox's text to a previously saved version, based
680
+ on versionType, which can be First, Previous, Next, SecondToLast, or Last.
681
+
682
+ If we are already at the start or end of the stack of files, and trying
683
+ to go further, do nothing.
684
+
685
+ Args:
686
+ versionType (prefs.VersionTypes): Enum describing which version to
687
+ fetch
688
+
689
+ """
690
+ tab_group = self.uiWorkboxTAB.currentWidget()
691
+
692
+ workbox_widget = tab_group.currentWidget()
693
+
694
+ idx, count = prefs.get_backup_file_index_and_count(
695
+ self.name,
696
+ workbox_widget.__workbox_id__(),
697
+ backup_file=workbox_widget.__backup_file__(),
698
+ )
699
+
700
+ # For ease of reading, set these variables.
701
+ forFirst = versionType == prefs.VersionTypes.First
702
+ forPrevious = versionType == prefs.VersionTypes.Previous
703
+ forNext = versionType == prefs.VersionTypes.Next
704
+ forLast = versionType == prefs.VersionTypes.Last
705
+ isFirstWorkbox = idx is None or idx == 0
706
+ isLastWorkbox = idx is None or idx + 1 == count
707
+ isDirty = workbox_widget.__is_dirty__()
708
+
709
+ # If we are on last workbox and it's dirty, do the user a solid, and
710
+ # save any thing they've typed.
711
+ if isLastWorkbox and isDirty:
712
+ workbox_widget.__save_prefs__(saveLinkedFile=False)
713
+ isFirstWorkbox = False
714
+
715
+ # If we are at either end of stack, and trying to go further, do nothing
716
+ if isFirstWorkbox and (forFirst or forPrevious):
717
+ return
718
+ if isLastWorkbox and (forNext or forLast):
719
+ return
720
+
721
+ filename, idx, count = workbox_widget.__load_workbox_version_text__(versionType)
722
+
723
+ # Get rid of the hash part of the filename
724
+ match = prefs.DATETIME_PATTERN.search(filename)
725
+ if match:
726
+ filename = match.group()
727
+
728
+ txt = "{} [{}/{}]".format(filename, idx, count)
729
+ self.setStatusText(txt)
730
+ self.autoHideStatusText()
731
+ self.updateTabColorsAndToolTips()
732
+
733
+ def openSetPreferredTextEditorDialog(self):
734
+ dlg = SetTextEditorPathDialog(parent=self)
735
+ self.setDialogFont(dlg)
736
+ dlg.exec()
737
+
738
+ def focusToConsole(self):
739
+ """Move focus to the console"""
740
+ self.console().setFocus()
741
+
742
+ def focusToWorkbox(self):
743
+ """Move focus to the current workbox"""
744
+ self.current_workbox().setFocus()
745
+
746
+ def copyToConsole(self):
747
+ """Copy current selection or line from workbox to console"""
748
+ workbox = self.current_workbox()
749
+ if not workbox.hasFocus():
750
+ return
751
+
752
+ text, _line = workbox.__selected_text__(selectText=True)
753
+ if not text:
754
+ line, index = workbox.__cursor_position__()
755
+ text = workbox.__text__(line)
756
+ text = text.rstrip('\r\n')
757
+ if not text:
758
+ return
759
+
760
+ cursor = self.console().textCursor()
761
+ if cursor.hasSelection():
762
+ cursor.removeSelectedText()
763
+
764
+ self.console().insertPlainText(text)
765
+ self.focusToConsole()
766
+
767
+ def copyToWorkbox(self):
768
+ """Copy current selection or line from console to workbox"""
769
+ console = self.console()
770
+ if not console.hasFocus():
771
+ return
772
+
773
+ cursor = console.textCursor()
774
+ if not cursor.hasSelection():
775
+ cursor.select(QTextCursor.SelectionType.LineUnderCursor)
776
+ text = cursor.selectedText()
777
+ prompt = console.prompt()
778
+ if text.startswith(prompt):
779
+ text = text[len(prompt) :]
780
+ text = text.lstrip()
781
+
782
+ outputPrompt = console.outputPrompt()
783
+ outputPrompt = outputPrompt.rstrip()
784
+ if text.startswith(outputPrompt):
785
+ text = text[len(outputPrompt) :]
786
+ text = text.lstrip()
787
+
788
+ if not text:
789
+ return
790
+
791
+ workbox = self.current_workbox()
792
+ workbox.__remove_selected_text__()
793
+ workbox.__insert_text__(text)
794
+
795
+ line, index = workbox.__cursor_position__()
796
+ index += len(text)
797
+ workbox.__set_cursor_position__(line, index)
798
+
799
+ self.focusToWorkbox()
800
+
801
+ def getNextCommand(self):
802
+ if hasattr(self.console(), 'getNextCommand'):
803
+ self.console().getNextCommand()
804
+
805
+ def getPrevCommand(self):
806
+ if hasattr(self.console(), 'getPrevCommand'):
807
+ self.console().getPrevCommand()
808
+
809
+ def wheelEvent(self, event):
810
+ """adjust font size on ctrl+scrollWheel"""
811
+ mods = event.modifiers()
812
+ ctrl = Qt.KeyboardModifier.ControlModifier
813
+ shift = Qt.KeyboardModifier.ShiftModifier
814
+ alt = Qt.KeyboardModifier.AltModifier
815
+
816
+ ctrlAlt = ctrl | alt
817
+ shiftAlt = shift | alt
818
+
819
+ # Assign mods by functionality. Using shift | alt for gui, because just shift or
820
+ # just alt has existing functionality which also processes.
821
+ code_font_mod = ctrl
822
+
823
+ if mods == code_font_mod or mods == self.gui_font_mod:
824
+ # WheelEvents can be emitted in a cluster, but we only want one at a time
825
+ # (ie to change font size by 1, rather than 2 or 3). Let's bail if previous
826
+ # font-resize wheel event was within a certain threshhold.
827
+ now = datetime.now()
828
+ elapsed = now - self.previousFontResizeTime
829
+ tolerance = timedelta(microseconds=100000)
830
+ if elapsed < tolerance:
831
+ return
832
+ self.previousFontResizeTime = now
833
+
834
+ # QT4 presents QWheelEvent.delta(), QT5 has QWheelEvent.angleDelta().y()
835
+ if hasattr(event, 'delta'): # Qt4
836
+ delta = event.delta()
837
+ else: # QT5
838
+ # Also holding alt reverses the data in angleDelta (!?), so transpose to
839
+ # get correct value
840
+ angleDelta = event.angleDelta()
841
+ if mods == alt or mods == ctrlAlt or mods == shiftAlt:
842
+ angleDelta = angleDelta.transposed()
843
+ delta = angleDelta.y()
844
+
845
+ # convert delta to +1 or -1, depending
846
+ delta = delta // abs(delta)
847
+ minSize = 5
848
+ maxSize = 50
849
+ if mods == code_font_mod:
850
+ font = self.console().font()
851
+ elif mods == self.gui_font_mod:
852
+ font = self.font()
853
+ newSize = font.pointSize() + delta
854
+ newSize = max(min(newSize, maxSize), minSize)
855
+
856
+ # If only ctrl was pressed, adjust code font size, otherwise adjust gui font
857
+ # size
858
+ if mods == self.gui_font_mod:
859
+ self.setGuiFont(newSize=newSize)
860
+ elif mods == code_font_mod:
861
+ self.setFontSize(newSize)
862
+ else:
863
+ Window.wheelEvent(self, event)
864
+
865
+ def adjustFontSize(self, kind, delta):
866
+ if kind == "Code":
867
+ size = self.console().font().pointSize()
868
+ size += delta
869
+ self.setFontSize(size)
870
+ else:
871
+ size = self.font().pointSize()
872
+ size += delta
873
+ self.setGuiFont(newSize=size)
874
+
875
+ def selectFont(
876
+ self, origFont=None, monospace=False, proportional=False, doGui=False
877
+ ):
878
+ """Present a QFontChooser dialog, offering, monospace, proportional, or all
879
+ fonts, based on user choice. If a font is chosen, set it on the console and
880
+ workboxes.
881
+
882
+ Args:
883
+ action (QAction): menu action associated with chosen font
884
+ """
885
+ if origFont is None:
886
+ origFont = self.console().font()
887
+ curFontFamily = origFont.family()
888
+
889
+ if monospace and proportional:
890
+ options = (
891
+ QFontDialog.FontDialogOption.MonospacedFonts
892
+ | QFontDialog.FontDialogOption.ProportionalFonts
893
+ )
894
+ kind = "monospace or proportional "
895
+ elif monospace:
896
+ options = QFontDialog.FontDialogOption.MonospacedFonts
897
+ kind = "monospace "
898
+ elif proportional:
899
+ options = QFontDialog.FontDialogOption.ProportionalFonts
900
+ kind = "proportional "
901
+
902
+ # Present a QFontDialog for user to choose a font
903
+ title = "Pick a {} font. Current font is: {}".format(kind, curFontFamily)
904
+ newFont, okClicked = QFontDialog.getFont(origFont, self, title, options=options)
905
+
906
+ if okClicked:
907
+ if doGui:
908
+ self.setGuiFont(newFont=newFont)
909
+ else:
910
+ self.console().setConsoleFont(newFont)
911
+ self.setWorkboxFontBasedOnConsole()
912
+ self.setEditorChooserFontBasedOnConsole()
913
+
914
+ def selectGuiFont(self, monospace=True, proportional=True):
915
+ font = self.font()
916
+ self.selectFont(
917
+ origFont=font, monospace=monospace, proportional=proportional, doGui=True
918
+ )
919
+
920
+ def setGuiFont(self, newSize=None, newFont=None):
921
+ current = self.uiWorkboxTAB.currentWidget()
922
+ if not current:
923
+ return
924
+
925
+ tabbar_class = current.tabBar().__class__
926
+ menubar_class = self.menuBar().__class__
927
+ label_class = self.uiStatusLBL.__class__
928
+ children = self.findChildren(tabbar_class, None)
929
+ children.extend(self.findChildren(menubar_class, None))
930
+ children.extend(self.findChildren(label_class, None))
931
+ children.extend(self.findChildren(QToolButton, None))
932
+ children.extend(self.findChildren(QMenu, None))
933
+ children.extend(self.findChildren(QToolTip, None))
934
+
935
+ for child in children:
936
+ if not hasattr(child, "setFont"):
937
+ continue
938
+ if newFont is None:
939
+ newFont = child.font()
940
+ if newSize is None:
941
+ newSize = newFont.pointSize()
942
+ newFont.setPointSize(newSize)
943
+ child.setFont(newFont)
944
+ self.setFont(newFont)
945
+ QToolTip.setFont(newFont)
946
+
947
+ self.setDialogFont(self.uiPreferencesPAGE)
948
+
949
+ def setFontSize(self, newSize):
950
+ """Update the font size in the console and current workbox.
951
+
952
+ Args:
953
+ newSize (int): The new size to set the font
954
+ """
955
+ font = self.console().font()
956
+ font.setPointSize(newSize)
957
+ # Also setPointSizeF, which is what gets written to prefs, to prevent
958
+ # needlessly writing prefs with only a change in pointSizeF precision.
959
+ font.setPointSizeF(font.pointSize())
960
+
961
+ self.console().setConsoleFont(font)
962
+
963
+ self.setWorkboxFontBasedOnConsole()
964
+ self.setEditorChooserFontBasedOnConsole()
965
+
966
+ def setWorkboxFontBasedOnConsole(self, workbox=None):
967
+ """If the current workbox's font is different to the console's font, set it to
968
+ match.
969
+ """
970
+ font = self.console().font()
971
+
972
+ if workbox is None:
973
+ workboxGroup = self.uiWorkboxTAB.currentWidget()
974
+ if workboxGroup is None:
975
+ return
976
+ workbox = workboxGroup.currentWidget()
977
+ if workbox is None:
978
+ return
979
+
980
+ if workbox.__font__() != font:
981
+ workbox.__set_margins_font__(font)
982
+ workbox.__set_font__(font)
983
+
984
+ def setEditorChooserFontBasedOnConsole(self):
985
+ """Set the EditorChooser font to match console. This helps with legibility when
986
+ using EditorChooser.
987
+ """
988
+ self.setDialogFont(self.uiEditorChooserWGT)
989
+
990
+ def setDialogFont(self, dialog):
991
+ """Helper for when creating a dialog to have the font match the PrEditor font
992
+
993
+ Args:
994
+ dialog (QDialog): The dialog for which to set the font
995
+ """
996
+ for thing in dialog.findChildren(QObject):
997
+ if hasattr(thing, "setFont"):
998
+ thing.setFont(self.font())
999
+
1000
+ @classmethod
1001
+ def _genPrefName(cls, baseName, index):
1002
+ if index:
1003
+ baseName = '{name}{index}'.format(name=baseName, index=index)
1004
+ return baseName
1005
+
1006
+ def adjustWorkboxOrientation(self, state):
1007
+ if state:
1008
+ self.uiSplitterSPLIT.setOrientation(Qt.Orientation.Horizontal)
1009
+ else:
1010
+ self.uiSplitterSPLIT.setOrientation(Qt.Orientation.Vertical)
1011
+
1012
+ def backupPreferences(self):
1013
+ """Saves a copy of the current preferences to a zip archive."""
1014
+ zip_path = prefs.backup()
1015
+ print('PrEditor Preferences backed up to "{}"'.format(zip_path))
1016
+ return zip_path
1017
+
1018
+ def browsePreferences(self):
1019
+ prefs.browse(core_name=self.name)
1020
+
1021
+ def console(self):
1022
+ return self.uiConsoleTXT
1023
+
1024
+ def setConsoleHighlightEnabled(self, state):
1025
+ self.console().codeHighlighter().setEnabled(state)
1026
+
1027
+ def clearLog(self):
1028
+ self.uiConsoleTXT.clear()
1029
+
1030
+ def clearLogToFile(self):
1031
+ """If installLogToFile has been called, clear the stdout."""
1032
+ if self._stds:
1033
+ self._stds[0].clear(stamp=True)
1034
+
1035
+ def prune_backup_files(self, sub_dir=None):
1036
+ """Prune the backup files to uiMaxNumBackupsSPIN value, per workbox
1037
+
1038
+ Args:
1039
+ sub_dir (str, optional): The subdir to operate on.
1040
+ """
1041
+ if sub_dir is None:
1042
+ sub_dir = 'workboxes'
1043
+
1044
+ directory = Path(prefs.prefs_path(sub_dir, core_name=self.name))
1045
+ files = list(directory.rglob("*.*"))
1046
+
1047
+ files_by_name = {}
1048
+ for file in files:
1049
+ match = PRUNE_PATTERN.search(str(file))
1050
+ if not match:
1051
+ continue
1052
+ name = match.groupdict().get("name")
1053
+
1054
+ parent = file.parent.name
1055
+ name = parent + "/" + name
1056
+ files_by_name.setdefault(name, []).append(file)
1057
+
1058
+ for _name, files in files_by_name.items():
1059
+ files.sort(key=lambda f: str(f).lower())
1060
+ files.reverse()
1061
+ max_num_backups = self.uiMaxNumBackupsSPIN.value()
1062
+ for file in files[max_num_backups:]:
1063
+ file.unlink()
1064
+
1065
+ # Remove any empty directories
1066
+ for file in directory.iterdir():
1067
+ if not file.is_dir():
1068
+ continue
1069
+
1070
+ # rmdir only operates on empty dirs. Try / except is faster than
1071
+ # getting number of files, ie len(list(file.iterdir()))
1072
+ try:
1073
+ file.rmdir()
1074
+ except OSError:
1075
+ pass
1076
+
1077
+ def remove_old_workbox_folders(self):
1078
+ """Remove from disk any old workbox backup folders. We find all current
1079
+ open workbox's workbox_ids, and add any workbox_ids from the recently
1080
+ closed workbox menu. Any workbox folders which are not in that list will
1081
+ be moved to the workbox_recycle_bin.
1082
+ """
1083
+
1084
+ # Collect the workbox_ids for all currently open workboxes, and all
1085
+ # recently closed workboxes.
1086
+ keeper_workbox_ids = []
1087
+ for info in self.uiWorkboxTAB.all_widgets():
1088
+ workbox = info[0]
1089
+ keeper_workbox_ids.append(workbox.__workbox_id__())
1090
+ for action in self.uiClosedWorkboxesMENU.actions():
1091
+ data = action.data()
1092
+ workbox_id = data.get("workbox_id")
1093
+ keeper_workbox_ids.append(workbox_id)
1094
+
1095
+ # Look at all workbox folders on disk. If it's in the list collected
1096
+ # above, it's a keeper, otherwise it's to be deleted.
1097
+ keepers = []
1098
+ to_remove = []
1099
+ workbox_dir = self.prefsPath("workboxes")
1100
+ for file in Path(workbox_dir).iterdir():
1101
+ if file.is_file():
1102
+ continue
1103
+ if file.name not in keeper_workbox_ids:
1104
+ to_remove.append(file)
1105
+ else:
1106
+ keepers.append(file)
1107
+
1108
+ # We should have at least one keeper. If not, it means this is being run
1109
+ # early, before the workboxes are shown, so we do not remove anything
1110
+ # (we would be removing every workbox directory in this case).
1111
+ if not keepers:
1112
+ return
1113
+
1114
+ # Go thru each to_remove folder, move it to the recycle bin.
1115
+ bin_path = Path(self.prefsPath("workbox_recycle_bin"))
1116
+ for directory in to_remove:
1117
+ new_path = bin_path / directory.name
1118
+ # If somehow new_path already exists, remove it first
1119
+ if new_path.exists():
1120
+ try:
1121
+ new_path.unlink()
1122
+ except PermissionError:
1123
+ msg = (
1124
+ "Unable to remove very old workbox directory:\n"
1125
+ f"{new_path}\ndue to a permission error."
1126
+ )
1127
+ logger.warning(msg)
1128
+
1129
+ shutil.move(directory, new_path)
1130
+
1131
+ def empty_workbox_recycle_bin(self):
1132
+ """Remove any old workbox folders from the workbox_recycle_bin"""
1133
+
1134
+ msg = "Are you sure you want to empty the workbox recycle bin?"
1135
+ ret = QMessageBox.question(
1136
+ self,
1137
+ 'Confirm empty workbox recycle bin',
1138
+ msg,
1139
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel,
1140
+ )
1141
+ if ret != QMessageBox.StandardButton.Yes:
1142
+ return
1143
+
1144
+ bin_path = Path(self.prefsPath("workbox_recycle_bin"))
1145
+ if not bin_path.exists():
1146
+ return
1147
+
1148
+ for file in bin_path.iterdir():
1149
+ if file.is_dir():
1150
+ for sub_file in file.iterdir():
1151
+ sub_file.unlink()
1152
+ file.rmdir()
1153
+ else:
1154
+ file.unlink()
1155
+
1156
+ def getBoxesChangedByInstance(self, timeOffset=0.05):
1157
+ self.latestTimeStrsForBoxesChangedViaInstance = {}
1158
+
1159
+ for editor_info in self.uiWorkboxTAB.all_widgets():
1160
+ editor, group_name, tab_name, group_idx, tab_idx = editor_info
1161
+ if not editor.__is_dirty__():
1162
+ continue
1163
+
1164
+ core_name = self.name
1165
+ workbox_id = editor.__workbox_id__()
1166
+ versionType = prefs.VersionTypes.Last
1167
+ latest_filepath, idx, count = prefs.get_backup_version_info(
1168
+ core_name, workbox_id, versionType
1169
+ )
1170
+ latest_filepath = prefs.get_relative_path(self.name, latest_filepath)
1171
+
1172
+ if latest_filepath != editor.__backup_file__():
1173
+ stem = Path(latest_filepath).stem
1174
+ match = prefs.DATETIME_PATTERN.search(stem)
1175
+ if not match:
1176
+ continue
1177
+
1178
+ datetimeStr = match.group()
1179
+ origStamp = datetime.strptime(datetimeStr, prefs.DATETIME_FORMAT)
1180
+
1181
+ newStamp = origStamp - timedelta(seconds=timeOffset)
1182
+ newStamp = newStamp.strftime(prefs.DATETIME_FORMAT)
1183
+
1184
+ self.latestTimeStrsForBoxesChangedViaInstance[workbox_id] = newStamp
1185
+ editor.__set_changed_by_instance__(True)
1186
+
1187
+ def setFileMonitoringEnabled(self, filename, state):
1188
+ """Enables/Disables open file change monitoring. If enabled, A dialog will pop
1189
+ up when ever the open file is changed externally. If file monitoring is
1190
+ disabled in the IDE settings it will be ignored.
1191
+
1192
+ Returns:
1193
+ bool:
1194
+ """
1195
+ # if file monitoring is enabled and we have a file name then set up the file
1196
+ # monitoring
1197
+ if not filename:
1198
+ return
1199
+
1200
+ if state:
1201
+ self.openFileMonitor.addPath(filename)
1202
+ else:
1203
+ self.openFileMonitor.removePath(filename)
1204
+
1205
+ def fileMonitoringEnabled(self, filename):
1206
+ """Returns whether the provide filename is currently being watched, ie
1207
+ is listed in self.openFileMonitor.files()
1208
+
1209
+ Args:
1210
+ filename (str): The filename to determine if being watched
1211
+
1212
+ Returns:
1213
+ bool: Whether filename is being watched.
1214
+ """
1215
+ if not filename:
1216
+ return False
1217
+
1218
+ watched_files = [Path(file) for file in self.openFileMonitor.files()]
1219
+ return Path(filename) in watched_files
1220
+
1221
+ def prefsPath(self, name='preditor_pref.json'):
1222
+ """Get the path to this core's prefs, for the given name
1223
+
1224
+ Args:
1225
+ name (str, optional): This name is appended to the found prefs path,
1226
+ defaults to 'preditor_pref.json'
1227
+
1228
+ Returns:
1229
+ path (str): The determined filepath
1230
+ """
1231
+ path = prefs.prefs_path(name, core_name=self.name)
1232
+ return path
1233
+
1234
+ def indexOfWorkboxOrTabGroup(self, widget):
1235
+ """For the given widget, the the index of it's tab widget that contains
1236
+ it.
1237
+
1238
+ Args:
1239
+ widget (GroupedTabWidget, WorkboxMixin): The workbox or tab group
1240
+ for which to find it's index
1241
+
1242
+ Returns:
1243
+ tabIdx (int, None): The found tab index or None
1244
+ """
1245
+ tabIdx = None
1246
+ if not (widget.parent() and widget.parent().parent()):
1247
+ return tabIdx
1248
+
1249
+ grandParent = widget.parent().parent()
1250
+ for index in range(grandParent.count()):
1251
+ curWidget = grandParent.widget(index)
1252
+ if curWidget == widget:
1253
+ tabIdx = index
1254
+ break
1255
+ return tabIdx
1256
+
1257
+ def updateTabColorsAndToolTips(self):
1258
+ """Go thru all the tab groups and update their text color and toolTips."""
1259
+ group = self.uiWorkboxTAB
1260
+ for index in range(self.uiWorkboxTAB.count()):
1261
+ grouped = group.widget(index)
1262
+ grouped.tabBar().updateColorsAndToolTips()
1263
+
1264
+ def linkedFileChanged(self, filename):
1265
+ """Slot for responding to the file watcher's signal. Handle updating this
1266
+ PrEditor instance accordingly.
1267
+
1268
+ Args:
1269
+ filename (str): The file which triggered the file changed signal
1270
+ """
1271
+
1272
+ # Either handle prefs or workbox
1273
+ if Path(filename) == Path(self.prefsPath()):
1274
+ # First, save workbox prefs. Don't save preditor.prefs because that
1275
+ # would just overwrite whatever changes we are responding to.
1276
+ self.getBoxesChangedByInstance()
1277
+ self.recordWorkboxPrefs()
1278
+ # Now restore prefs, which will use the updated preditor prefs (from
1279
+ # another preditor instance)
1280
+ self.restorePrefs(skip_geom=True)
1281
+ else:
1282
+ for info in self.uiWorkboxTAB.all_widgets():
1283
+ editor, _, _, _, _ = info
1284
+ if not editor or not editor.__filename__():
1285
+ continue
1286
+ if Path(editor.__filename__()) == Path(filename):
1287
+ editor.__set_file_monitoring_enabled__(False)
1288
+
1289
+ choice = editor.__maybe_reload_file__()
1290
+ # Save a backup of any unsaved changes
1291
+ if choice:
1292
+ editor.__save_prefs__(saveLinkedFile=False, force=True)
1293
+
1294
+ filename = editor.__filename__()
1295
+ if filename and Path(filename).is_file():
1296
+ editor.__set_file_monitoring_enabled__(True)
1297
+ self.updateTabColorsAndToolTips()
1298
+
1299
+ def closeEvent(self, event):
1300
+ self.recordPrefs()
1301
+ # Save the logger configuration
1302
+ lcfg = LoggingConfig(core_name=self.name)
1303
+ lcfg.build()
1304
+ lcfg.save()
1305
+
1306
+ super(LoggerWindow, self).closeEvent(event)
1307
+ if self.uiConsoleTOOLBAR.isFloating():
1308
+ self.uiConsoleTOOLBAR.hide()
1309
+
1310
+ # Handle any cleanup each workbox tab may need to do before closing
1311
+ for editor, _, _, _, _ in self.uiWorkboxTAB.all_widgets():
1312
+ editor.__close__()
1313
+
1314
+ def closeLoggerByAction(self):
1315
+ if self.uiConfirmBeforeCloseCHK.isChecked():
1316
+ msg = "Are you sure you want to close PrEditor?"
1317
+
1318
+ state_str = "enabled" if self.autoSaveEnabled() else "disabled"
1319
+ msg += f"\n\nAuto Save is {state_str}"
1320
+ ret = QMessageBox.question(
1321
+ self,
1322
+ 'Confirm close',
1323
+ msg,
1324
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel,
1325
+ )
1326
+ if ret != QMessageBox.StandardButton.Yes:
1327
+ return
1328
+ self.close()
1329
+
1330
+ def execAll(self):
1331
+ """Clears the console before executing all workbox code"""
1332
+ if self.uiClearBeforeRunningCHK.isChecked():
1333
+ self.clearLog()
1334
+ self.current_workbox().__exec_all__()
1335
+
1336
+ if self.uiAutoPromptCHK.isChecked():
1337
+ console = self.console()
1338
+ prompt = console.prompt()
1339
+ console.startPrompt(prompt)
1340
+
1341
+ def execSelected(self, truncate=True):
1342
+ """Clears the console before executing selected workbox code.
1343
+
1344
+ NOTE! This method is not called when the uiRunSelectedACT is triggered,
1345
+ because the workbox will always intercept it. So instead, the workbox's
1346
+ keyPressEvent will notice the shortcut and call this method.
1347
+ """
1348
+ if self.uiClearBeforeRunningCHK.isChecked():
1349
+ self.clearLog()
1350
+
1351
+ self.current_workbox().__exec_selected__(truncate=truncate)
1352
+
1353
+ if self.uiAutoPromptCHK.isChecked():
1354
+ self.console().startInputLine()
1355
+
1356
+ def clearExecutionTime(self):
1357
+ """Update status text with hyphens to indicate execution has begun."""
1358
+ self.setStatusText('Exec: -.- Seconds')
1359
+ QApplication.instance().processEvents()
1360
+ self.statusTimer.stop()
1361
+
1362
+ def reportExecutionTime(self, seconds):
1363
+ """Update status text with seconds passed in."""
1364
+ self.uiStatusLBL.showSeconds(seconds)
1365
+ self.uiMenuBar.adjustSize()
1366
+ self.statusTimer.stop()
1367
+
1368
+ def recordPrefs(self, manual=False, disableFileMonitoring=False):
1369
+ if not manual and not self.autoSaveEnabled():
1370
+ return
1371
+
1372
+ # When applying a change to editor class, we may essentially auto-save
1373
+ # prefs, in order to reload on the next class. In doing so, we may be
1374
+ # changing workbox filename(s), if any, so let's remove them from file
1375
+ # monitoring. They will be re-added during restorePrefs.
1376
+ if disableFileMonitoring:
1377
+ for editor_info in self.uiWorkboxTAB.all_widgets():
1378
+ editor = editor_info[0]
1379
+ editor.__set_file_monitoring_enabled__(False)
1380
+
1381
+ origPref = self.load_prefs()
1382
+ pref = copy.deepcopy(origPref)
1383
+ geo = self.geometry()
1384
+
1385
+ pref.update(
1386
+ {
1387
+ 'loggergeom': [geo.x(), geo.y(), geo.width(), geo.height()],
1388
+ 'windowState': QtCompat.enumValue(self.windowState()),
1389
+ 'splitterVertical': self.uiEditorVerticalCHK.isChecked(),
1390
+ 'splitterSize': self.uiSplitterSPLIT.sizes(),
1391
+ 'tabIndent': self.uiIndentationsTabsCHK.isChecked(),
1392
+ 'copyIndentsAsSpaces': self.uiCopyTabsToSpacesCHK.isChecked(),
1393
+ 'hintingEnabled': self.uiConsoleAutoCompleteEnabledCHK.isChecked(),
1394
+ 'workboxHintingEnabled': (
1395
+ self.uiWorkboxAutoCompleteEnabledCHK.isChecked()
1396
+ ),
1397
+ 'spellCheckEnabled': self.uiSpellCheckEnabledCHK.isChecked(),
1398
+ 'wordWrap': self.uiWordWrapCHK.isChecked(),
1399
+ 'clearBeforeRunning': self.uiClearBeforeRunningCHK.isChecked(),
1400
+ 'toolbarStates': str(self.saveState().toHex(), 'utf-8'),
1401
+ 'guiFont': self.font().toString(),
1402
+ 'consoleFont': self.console().font().toString(),
1403
+ 'autoSaveSettings': self.autoSaveEnabled(),
1404
+ 'promptOnLinkedChange': self.promptOnLinkedChange(),
1405
+ 'autoPrompt': self.uiAutoPromptCHK.isChecked(),
1406
+ 'errorHyperlinks': self.uiErrorHyperlinksCHK.isChecked(),
1407
+ 'uiStatusLbl_limit': self.uiStatusLBL.limit(),
1408
+ 'textEditorPath': self.textEditorPath,
1409
+ 'textEditorCmdTempl': self.textEditorCmdTempl,
1410
+ 'separateTraceback': self.uiSeparateTracebackCHK.isChecked(),
1411
+ 'currentStyleSheet': self._stylesheet,
1412
+ 'flash_time': self.uiFlashTimeSPIN.value(),
1413
+ 'find_files_regex': self.uiFindInWorkboxesWGT.uiRegexBTN.isChecked(),
1414
+ 'find_files_cs': (
1415
+ self.uiFindInWorkboxesWGT.uiCaseSensitiveBTN.isChecked()
1416
+ ),
1417
+ 'find_files_context': self.uiFindInWorkboxesWGT.uiContextSPN.value(),
1418
+ 'find_files_text': self.uiFindInWorkboxesWGT.uiFindTXT.text(),
1419
+ 'highlightExactCompletion': (
1420
+ self.uiHighlightExactCompletionCHK.isChecked()
1421
+ ),
1422
+ 'dont_ask_again': self.dont_ask_again,
1423
+ 'max_num_backups': self.uiMaxNumBackupsSPIN.value(),
1424
+ 'max_recent_workboxes': self.uiMaxNumRecentWorkboxesSPIN.value(),
1425
+ 'closedWorkboxData': self.getClosedWorkboxData(),
1426
+ 'confirmBeforeClose': self.uiConfirmBeforeCloseCHK.isChecked(),
1427
+ 'displayExtraTooltipInfo': self.uiExtraTooltipInfoCHK.isChecked(),
1428
+ 'consoleHighlightEnabled': (
1429
+ self.uiConsoleHighlightEnabledCHK.isChecked()
1430
+ ),
1431
+ 'repaintConsolesOnWrite': self.uiRepaintConsolesOnWriteCHK.isChecked(),
1432
+ 'repaintProcessEventsOccasionally': (
1433
+ self.uiRepaintProcessEventsOccasionallyCHK.isChecked()
1434
+ ),
1435
+ 'repaintConsolesperSecond': self.uiRepaintConsolesPerSecondSPIN.value(),
1436
+ }
1437
+ )
1438
+
1439
+ # completer settings
1440
+ completer = self.console().completer()
1441
+ pref["caseSensitive"] = completer.caseSensitive()
1442
+ pref["completerMode"] = completer.completerMode().value
1443
+
1444
+ if self._stylesheet == 'Custom':
1445
+ pref['styleSheet'] = self.styleSheet()
1446
+
1447
+ workbox_prefs = self.uiWorkboxTAB.save_prefs()
1448
+ pref['workbox_prefs'] = workbox_prefs
1449
+
1450
+ pref['editor_cls'] = self.editor_cls_name
1451
+
1452
+ # Allow any plugins to add their own preferences dictionary
1453
+ pref["plugins"] = {}
1454
+ for name, plugin in self.plugins.items():
1455
+ plugin_pref = plugin.record_prefs(name)
1456
+ if plugin_pref:
1457
+ pref["plugins"][name] = plugin_pref
1458
+
1459
+ # Only save if different from previous pref.
1460
+ if pref != origPref:
1461
+ self.save_prefs(pref)
1462
+ self.setStatusText("Prefs saved")
1463
+ else:
1464
+ self.setStatusText("No changed prefs to save")
1465
+ self.autoHideStatusText()
1466
+
1467
+ def auto_backup_prefs(self, filename, onlyFirst=False):
1468
+ """Auto backup prefs for logger window itself.
1469
+
1470
+ TODO: Implement method to easily scroll thru backups. Maybe difficult, due the
1471
+ myriad combinations of workboxes and workboxes version. Maybe ignore workboxes,
1472
+ and just to the dialog prefs and/or existing workbox names
1473
+
1474
+ Args:
1475
+ filename (str): The filename to backup
1476
+ onlyFirst (bool, optional): Flag to create initial backup, and not
1477
+ subsequent ones. Used when dialog launched for the first time.
1478
+ """
1479
+ path = Path(filename)
1480
+ name = path.name
1481
+ stem = path.stem
1482
+ bak_path = prefs.create_stamped_path(self.name, name, sub_dir='prefs_bak')
1483
+
1484
+ # If we are calling from load_prefs, onlyFirst will be True, so we can
1485
+ # autoBack the prefs the first time.
1486
+ existing = list(Path(bak_path).parent.glob("{}*.json".format(stem)))
1487
+ if onlyFirst and len(existing):
1488
+ return
1489
+
1490
+ if path.is_file():
1491
+ shutil.copy(path, bak_path)
1492
+
1493
+ self.setStatusText("Prefs saved")
1494
+ self.autoHideStatusText()
1495
+
1496
+ def load_prefs(self):
1497
+ filename = self.prefsPath()
1498
+ self.setStatusText('Loaded Prefs: {} '.format(filename))
1499
+ self.autoHideStatusText()
1500
+
1501
+ prefs_dict = {}
1502
+ self.auto_backup_prefs(filename, onlyFirst=True)
1503
+ filename = Path(filename)
1504
+ if filename.exists():
1505
+ try:
1506
+ prefs_dict = Json(filename).load()
1507
+ except ValueError as error:
1508
+ # If there is a problem with the preferences ask the user if they
1509
+ # want to reset them. Depending on the problem the loaded workbox's
1510
+ # have likely already losing the tab information, but this does
1511
+ # allow the user to try to debug the file instead of just resetting
1512
+ # preferences. The .py files likely still exist but won't have names.
1513
+ msg = ( # noqa: E702, E231
1514
+ "The following error happened while restoring PrEditor prefs:",
1515
+ f'<p style="color: red;">{error}</p>',
1516
+ "This can be resolved by resetting the prefs. Do you want "
1517
+ "to do it?",
1518
+ )
1519
+ box = QMessageBox()
1520
+ box.setIcon(QMessageBox.Icon.Question)
1521
+ box.setWindowTitle("Reset Corrupted Preferences?")
1522
+ box.setTextFormat(Qt.TextFormat.RichText)
1523
+ box.setText("<br>".join(msg))
1524
+ box.addButton(QMessageBox.StandardButton.Yes)
1525
+ box.addButton(QMessageBox.StandardButton.No)
1526
+ if box.exec() == QMessageBox.StandardButton.Yes:
1527
+ prefs_dict = {}
1528
+ with filename.open("w") as fp:
1529
+ json.dump(prefs_dict, fp, indent=4, sort_keys=True)
1530
+ else:
1531
+ raise
1532
+
1533
+ return prefs_dict
1534
+
1535
+ def autoBackupForTransition(self, prefs_dict):
1536
+ """Since changing how workboxes are based to workbox_id is a major change,
1537
+ do a full prefs backup the first time. This is based on the prefs attr
1538
+ 'prefs_version'. If less than 2.0, it will perform a full backup.
1539
+
1540
+ Args:
1541
+ prefs_dict (dict): The (newly loaded) prefs.
1542
+ """
1543
+ prefs_version = prefs_dict.get("prefs_version", 1.0)
1544
+ if prefs_version < 2.0:
1545
+ self.backupPreferences()
1546
+
1547
+ def transitionToNewPrefs(self, prefs_dict):
1548
+ """To facilitate renaming / changing prefs attrs, load a json dict which
1549
+ defines the changes, and then apply them. This can usually include a
1550
+ 'prefs_version' attr associated with the changes.
1551
+
1552
+ Args:
1553
+ prefs_dict (dict): The (newly loaded) prefs.
1554
+
1555
+ Returns:
1556
+ new_prefs_dict (dict): The updated prefs dict
1557
+ """
1558
+ self.prefs_updates = prefs.get_prefs_updates()
1559
+
1560
+ orig_prefs_dict = copy.deepcopy(prefs_dict)
1561
+ new_prefs_dict = prefs.update_prefs_args(
1562
+ self.name, prefs_dict, self.prefs_updates
1563
+ )
1564
+ if new_prefs_dict != orig_prefs_dict:
1565
+ self.save_prefs(new_prefs_dict, at_prefs_update=True)
1566
+
1567
+ return new_prefs_dict
1568
+
1569
+ def save_prefs(self, pref, at_prefs_update=False):
1570
+ # Save preferences to disk
1571
+ filename = self.prefsPath()
1572
+ path = Path(filename)
1573
+ path.parent.mkdir(exist_ok=True, parents=True)
1574
+
1575
+ # Write to temp file first, then copy over, because we may have a
1576
+ # QFileSystemWatcher for the prefs file, and the 2 lines "with open"
1577
+ # and "json.dump" triggers 2 file changed signals.
1578
+ temp_stem = path.stem + "_TEMP"
1579
+ temp_name = temp_stem + path.suffix
1580
+ temp_path = path.with_name(temp_name)
1581
+ with open(temp_path, 'w') as fp:
1582
+ json.dump(pref, fp, indent=4, sort_keys=True)
1583
+
1584
+ self.setFileMonitoringEnabled(self.prefsPath(), False)
1585
+ shutil.copy(temp_path, path)
1586
+ self.setFileMonitoringEnabled(self.prefsPath(), True)
1587
+ temp_path.unlink()
1588
+
1589
+ self.auto_backup_prefs(filename)
1590
+
1591
+ # We may have just updated prefs, and are saving that update. In this
1592
+ # case, do not prune or remove old folder, because we don't have the correct
1593
+ # max number values set yet spinner values.
1594
+ if not at_prefs_update:
1595
+ self.prune_backup_files(sub_dir='workboxes')
1596
+ self.prune_backup_files(sub_dir='prefs_bak')
1597
+ self.remove_old_workbox_folders()
1598
+
1599
+ def maybeDisplayDialog(self, dialog):
1600
+ """If user hasn't previously opted to not show this particular dialog again,
1601
+ show it.
1602
+ """
1603
+ if dialog.objectName() in self.dont_ask_again:
1604
+ return
1605
+
1606
+ dialog.exec()
1607
+
1608
+ def restartLogger(self):
1609
+ """Closes this PrEditor instance and starts a new process with the same
1610
+ cli arguments.
1611
+
1612
+ Note: This only works if PrEditor is running in standalone mode. It doesn't
1613
+ quit the QApplication or other host process. It simply closes this instance
1614
+ of PrEditor, saving its preferences, which should allow Qt to exit if no
1615
+ other windows are open.
1616
+ """
1617
+ self.close()
1618
+
1619
+ # Get the current command and launch it as a new process. This handles
1620
+ # use of the preditor/preditor executable launchers.
1621
+ cmd = sys.argv[0]
1622
+ args = sys.argv[1:]
1623
+
1624
+ if os.path.basename(cmd) == "__main__.py":
1625
+ # Handles using `python -m preditor` style launch.
1626
+ cmd = sys.executable
1627
+ args = ["-m", "preditor"] + args
1628
+ QtCore.QProcess.startDetached(cmd, args)
1629
+
1630
+ def recordWorkboxPrefs(self):
1631
+ self.uiWorkboxTAB.save_prefs()
1632
+
1633
+ def restoreWorkboxPrefs(self, pref):
1634
+ workbox_prefs = pref.get('workbox_prefs', {})
1635
+ try:
1636
+ self.uiWorkboxTAB.hide()
1637
+ self.uiWorkboxTAB.restore_prefs(workbox_prefs)
1638
+ finally:
1639
+ self.uiWorkboxTAB.show()
1640
+
1641
+ def restorePrefs(self, skip_geom=False):
1642
+ pref = self.load_prefs()
1643
+
1644
+ # Make changes to prefs attrs. Depending on the changes, perform a full
1645
+ # auto-backup first.
1646
+ self.autoBackupForTransition(pref)
1647
+ pref = self.transitionToNewPrefs(pref)
1648
+
1649
+ workbox_path = self.prefsPath("workboxes")
1650
+ Path(workbox_path).mkdir(exist_ok=True)
1651
+
1652
+ # Editor selection
1653
+ self.editor_cls_name = pref.get('editor_cls')
1654
+ if self.editor_cls_name:
1655
+ self.editor_cls_name, editor_cls = plugins.editor(self.editor_cls_name)
1656
+ self.uiWorkboxTAB.editor_cls = editor_cls
1657
+ else:
1658
+ self.uiWorkboxTAB.editor_cls = None
1659
+ # Set the workbox core_name so it reads/writes its tabs content into the
1660
+ # same core_name preference folder.
1661
+ self.uiWorkboxTAB.core_name = self.name
1662
+ self.uiEditorChooserWGT.set_editor_name(self.editor_cls_name)
1663
+
1664
+ # Workboxes
1665
+ self.restoreWorkboxPrefs(pref)
1666
+
1667
+ # Geometry
1668
+ if 'loggergeom' in pref and not skip_geom:
1669
+ self.setGeometry(*pref['loggergeom'])
1670
+ self.uiEditorVerticalCHK.setChecked(pref.get('splitterVertical', False))
1671
+ self.adjustWorkboxOrientation(self.uiEditorVerticalCHK.isChecked())
1672
+
1673
+ sizes = pref.get('splitterSize')
1674
+ if sizes:
1675
+ self.uiSplitterSPLIT.setSizes(sizes)
1676
+ self.setWindowState(Qt.WindowState(pref.get('windowState', 0)))
1677
+ self.uiIndentationsTabsCHK.setChecked(pref.get('tabIndent', True))
1678
+ self.uiCopyTabsToSpacesCHK.setChecked(pref.get('copyIndentsAsSpaces', False))
1679
+
1680
+ # completer settings
1681
+ self.setCaseSensitive(pref.get('caseSensitive', True))
1682
+ completerMode = CompleterMode(pref.get('completerMode', 0))
1683
+ self.cycleToCompleterMode(completerMode)
1684
+ self.setCompleterMode(completerMode)
1685
+ self.uiHighlightExactCompletionCHK.setChecked(
1686
+ pref.get('highlightExactCompletion', False)
1687
+ )
1688
+
1689
+ self.setSpellCheckEnabled(self.uiSpellCheckEnabledCHK.isChecked())
1690
+ self.uiSpellCheckEnabledCHK.setChecked(pref.get('spellCheckEnabled', False))
1691
+ self.uiSpellCheckEnabledCHK.setDisabled(False)
1692
+ self.setAutoSaveEnabled(pref.get('autoSaveSettings', True))
1693
+ self.setPromptOnLinkedChange(pref.get('promptOnLinkedChange', True))
1694
+ self.uiAutoPromptCHK.setChecked(pref.get('autoPrompt', False))
1695
+ self.uiErrorHyperlinksCHK.setChecked(pref.get('errorHyperlinks', True))
1696
+ self.uiStatusLBL.setLimit(pref.get('uiStatusLbl_limit', 5))
1697
+
1698
+ # Find Files settings
1699
+ self.uiFindInWorkboxesWGT.uiRegexBTN.setChecked(
1700
+ pref.get('find_files_regex', False)
1701
+ )
1702
+ self.uiFindInWorkboxesWGT.uiCaseSensitiveBTN.setChecked(
1703
+ pref.get('find_files_cs', False)
1704
+ )
1705
+ self.uiFindInWorkboxesWGT.uiContextSPN.setValue(
1706
+ pref.get('find_files_context', 3)
1707
+ )
1708
+ self.uiFindInWorkboxesWGT.uiFindTXT.setText(pref.get('find_files_text', ''))
1709
+
1710
+ # External text editor filepath and command template
1711
+ defaultExePath = r"C:\Program Files\Sublime Text\sublime_text.exe"
1712
+ defaultCmd = r'"{exePath}" "{modulePath}":{lineNum}'
1713
+ self.textEditorPath = pref.get('textEditorPath', defaultExePath)
1714
+ self.textEditorCmdTempl = pref.get('textEditorCmdTempl', defaultCmd)
1715
+
1716
+ self.uiSeparateTracebackCHK.setChecked(pref.get('separateTraceback', True))
1717
+
1718
+ self.uiWordWrapCHK.setChecked(pref.get('wordWrap', True))
1719
+ self.setWordWrap(self.uiWordWrapCHK.isChecked())
1720
+ self.uiClearBeforeRunningCHK.setChecked(pref.get('clearBeforeRunning', False))
1721
+ self.setClearBeforeRunning(self.uiClearBeforeRunningCHK.isChecked())
1722
+
1723
+ self._stylesheet = pref.get('currentStyleSheet', 'Bright')
1724
+ if self._stylesheet == 'Custom':
1725
+ self.setStyleSheet(pref.get('styleSheet', ''))
1726
+ else:
1727
+ self.setStyleSheet(self._stylesheet)
1728
+ self.uiFlashTimeSPIN.setValue(pref.get('flash_time', 1.0))
1729
+
1730
+ hintingEnabled = pref.get('hintingEnabled', True)
1731
+ self.uiConsoleAutoCompleteEnabledCHK.setChecked(hintingEnabled)
1732
+ self.setAutoCompleteEnabled(hintingEnabled, console=True)
1733
+ workboxHintingEnabled = pref.get('workboxHintingEnabled', True)
1734
+ self.uiWorkboxAutoCompleteEnabledCHK.setChecked(workboxHintingEnabled)
1735
+ self.setAutoCompleteEnabled(workboxHintingEnabled, console=False)
1736
+
1737
+ self.uiConsoleHighlightEnabledCHK.setChecked(
1738
+ pref.get('consoleHighlightEnabled', True)
1739
+ )
1740
+
1741
+ # Max backups and recently closed workboxes
1742
+ max_recent_workboxes = pref.get('max_recent_workboxes', 25)
1743
+ self.uiMaxNumRecentWorkboxesSPIN.setValue(max_recent_workboxes)
1744
+ self.uiMaxNumBackupsSPIN.setValue(pref.get('max_num_backups', 99))
1745
+
1746
+ # List recently closed workboxes
1747
+ closedWorkboxData = pref.get('closedWorkboxData', [])
1748
+ self.buildClosedWorkBoxMenu(closedWorkboxData=closedWorkboxData)
1749
+
1750
+ confirmBeforeClose = pref.get('confirmBeforeClose', True)
1751
+ self.uiConfirmBeforeCloseCHK.setChecked(confirmBeforeClose)
1752
+
1753
+ # Repaint on write configuration
1754
+ self.uiRepaintConsolesOnWriteCHK.setChecked(
1755
+ pref.get('repaintConsolesOnWrite', True)
1756
+ )
1757
+ self.uiRepaintConsolesPerSecondSPIN.setValue(
1758
+ pref.get('repaintConsolesperSecond', 0.2)
1759
+ )
1760
+ self.uiRepaintProcessEventsOccasionallyCHK.setChecked(
1761
+ pref.get('repaintProcessEventsOccasionally', True)
1762
+ )
1763
+ self.uiRepaintProcessEventsOccasionallyCHK.setEnabled(
1764
+ self.uiRepaintConsolesOnWriteCHK.isChecked()
1765
+ )
1766
+ self.updateRepaintDelay()
1767
+
1768
+ # Ensure the correct workbox stack page is shown
1769
+ self.update_workbox_stack()
1770
+
1771
+ fontStr = pref.get('consoleFont', None)
1772
+ if fontStr:
1773
+ font = QFont()
1774
+ if QtCompat.QFont.fromString(font, fontStr):
1775
+ self.console().setConsoleFont(font)
1776
+
1777
+ guiFontStr = pref.get('guiFont', None)
1778
+ if guiFontStr:
1779
+ guiFont = QFont()
1780
+ if QtCompat.QFont.fromString(guiFont, guiFontStr):
1781
+ self.setGuiFont(newFont=guiFont)
1782
+
1783
+ self.dont_ask_again = pref.get('dont_ask_again', [])
1784
+
1785
+ self.uiExtraTooltipInfoCHK.setChecked(
1786
+ pref.get("displayExtraTooltipInfo", False)
1787
+ )
1788
+
1789
+ # Allow any plugins to restore their own preferences
1790
+ for name, plugin in self.plugins.items():
1791
+ plugin.restore_prefs(name, pref.get("plugins", {}).get(name))
1792
+
1793
+ self.restoreToolbars(pref=pref)
1794
+
1795
+ def restoreToolbars(self, pref=None):
1796
+ if pref is None:
1797
+ pref = self.load_prefs()
1798
+
1799
+ state = pref.get('toolbarStates', None)
1800
+ if state:
1801
+ state = QByteArray.fromHex(bytes(state, 'utf-8'))
1802
+ self.restoreState(state)
1803
+
1804
+ def addRecentlyClosedWorkbox(self, workbox):
1805
+ """Add the name of a recently closed workbox to the Recently Closed
1806
+ Workboxes menu, and add a section of it's text as a tooltip. Also, add
1807
+ data (a dict) with information about the workbox, so it can be restored.
1808
+
1809
+ Args:
1810
+ workbox (WorkboxMixin): The workbox being closed
1811
+ max_text_lines (int): How many lines of the workbox text to include
1812
+ on the action's tooltip
1813
+ """
1814
+ # No need to save a blank workbox
1815
+ if not workbox.__text__():
1816
+ return
1817
+
1818
+ workbox_id = workbox.__workbox_id__()
1819
+ workbox_name = workbox.__workbox_name__()
1820
+ filename = workbox.__filename__()
1821
+ backup_file = workbox.__backup_file__()
1822
+
1823
+ # Disable file monitoring
1824
+ workbox.__set_file_monitoring_enabled__(False)
1825
+ # Add a portion of the text so user can understand what is in each box
1826
+ text_sample = Truncate(workbox.__text__()).lines()
1827
+
1828
+ # Collect all the info for this workbox
1829
+ workboxDatum = dict(
1830
+ workbox_id=workbox_id,
1831
+ workbox_name=workbox_name,
1832
+ filename=filename,
1833
+ backup_file=backup_file,
1834
+ text_sample=text_sample,
1835
+ )
1836
+ workboxesData = [workboxDatum]
1837
+
1838
+ # We want to add the new action at the top.
1839
+ # Menu.insertAction behaves weirdly. It either replaces the 'before' action, or
1840
+ # doesn't retain any of the newly added actions, so instead we clear the
1841
+ # actions, and recreate the menu with Menu.addAction, limiting to the maxNum.
1842
+ existingActions = self.uiClosedWorkboxesMENU.actions()
1843
+ for existingAction in existingActions:
1844
+ existingDatum = existingAction.data()
1845
+ existingId = existingDatum.get("workbox_id")
1846
+ if existingId != workbox_id:
1847
+ workboxesData.append(existingDatum)
1848
+ self.uiClosedWorkboxesMENU.removeAction(existingAction)
1849
+
1850
+ # Limit list to self.max_recent_workboxes
1851
+ max_recent_workboxes = self.uiMaxNumRecentWorkboxesSPIN.value()
1852
+ closedWorkboxData = workboxesData[:max_recent_workboxes]
1853
+
1854
+ self.createClosedWorkboxMenuActions(closedWorkboxData)
1855
+
1856
+ def buildClosedWorkBoxMenu(self, closedWorkboxData=None):
1857
+ """When dialog launched, populate the Recently Closed Workbox list here.
1858
+ Normally, we add new names to top of list, but to start we add them in order.
1859
+
1860
+ Args:
1861
+ closedWorkboxData (list): The restored names of closed workboxes.
1862
+ """
1863
+ # Limit list to max_recent_workboxes
1864
+ if closedWorkboxData is None:
1865
+ closedWorkboxData = self.getClosedWorkboxData()
1866
+
1867
+ self.uiClosedWorkboxesMENU.clear()
1868
+
1869
+ max_recent_workboxes = self.uiMaxNumRecentWorkboxesSPIN.value()
1870
+ closedWorkboxData = closedWorkboxData[:max_recent_workboxes]
1871
+ self.createClosedWorkboxMenuActions(closedWorkboxData)
1872
+
1873
+ def createClosedWorkboxMenuActions(self, closedWorkboxData):
1874
+ """Create Recently Closed Workboxes actions and add the the recently
1875
+ closed workboxes menu.
1876
+
1877
+ Args:
1878
+ closedWorkboxData (list): A list of dictionary containing data for
1879
+ each recently closed workbox. Each dictionary is setup like this:
1880
+ workboxDatum = dict(
1881
+ workbox_id=workbox_id,
1882
+ workbox_name=workbox_name,
1883
+ filename=filename,
1884
+ text_sample=text_sample,
1885
+ )
1886
+ """
1887
+ for workboxDatum in closedWorkboxData:
1888
+ workbox_name = workboxDatum.get("workbox_name")
1889
+ filename = workboxDatum.get("filename")
1890
+ text_sample = workboxDatum.get("text_sample")
1891
+
1892
+ # Create a toolTip
1893
+ tip = ""
1894
+ if filename:
1895
+ tip += "filename: {}".format(filename)
1896
+ if text_sample:
1897
+ if tip:
1898
+ tip += "\n\n"
1899
+ tip += text_sample
1900
+
1901
+ action = self.uiClosedWorkboxesMENU.addAction(workbox_name)
1902
+ action.setData(workboxDatum)
1903
+ action.triggered.connect(self.recentWorkboxActionTriggered)
1904
+ action.setToolTip(tip)
1905
+
1906
+ def getClosedWorkboxData(self):
1907
+ """When saving prefs, collected all the Recently Closed Workbox names in the
1908
+ menu.
1909
+
1910
+ Return:
1911
+ names (list): The list of workboxes in the Recently Closed Workboxes list
1912
+ """
1913
+ data = []
1914
+ for act in self.uiClosedWorkboxesMENU.actions():
1915
+ datum = act.data()
1916
+ if datum:
1917
+ data.append(datum)
1918
+ return data
1919
+
1920
+ def recentWorkboxActionTriggered(self, checked=None, action=None):
1921
+ """Slot for when user selects a Recently Closed Workbox. First, try to just show
1922
+ the workbox if it's currently open. If not, recreate it. In both cases, set
1923
+ focus on that workbox.
1924
+
1925
+ Args:
1926
+ checked (bool, optional): If this is method is called as slot, the
1927
+ arg 'checked' is automatically passed
1928
+ action (QAction, optional): If this method is called by
1929
+ openMostRecentlyClosedWorkbox, this is the determined most recent
1930
+ workbox action.
1931
+
1932
+ """
1933
+ if action is None:
1934
+ action = self.sender()
1935
+
1936
+ workboxDatum = action.data()
1937
+ workbox_id = workboxDatum.get("workbox_id")
1938
+ workbox_filename = workboxDatum.get("filename")
1939
+ workbox_name = workboxDatum.pop("workbox_name")
1940
+ workboxDatum.pop("text_sample")
1941
+
1942
+ self.uiClosedWorkboxesMENU.removeAction(action)
1943
+
1944
+ workbox = self.workbox_for_id(workbox_id, visible=True)
1945
+ if workbox is None:
1946
+ groupName, workboxTitle = workbox_name.split("/")
1947
+ try:
1948
+ self.uiWorkboxTAB.hide()
1949
+ _, workbox = self.uiWorkboxTAB.add_new_tab(
1950
+ groupName, workboxTitle, prefs=workboxDatum
1951
+ )
1952
+ finally:
1953
+ self.uiWorkboxTAB.show()
1954
+
1955
+ if not workbox_filename:
1956
+ versionType = prefs.VersionTypes.Last
1957
+ filename, idx, count = workbox.__load_workbox_version_text__(
1958
+ versionType
1959
+ )
1960
+
1961
+ # Get rid of the hash part of the filename
1962
+ match = prefs.DATETIME_PATTERN.search(filename)
1963
+ if match:
1964
+ filename = match.group()
1965
+
1966
+ txt = "{} [{}/{}]".format(filename, idx, count)
1967
+ self.setStatusText(txt)
1968
+ self.autoHideStatusText()
1969
+ else:
1970
+ workbox.__load__(workbox_filename)
1971
+ workbox.__save_prefs__(saveLinkedFile=False)
1972
+
1973
+ workbox.__tab_widget__().tabBar().updateColorsAndToolTips()
1974
+
1975
+ if workbox is not None:
1976
+ workbox.__tab_widget__().tabBar().updateColorsAndToolTips()
1977
+
1978
+ def openMostRecentlyClosedWorkbox(self):
1979
+ """Restore the most recently closed workbox"""
1980
+ actions = self.uiClosedWorkboxesMENU.actions()
1981
+ if actions:
1982
+ action = actions[0]
1983
+ self.recentWorkboxActionTriggered(action=action)
1984
+
1985
+ def setAutoCompleteEnabled(self, state, console=True):
1986
+ if console:
1987
+ self.uiConsoleTXT.completer().setEnabled(state)
1988
+ else:
1989
+ for workbox, _, _, _, _ in self.uiWorkboxTAB.all_widgets():
1990
+ workbox.__set_auto_complete_enabled__(state)
1991
+
1992
+ def setSpellCheckEnabled(self, state):
1993
+ try:
1994
+ self.delayable_engine.set_delayable_enabled('spell_check', state)
1995
+ except KeyError:
1996
+ # Spell check can not be enabled
1997
+ if self.isVisible():
1998
+ # Only show warning if Logger is visible and also disable the action
1999
+ self.uiSpellCheckEnabledCHK.setDisabled(True)
2000
+ QMessageBox.warning(
2001
+ self, "Spell-Check", 'Unable to activate spell check.'
2002
+ )
2003
+
2004
+ def setStatusText(self, txt):
2005
+ """Set the text shown in the menu corner of the menu bar.
2006
+
2007
+ Args:
2008
+ txt (str): The text to show in the status text label.
2009
+ """
2010
+ self.uiStatusLBL.setText(txt)
2011
+ self.uiMenuBar.adjustSize()
2012
+
2013
+ def setupStatusTimer(self):
2014
+ # Create timer to autohide status messages
2015
+ self.statusTimer = QTimer()
2016
+ self.statusTimer.setSingleShot(True)
2017
+ self.statusTimer.setInterval(5000)
2018
+ self.statusTimer.timeout.connect(self.clearStatusText)
2019
+
2020
+ def clearStatusText(self):
2021
+ """Clear any displayed status text"""
2022
+ self.uiStatusLBL.clear()
2023
+ self.uiMenuBar.adjustSize()
2024
+
2025
+ def autoHideStatusText(self):
2026
+ """Set timer to automatically clear status text.
2027
+
2028
+ If timer is already running, it will be automatically stopped first (We can't
2029
+ use static method QTimer.singleShot for this)
2030
+ """
2031
+ self.statusTimer.start()
2032
+
2033
+ def setStyleSheet(self, stylesheet, recordPrefs=True):
2034
+ """Accepts the name of a stylesheet included with blurdev, or a full
2035
+ path to any stylesheet. If given None, it will default to Bright.
2036
+ """
2037
+ sheet = None
2038
+ if stylesheet is None:
2039
+ stylesheet = 'Bright'
2040
+ if os.path.isfile(stylesheet):
2041
+ # A path to a stylesheet was passed in
2042
+ with open(stylesheet) as f:
2043
+ sheet = f.read()
2044
+ self._stylesheet = stylesheet
2045
+ else:
2046
+ # Try to find an installed stylesheet with the given name
2047
+ sheet, valid = stylesheets.read_stylesheet(stylesheet)
2048
+ if valid:
2049
+ self._stylesheet = stylesheet
2050
+ else:
2051
+ # Assume the user passed the text of the stylesheet directly
2052
+ sheet = stylesheet
2053
+ self._stylesheet = 'Custom'
2054
+
2055
+ # Load the stylesheet
2056
+ if sheet is not None:
2057
+ super(LoggerWindow, self).setStyleSheet(sheet)
2058
+
2059
+ # Update the style menu
2060
+ for act in self.uiStyleMENU.actions():
2061
+ name = act.objectName()
2062
+ isCurrent = name == 'ui{}ACT'.format(self._stylesheet)
2063
+ act.setChecked(isCurrent)
2064
+
2065
+ # Notify widgets that the styleSheet has changed
2066
+ self.styleSheetChanged.emit(stylesheet)
2067
+ self.updateTabColorsAndToolTips()
2068
+
2069
+ def setCaseSensitive(self, state):
2070
+ """Set completer case-sensivity"""
2071
+ completer = self.console().completer()
2072
+ completer.setCaseSensitive(state)
2073
+ self.uiAutoCompleteCaseSensitiveACT.setChecked(state)
2074
+ self.reportCaseChange(state)
2075
+ completer.refreshList()
2076
+
2077
+ def toggleCaseSensitive(self):
2078
+ """Toggle completer case-sensitivity"""
2079
+ state = self.console().completer().caseSensitive()
2080
+ self.reportCaseChange(state)
2081
+ self.setCaseSensitive(not state)
2082
+
2083
+ # Completer Modes
2084
+ def cycleCompleterMode(self):
2085
+ """Cycle comleter mode"""
2086
+ completerMode = next(self.completerModeCycle)
2087
+ self.setCompleterMode(completerMode)
2088
+ self.reportCompleterModeChange(completerMode)
2089
+
2090
+ def cycleToCompleterMode(self, completerMode):
2091
+ """
2092
+ Syncs the completerModeCycle iterator to currently chosen completerMode
2093
+ Args:
2094
+ completerMode: Chosen CompleterMode ENUM member
2095
+ """
2096
+ for _ in range(len(CompleterMode)):
2097
+ tempMode = next(self.completerModeCycle)
2098
+ if tempMode == completerMode:
2099
+ break
2100
+
2101
+ def setCompleterMode(self, completerMode):
2102
+ """
2103
+ Set the completer mode to chosen mode
2104
+ Args:
2105
+ completerMode: Chosen CompleterMode ENUM member
2106
+ """
2107
+ completer = self.console().completer()
2108
+
2109
+ completer.setCompleterMode(completerMode)
2110
+ completer.buildCompleter()
2111
+
2112
+ for action in self.uiCompleterModeMENU.actions():
2113
+ action.setChecked(action.data() == completerMode)
2114
+
2115
+ def selectCompleterMode(self, action):
2116
+ if not action.isChecked():
2117
+ action.setChecked(True)
2118
+ return
2119
+ """
2120
+ Handle when completer mode is chosen via menu
2121
+ Will sync mode iterator and set the completion mode
2122
+ Args:
2123
+ action: the menu action associated with the chosen mode
2124
+ """
2125
+
2126
+ # update cycleToCompleterMode to current Mode
2127
+ mode = action.data()
2128
+ self.cycleToCompleterMode(mode)
2129
+ self.setCompleterMode(mode)
2130
+
2131
+ def reportCaseChange(self, state):
2132
+ """Update status text with current Case Sensitivity Mode"""
2133
+ text = "Case Sensitive " if state else "Case Insensitive "
2134
+ self.setStatusText(text)
2135
+ self.autoHideStatusText()
2136
+
2137
+ def reportCompleterModeChange(self, mode):
2138
+ """Update status text with current Completer Mode"""
2139
+ self.setStatusText('Completer Mode: {} '.format(mode.displayName()))
2140
+ self.autoHideStatusText()
2141
+
2142
+ def setClearBeforeRunning(self, state):
2143
+ self.uiRunSelectedACT.setIcon(QIcon(resourcePath('img/playlist-play.png')))
2144
+ self.uiRunAllACT.setIcon(QIcon(resourcePath('img/play.png')))
2145
+
2146
+ def setFlashWindowInterval(self):
2147
+ value = self.uiConsoleTXT.flash_time
2148
+ msg = (
2149
+ 'If running code in the logger takes X seconds or longer,\n'
2150
+ 'the window will flash if it is not in focus.\n'
2151
+ 'Setting the value to zero will disable flashing.'
2152
+ )
2153
+ value, success = QInputDialog.getDouble(self, 'Set flash window', msg, value)
2154
+ if success:
2155
+ self.uiConsoleTXT.flash_time = value
2156
+
2157
+ def setWordWrap(self, state):
2158
+ if state:
2159
+ self.uiConsoleTXT.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)
2160
+ else:
2161
+ self.uiConsoleTXT.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap)
2162
+
2163
+ def show_about(self):
2164
+ """Shows `preditor.about_preditor()`'s output in a message box."""
2165
+ msg = about_preditor(instance=self)
2166
+ QMessageBox.information(self, 'About PrEditor', '<pre>{}</pre>'.format(msg))
2167
+
2168
+ def showEnvironmentVars(self):
2169
+ dlg = Dialog(self)
2170
+ lyt = QVBoxLayout(dlg)
2171
+ lbl = QTextBrowser(dlg)
2172
+ lyt.addWidget(lbl)
2173
+ dlg.setWindowTitle('Blurdev Environment Variable Help')
2174
+ with open(resourcePath('environment_variables.html')) as f:
2175
+ lbl.setText(f.read().replace('\n', ''))
2176
+ dlg.setMinimumSize(600, 400)
2177
+ dlg.show()
2178
+
2179
+ def showEvent(self, event):
2180
+ super(LoggerWindow, self).showEvent(event)
2181
+ self.updateIndentationsUseTabs()
2182
+ self.updateCopyIndentsAsSpaces()
2183
+
2184
+ # Adjust the minimum height of the label so it's text is the same as
2185
+ # the action menu text
2186
+ height = self.uiMenuBar.actionGeometry(self.uiFileMENU.menuAction()).height()
2187
+ self.uiStatusLBL.setMinimumHeight(height)
2188
+
2189
+ @Slot()
2190
+ def show_workbox_options(self):
2191
+ self.uiWorkboxSTACK.setCurrentIndex(WorkboxPages.Options)
2192
+
2193
+ @Slot()
2194
+ def show_preferences(self):
2195
+ self.uiWorkboxSTACK.setCurrentIndex(WorkboxPages.Preferences)
2196
+
2197
+ @Slot()
2198
+ def show_find_in_workboxes(self):
2199
+ """Ensure the find workboxes widget is visible and has focus."""
2200
+ self.uiFindInWorkboxesWGT.activate()
2201
+
2202
+ @Slot()
2203
+ def show_focus_name(self):
2204
+ model = GroupTabListItemModel(manager=self.uiWorkboxTAB)
2205
+ model.process()
2206
+
2207
+ def update_tab(index):
2208
+ group, tab = model.workbox_indexes_from_model_index(index)
2209
+ if group is not None:
2210
+ self.uiWorkboxTAB.set_current_groups_from_index(group, tab)
2211
+
2212
+ w = FuzzySearch(model, parent=self)
2213
+ w.selected.connect(update_tab)
2214
+ w.canceled.connect(update_tab)
2215
+ w.highlighted.connect(update_tab)
2216
+ w.popup()
2217
+
2218
+ def updateCopyIndentsAsSpaces(self):
2219
+ for workbox, _, _, _, _ in self.uiWorkboxTAB.all_widgets():
2220
+ workbox.__set_copy_indents_as_spaces__(
2221
+ self.uiCopyTabsToSpacesCHK.isChecked()
2222
+ )
2223
+
2224
+ def updateIndentationsUseTabs(self):
2225
+ for workbox, _, _, _, _ in self.uiWorkboxTAB.all_widgets():
2226
+ workbox.__set_indentations_use_tabs__(
2227
+ self.uiIndentationsTabsCHK.isChecked()
2228
+ )
2229
+
2230
+ @Slot()
2231
+ def updateRepaintDelay(self):
2232
+ """Update write repaint delay for change to uiRepaintConsolesPerSecondSPIN.
2233
+
2234
+ `repaintConsolesDelay` is stored as an int nanosecond value so we can use
2235
+ `time.time_ns()` without converting to floats which adds a small but
2236
+ cumulative time to each write call. Pre-converting this helps limit the
2237
+ total delay time.
2238
+ """
2239
+ secs = self.uiRepaintConsolesPerSecondSPIN.value()
2240
+ self.repaintConsolesDelay = round(round(secs * 1e9))
2241
+
2242
+ @Slot()
2243
+ def update_workbox_stack(self):
2244
+ if self.uiWorkboxTAB.editor_cls:
2245
+ index = WorkboxPages.Workboxes
2246
+ else:
2247
+ index = WorkboxPages.Options
2248
+
2249
+ self.uiWorkboxSTACK.setCurrentIndex(index)
2250
+
2251
+ @Slot()
2252
+ def update_window_settings(self):
2253
+ self.buildClosedWorkBoxMenu()
2254
+
2255
+ def shutdown(self):
2256
+ # close out of the ide system
2257
+
2258
+ # if this is the global instance, then allow it to be deleted on close
2259
+ if self == LoggerWindow._instance:
2260
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
2261
+ LoggerWindow._instance = None
2262
+
2263
+ # clear out the system
2264
+ self.close()
2265
+
2266
+ def nextTab(self):
2267
+ """Move focus to next workbox tab"""
2268
+ tabWidget = self.uiWorkboxTAB.currentWidget()
2269
+ if not tabWidget.currentWidget().hasFocus():
2270
+ tabWidget.currentWidget().setFocus()
2271
+
2272
+ index = tabWidget.currentIndex()
2273
+ if index == tabWidget.count() - 1:
2274
+ tabWidget.setCurrentIndex(0)
2275
+ else:
2276
+ tabWidget.setCurrentIndex(index + 1)
2277
+
2278
+ def prevTab(self):
2279
+ """Move focus to previous workbox tab"""
2280
+ tabWidget = self.uiWorkboxTAB.currentWidget()
2281
+ if not tabWidget.currentWidget().hasFocus():
2282
+ tabWidget.currentWidget().setFocus()
2283
+
2284
+ index = tabWidget.currentIndex()
2285
+ if index == 0:
2286
+ tabWidget.setCurrentIndex(tabWidget.count() - 1)
2287
+ else:
2288
+ tabWidget.setCurrentIndex(index - 1)
2289
+
2290
+ def gotoGroupByIndex(self, index):
2291
+ """Generally to be used in conjunction with the Ctrl+Alt+<num> keyboard
2292
+ shortcuts, which allow user to jump directly to another tab, mimicking
2293
+ web browser functionality.
2294
+ """
2295
+ if index == -1:
2296
+ index = self.uiWorkboxTAB.count() - 1
2297
+ else:
2298
+ count = self.uiWorkboxTAB.count()
2299
+ index = min(index, count)
2300
+ index -= 1
2301
+
2302
+ self.uiWorkboxTAB.setCurrentIndex(index)
2303
+
2304
+ def gotoTabByIndex(self, index):
2305
+ """Generally to be used in conjunction with the Ctrl+<num> keyboard
2306
+ shortcuts, which allow user to jump directly to another tab, mimicking
2307
+ web browser functionality.
2308
+ """
2309
+ group_tab = self.uiWorkboxTAB.currentWidget()
2310
+ if index == -1:
2311
+ index = group_tab.count() - 1
2312
+ else:
2313
+ count = group_tab.count()
2314
+ index = min(index, count)
2315
+ index -= 1
2316
+
2317
+ group_tab.setCurrentIndex(index)
2318
+
2319
+ @staticmethod
2320
+ def instance(
2321
+ parent=None, name=None, run_workbox=False, create=True, standalone=False
2322
+ ):
2323
+ """Returns the existing instance of the PrEditor gui creating it on first call.
2324
+
2325
+ Args:
2326
+ parent (QWidget, optional): If the instance hasn't been created yet, create
2327
+ it and parent it to this object.
2328
+ run_workbox (bool, optional): If the instance hasn't been created yet, this
2329
+ will execute the active workbox's code once fully initialized.
2330
+ create (bool, optional): Returns None if the instance has not been created.
2331
+ standalone (bool, optional): Launch PrEditor in standalone mode. This
2332
+ enables extra options that only make sense when it is running as
2333
+ its own app, not inside of another app.
2334
+
2335
+ Returns:
2336
+ Returns a fully initialized instance of the PrEditor gui. If called more
2337
+ than once, the same instance will be returned. If create is False, it may
2338
+ return None.
2339
+ """
2340
+ # create the instance for the logger
2341
+ if not LoggerWindow._instance:
2342
+ if not create:
2343
+ return None
2344
+
2345
+ # create the logger instance
2346
+ inst = LoggerWindow(
2347
+ parent, name=name, run_workbox=run_workbox, standalone=standalone
2348
+ )
2349
+
2350
+ # protect the memory
2351
+ inst.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, False)
2352
+
2353
+ # cache the instance
2354
+ LoggerWindow._instance = inst
2355
+
2356
+ # Allow customization when the instance is first created.
2357
+ if config.on_create_callback:
2358
+ config.on_create_callback(inst)
2359
+
2360
+ return LoggerWindow._instance
2361
+
2362
+ def installLogToFile(self):
2363
+ """All stdout/stderr output is also appended to this file.
2364
+
2365
+ This uses preditor.debug.logToFile(path, useOldStd=True).
2366
+ """
2367
+ if self._logToFilePath is None:
2368
+ path = osystem.defaultLogFile()
2369
+ path, _ = QtCompat.QFileDialog.getSaveFileName(
2370
+ self, "Log Output to File", path
2371
+ )
2372
+ if not path:
2373
+ return
2374
+ path = os.path.normpath(path)
2375
+ print('Output logged to: "{}"'.format(path))
2376
+ debug.logToFile(path, useOldStd=True)
2377
+ # Store the std's so we can clear them later
2378
+ self._stds = (sys.stdout, sys.stderr)
2379
+ self.uiLogToFileACT.setText('Output Logged to File')
2380
+ self.uiLogToFileClearACT.setVisible(True)
2381
+ self._logToFilePath = path
2382
+ else:
2383
+ print('Output logged to: "{}"'.format(self._logToFilePath))
2384
+
2385
+ @classmethod
2386
+ def instance_shutdown(cls):
2387
+ """Call shutdown the LoggerWindow instance only if it was instantiated.
2388
+
2389
+ Returns:
2390
+ bool: If a shutdown was required
2391
+ """
2392
+ if cls._instance:
2393
+ try:
2394
+ cls._instance.shutdown()
2395
+ except RuntimeError as error:
2396
+ # If called after the host Qt application has been closed then
2397
+ # the instance has been deleted and we can't save preferences
2398
+ # without getting a RuntimeError about C/C++ being deleted.
2399
+ logger.warning(
2400
+ f"instance_shutdown failed PrEditor prefs likely not saved: {error}"
2401
+ )
2402
+ return False
2403
+
2404
+ return True
2405
+ return False