PrEditor 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- preditor/__init__.py +315 -0
- preditor/__main__.py +13 -0
- preditor/about_module.py +165 -0
- preditor/cli.py +192 -0
- preditor/config.py +318 -0
- preditor/constants.py +13 -0
- preditor/contexts.py +210 -0
- preditor/cores/__init__.py +0 -0
- preditor/cores/core.py +20 -0
- preditor/dccs/.hab.json +10 -0
- preditor/dccs/maya/PrEditor_maya.mod +1 -0
- preditor/dccs/maya/README.md +22 -0
- preditor/dccs/maya/plug-ins/PrEditor_maya.py +141 -0
- preditor/dccs/studiomax/PackageContents.xml +32 -0
- preditor/dccs/studiomax/PrEditor-PrEditor_Show.mcr +8 -0
- preditor/dccs/studiomax/README.md +17 -0
- preditor/dccs/studiomax/preditor.ms +16 -0
- preditor/dccs/studiomax/preditor_menu.mnx +7 -0
- preditor/debug.py +149 -0
- preditor/delayable_engine/__init__.py +302 -0
- preditor/delayable_engine/delayables.py +85 -0
- preditor/enum.py +728 -0
- preditor/excepthooks.py +165 -0
- preditor/gui/__init__.py +56 -0
- preditor/gui/app.py +163 -0
- preditor/gui/codehighlighter.py +289 -0
- preditor/gui/completer.py +237 -0
- preditor/gui/console.py +605 -0
- preditor/gui/console_base.py +911 -0
- preditor/gui/dialog.py +181 -0
- preditor/gui/drag_tab_bar.py +625 -0
- preditor/gui/editor_chooser.py +57 -0
- preditor/gui/errordialog.py +69 -0
- preditor/gui/find_files.py +137 -0
- preditor/gui/fuzzy_search/__init__.py +0 -0
- preditor/gui/fuzzy_search/fuzzy_search.py +97 -0
- preditor/gui/group_tab_widget/__init__.py +0 -0
- preditor/gui/group_tab_widget/group_tab_widget.py +528 -0
- preditor/gui/group_tab_widget/grouped_tab_menu.py +35 -0
- preditor/gui/group_tab_widget/grouped_tab_models.py +107 -0
- preditor/gui/group_tab_widget/grouped_tab_widget.py +223 -0
- preditor/gui/group_tab_widget/one_tab_widget.py +96 -0
- preditor/gui/level_buttons.py +358 -0
- preditor/gui/logger_window_handler.py +77 -0
- preditor/gui/logger_window_plugin.py +35 -0
- preditor/gui/loggerwindow.py +2405 -0
- preditor/gui/newtabwidget.py +69 -0
- preditor/gui/output_console.py +11 -0
- preditor/gui/qtdesigner/__init__.py +21 -0
- preditor/gui/qtdesigner/_log_plugin.py +29 -0
- preditor/gui/qtdesigner/console_base_plugin.py +48 -0
- preditor/gui/qtdesigner/console_predit_plugin.py +48 -0
- preditor/gui/set_text_editor_path_dialog.py +61 -0
- preditor/gui/status_label.py +99 -0
- preditor/gui/suggest_path_quotes_dialog.py +50 -0
- preditor/gui/ui/editor_chooser.ui +93 -0
- preditor/gui/ui/errordialog.ui +74 -0
- preditor/gui/ui/find_files.ui +140 -0
- preditor/gui/ui/loggerwindow.ui +1909 -0
- preditor/gui/ui/set_text_editor_path_dialog.ui +189 -0
- preditor/gui/ui/suggest_path_quotes_dialog.ui +225 -0
- preditor/gui/window.py +161 -0
- preditor/gui/workbox_mixin.py +1139 -0
- preditor/gui/workbox_text_edit.py +136 -0
- preditor/gui/workboxwidget.py +315 -0
- preditor/logging_config.py +55 -0
- preditor/osystem.py +401 -0
- preditor/plugins.py +118 -0
- preditor/prefs.py +381 -0
- preditor/resource/environment_variables.html +26 -0
- preditor/resource/error_mail.html +85 -0
- preditor/resource/error_mail_inline.html +41 -0
- preditor/resource/img/README.md +17 -0
- preditor/resource/img/arrow_forward.png +0 -0
- preditor/resource/img/check-bold.png +0 -0
- preditor/resource/img/chevron-down.png +0 -0
- preditor/resource/img/chevron-up.png +0 -0
- preditor/resource/img/close-thick.png +0 -0
- preditor/resource/img/comment-edit.png +0 -0
- preditor/resource/img/content-copy.png +0 -0
- preditor/resource/img/content-cut.png +0 -0
- preditor/resource/img/content-duplicate.png +0 -0
- preditor/resource/img/content-paste.png +0 -0
- preditor/resource/img/content-save.png +0 -0
- preditor/resource/img/debug_disabled.png +0 -0
- preditor/resource/img/eye-check.png +0 -0
- preditor/resource/img/file-plus.png +0 -0
- preditor/resource/img/file-remove.png +0 -0
- preditor/resource/img/format-align-left.png +0 -0
- preditor/resource/img/format-letter-case-lower.png +0 -0
- preditor/resource/img/format-letter-case-upper.png +0 -0
- preditor/resource/img/format-letter-case.svg +1 -0
- preditor/resource/img/information.png +0 -0
- preditor/resource/img/logging_critical.png +0 -0
- preditor/resource/img/logging_custom.png +0 -0
- preditor/resource/img/logging_debug.png +0 -0
- preditor/resource/img/logging_error.png +0 -0
- preditor/resource/img/logging_info.png +0 -0
- preditor/resource/img/logging_not_set.png +0 -0
- preditor/resource/img/logging_warning.png +0 -0
- preditor/resource/img/marker.png +0 -0
- preditor/resource/img/play.png +0 -0
- preditor/resource/img/playlist-play.png +0 -0
- preditor/resource/img/plus-minus-variant.png +0 -0
- preditor/resource/img/preditor.ico +0 -0
- preditor/resource/img/preditor.png +0 -0
- preditor/resource/img/preditor.psd +0 -0
- preditor/resource/img/preditor.svg +44 -0
- preditor/resource/img/regex.svg +1 -0
- preditor/resource/img/restart.svg +1 -0
- preditor/resource/img/skip-forward-outline.png +0 -0
- preditor/resource/img/skip-next-outline.png +0 -0
- preditor/resource/img/skip-next.png +0 -0
- preditor/resource/img/skip-previous.png +0 -0
- preditor/resource/img/subdirectory-arrow-right.png +0 -0
- preditor/resource/img/text-search-variant.png +0 -0
- preditor/resource/img/warning-big.png +0 -0
- preditor/resource/lang/python.json +30 -0
- preditor/resource/pref_updates/pref_updates.json +17 -0
- preditor/resource/settings.ini +25 -0
- preditor/resource/stylesheet/Bright.css +76 -0
- preditor/resource/stylesheet/Dark.css +210 -0
- preditor/scintilla/__init__.py +40 -0
- preditor/scintilla/delayables/__init__.py +11 -0
- preditor/scintilla/delayables/smart_highlight.py +97 -0
- preditor/scintilla/delayables/spell_check.py +174 -0
- preditor/scintilla/documenteditor.py +1924 -0
- preditor/scintilla/finddialog.py +68 -0
- preditor/scintilla/lang/__init__.py +80 -0
- preditor/scintilla/lang/config/bash.ini +15 -0
- preditor/scintilla/lang/config/batch.ini +14 -0
- preditor/scintilla/lang/config/cpp.ini +19 -0
- preditor/scintilla/lang/config/css.ini +19 -0
- preditor/scintilla/lang/config/eyeonscript.ini +17 -0
- preditor/scintilla/lang/config/html.ini +21 -0
- preditor/scintilla/lang/config/javascript.ini +24 -0
- preditor/scintilla/lang/config/lua.ini +16 -0
- preditor/scintilla/lang/config/maxscript.ini +20 -0
- preditor/scintilla/lang/config/mel.ini +18 -0
- preditor/scintilla/lang/config/mu.ini +22 -0
- preditor/scintilla/lang/config/nsi.ini +19 -0
- preditor/scintilla/lang/config/perl.ini +19 -0
- preditor/scintilla/lang/config/puppet.ini +19 -0
- preditor/scintilla/lang/config/python.ini +28 -0
- preditor/scintilla/lang/config/ruby.ini +19 -0
- preditor/scintilla/lang/config/sql.ini +7 -0
- preditor/scintilla/lang/config/xml.ini +21 -0
- preditor/scintilla/lang/config/yaml.ini +18 -0
- preditor/scintilla/lang/language.py +240 -0
- preditor/scintilla/lexers/__init__.py +0 -0
- preditor/scintilla/lexers/cpplexer.py +22 -0
- preditor/scintilla/lexers/javascriptlexer.py +27 -0
- preditor/scintilla/lexers/maxscriptlexer.py +235 -0
- preditor/scintilla/lexers/mellexer.py +369 -0
- preditor/scintilla/lexers/mulexer.py +33 -0
- preditor/scintilla/lexers/pythonlexer.py +42 -0
- preditor/scintilla/ui/finddialog.ui +160 -0
- preditor/settings.py +71 -0
- preditor/stream/__init__.py +72 -0
- preditor/stream/console_handler.py +169 -0
- preditor/stream/director.py +144 -0
- preditor/stream/manager.py +97 -0
- preditor/streamhandler_helper.py +46 -0
- preditor/utils/__init__.py +191 -0
- preditor/utils/call_stack.py +86 -0
- preditor/utils/cute.py +106 -0
- preditor/utils/stylesheets.py +54 -0
- preditor/utils/text_search.py +338 -0
- preditor/version.py +34 -0
- preditor/weakref.py +363 -0
- preditor-2.1.0.dist-info/METADATA +308 -0
- preditor-2.1.0.dist-info/RECORD +179 -0
- preditor-2.1.0.dist-info/WHEEL +5 -0
- preditor-2.1.0.dist-info/entry_points.txt +19 -0
- preditor-2.1.0.dist-info/licenses/LICENSE +165 -0
- preditor-2.1.0.dist-info/top_level.txt +3 -0
- tests/encodings/test_ecoding.py +33 -0
- tests/find_files/test_find_files.py +74 -0
- tests/ide/test_delayable_engine.py +171 -0
|
@@ -0,0 +1,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
|