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,1139 @@
|
|
|
1
|
+
from __future__ import absolute_import, print_function
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
import io
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
import textwrap
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import charset_normalizer
|
|
14
|
+
import Qt as Qt_py
|
|
15
|
+
from Qt.QtCore import Qt, Signal
|
|
16
|
+
from Qt.QtWidgets import QMessageBox, QStackedWidget
|
|
17
|
+
|
|
18
|
+
from ..prefs import (
|
|
19
|
+
VersionTypes,
|
|
20
|
+
create_stamped_path,
|
|
21
|
+
get_backup_version_info,
|
|
22
|
+
get_full_path,
|
|
23
|
+
get_prefs_dir,
|
|
24
|
+
get_relative_path,
|
|
25
|
+
)
|
|
26
|
+
from .group_tab_widget.one_tab_widget import OneTabWidget
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EolTypes(enum.Enum):
|
|
32
|
+
EolWindows = '\r\n'
|
|
33
|
+
EolUnix = '\n'
|
|
34
|
+
EolMac = '\r'
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class WorkboxName(str):
|
|
38
|
+
"""The joined name of a workbox `group/workbox` with access to its parts.
|
|
39
|
+
|
|
40
|
+
You may pass the group, workbox, or the fully formed workbox name:
|
|
41
|
+
examples:
|
|
42
|
+
workboxName = WorkboxName("Group01", "Workbox05")
|
|
43
|
+
workboxName = WorkboxName("Group01/Workbox05")
|
|
44
|
+
This subclass provides properties for the group and workbox values separately.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __new__(cls, name, sub_name=None):
|
|
48
|
+
if sub_name is not None:
|
|
49
|
+
txt = "/".join((name, sub_name))
|
|
50
|
+
else:
|
|
51
|
+
txt = name
|
|
52
|
+
try:
|
|
53
|
+
name, sub_name = txt.split("/")
|
|
54
|
+
except ValueError:
|
|
55
|
+
msg = "A fully formed name, or a group and name, must be passed in."
|
|
56
|
+
raise ValueError(msg) from None
|
|
57
|
+
|
|
58
|
+
ret = super().__new__(cls, txt)
|
|
59
|
+
# Preserve the imitable nature of str's by using properties without setters.
|
|
60
|
+
ret._group = name
|
|
61
|
+
ret._workbox = sub_name
|
|
62
|
+
return ret
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def group(self):
|
|
66
|
+
"""The tab name of the group tab that contains the workbox."""
|
|
67
|
+
return self._group
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def workbox(self):
|
|
71
|
+
"""The workbox of the tab for this workbox inside of the group."""
|
|
72
|
+
return self._workbox
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class WorkboxMixin(object):
|
|
76
|
+
_warning_text = None
|
|
77
|
+
"""When a user is picking this Workbox class, show a warning with this text."""
|
|
78
|
+
|
|
79
|
+
workboxSaved = Signal()
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
parent=None,
|
|
84
|
+
console=None,
|
|
85
|
+
workbox_id=None,
|
|
86
|
+
filename=None,
|
|
87
|
+
backup_file=None,
|
|
88
|
+
tempfile=None,
|
|
89
|
+
delayable_engine='default',
|
|
90
|
+
core_name=None,
|
|
91
|
+
**kwargs,
|
|
92
|
+
):
|
|
93
|
+
super(WorkboxMixin, self).__init__(parent=parent, **kwargs)
|
|
94
|
+
self._is_loaded = False
|
|
95
|
+
self._show_blank = False
|
|
96
|
+
self._tempdir = None
|
|
97
|
+
|
|
98
|
+
# As event-driven dialogs are shown, add the tuple of (title, message)
|
|
99
|
+
# to this list, to prevent multiple dialogs showing for same reason.
|
|
100
|
+
self.shownDialogs = []
|
|
101
|
+
|
|
102
|
+
self.core_name = core_name
|
|
103
|
+
|
|
104
|
+
if not workbox_id:
|
|
105
|
+
workbox_id = self.__create_workbox_id__(self.core_name)
|
|
106
|
+
self.__set_workbox_id__(workbox_id)
|
|
107
|
+
|
|
108
|
+
self.__set_filename__(filename)
|
|
109
|
+
self.__set_backup_file__(backup_file)
|
|
110
|
+
self.__set_tempfile__(tempfile)
|
|
111
|
+
|
|
112
|
+
self._tab_widget = parent
|
|
113
|
+
|
|
114
|
+
self.__set_last_saved_text__("")
|
|
115
|
+
# You would think we should also __set_last_workbox_name_ here, but we
|
|
116
|
+
# wait until __show__ so that we know the tab exists, and has tabText
|
|
117
|
+
self._last_workbox_name = None
|
|
118
|
+
|
|
119
|
+
self._promptOnLinkedChange = True
|
|
120
|
+
|
|
121
|
+
self.__set_orphaned_by_instance__(False)
|
|
122
|
+
self.__set_changed_by_instance__(False)
|
|
123
|
+
self._changed_saved = False
|
|
124
|
+
|
|
125
|
+
self.textChanged.connect(self._tab_widget.tabBar().updateColorsAndToolTips)
|
|
126
|
+
self.workboxSaved.connect(self._tab_widget.tabBar().updateColorsAndToolTips)
|
|
127
|
+
|
|
128
|
+
def __prompt_on_linked_change__(self):
|
|
129
|
+
"""Whether the option to prompt on linked file change is set
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
bool: Whether the option to prompt on linked file change is set
|
|
133
|
+
"""
|
|
134
|
+
window = self.window()
|
|
135
|
+
if window and hasattr(window, "promptOnLinkedChange"):
|
|
136
|
+
promptOnLinkedChange = window.promptOnLinkedChange()
|
|
137
|
+
else:
|
|
138
|
+
promptOnLinkedChange = self._promptOnLinkedChange
|
|
139
|
+
return promptOnLinkedChange
|
|
140
|
+
|
|
141
|
+
def __set_last_saved_text__(self, text):
|
|
142
|
+
"""Store text as last_saved_text on this workbox so checking if if_dirty
|
|
143
|
+
is quick.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
text (str): The text to define as last_saved_text
|
|
147
|
+
"""
|
|
148
|
+
self._last_saved_text = text
|
|
149
|
+
|
|
150
|
+
tab_widget = self.__tab_widget__()
|
|
151
|
+
if tab_widget is not None:
|
|
152
|
+
tab_widget.tabBar().update()
|
|
153
|
+
|
|
154
|
+
def __last_saved_text__(self):
|
|
155
|
+
"""Returns the last_saved_text on this workbox
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
last_saved_text (str): The _last_saved_text on this workbox
|
|
159
|
+
"""
|
|
160
|
+
return self._last_saved_text
|
|
161
|
+
|
|
162
|
+
def __set_last_workbox_name__(self, name=None):
|
|
163
|
+
"""Store text as last_workbox_name on this workbox so checking if
|
|
164
|
+
if_dirty is quick.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
name (str): The name to define as last_workbox_name
|
|
168
|
+
"""
|
|
169
|
+
if name is None:
|
|
170
|
+
name = self.__workbox_name__(workbox=self)
|
|
171
|
+
self._last_workbox_name = name
|
|
172
|
+
|
|
173
|
+
def __last_workbox_name__(self):
|
|
174
|
+
"""Returns the last_workbox_name on this workbox
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
last_workbox_name (str): The last_workbox_name on this workbox
|
|
178
|
+
"""
|
|
179
|
+
return self._last_workbox_name
|
|
180
|
+
|
|
181
|
+
def __tab_widget__(self):
|
|
182
|
+
"""Return the tab widget which contains this workbox
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
GroupedTabWidget: The tab widget which contains this workbox
|
|
186
|
+
"""
|
|
187
|
+
tab_widget = None
|
|
188
|
+
parent = self.parent()
|
|
189
|
+
while parent is not None:
|
|
190
|
+
if issubclass(parent.__class__, OneTabWidget):
|
|
191
|
+
tab_widget = parent
|
|
192
|
+
break
|
|
193
|
+
parent = parent.parent()
|
|
194
|
+
return tab_widget
|
|
195
|
+
|
|
196
|
+
def __set_tab_widget__(self, tab_widget):
|
|
197
|
+
"""Set this workbox's _tab_widget to the provided tab_widget"""
|
|
198
|
+
self._tab_widget = tab_widget
|
|
199
|
+
|
|
200
|
+
def __auto_complete_enabled__(self):
|
|
201
|
+
raise NotImplementedError("Mixin method not overridden.")
|
|
202
|
+
|
|
203
|
+
def __set_auto_complete_enabled__(self, state):
|
|
204
|
+
raise NotImplementedError("Mixin method not overridden.")
|
|
205
|
+
|
|
206
|
+
def __clear__(self):
|
|
207
|
+
raise NotImplementedError("Mixin method not overridden.")
|
|
208
|
+
|
|
209
|
+
def __close__(self):
|
|
210
|
+
"""Called just before the LoggerWindow is closed to allow for workbox cleanup"""
|
|
211
|
+
|
|
212
|
+
def __comment_toggle__(self):
|
|
213
|
+
raise NotImplementedError("Mixin method not overridden.")
|
|
214
|
+
|
|
215
|
+
def __console__(self):
|
|
216
|
+
"""Returns the PrEditor console to code is executed in if set."""
|
|
217
|
+
try:
|
|
218
|
+
return self._console
|
|
219
|
+
except AttributeError:
|
|
220
|
+
self._console = None
|
|
221
|
+
|
|
222
|
+
def __set_console__(self, console):
|
|
223
|
+
self._console = console
|
|
224
|
+
|
|
225
|
+
def __copy_indents_as_spaces__(self):
|
|
226
|
+
"""When copying code, should it convert leading tabs to spaces?"""
|
|
227
|
+
raise NotImplementedError("Mixin method not overridden.")
|
|
228
|
+
|
|
229
|
+
def __set_copy_indents_as_spaces__(self, state):
|
|
230
|
+
raise NotImplementedError("Mixin method not overridden.")
|
|
231
|
+
|
|
232
|
+
def __cursor_position__(self):
|
|
233
|
+
"""Returns the line and index of the cursor."""
|
|
234
|
+
raise NotImplementedError("Mixin method not overridden.")
|
|
235
|
+
|
|
236
|
+
def __set_cursor_position__(self, line, index):
|
|
237
|
+
"""Set the cursor to this line number and index"""
|
|
238
|
+
raise NotImplementedError("Mixin method not overridden.")
|
|
239
|
+
|
|
240
|
+
def __exec_all__(self):
|
|
241
|
+
txt = self.__unix_end_lines__(self.__text__()).rstrip()
|
|
242
|
+
title = self.__workbox_trace_title__()
|
|
243
|
+
self.__console__().executeString(txt, filename=title)
|
|
244
|
+
|
|
245
|
+
def __exec_selected__(self, truncate=True):
|
|
246
|
+
txt, lineNum = self.__selected_text__()
|
|
247
|
+
|
|
248
|
+
# Get rid of pesky \r's
|
|
249
|
+
txt = self.__unix_end_lines__(txt)
|
|
250
|
+
|
|
251
|
+
# Remove any leading white space shared across all lines
|
|
252
|
+
txt = textwrap.dedent(txt)
|
|
253
|
+
|
|
254
|
+
# Make workbox line numbers match the workbox line numbers, by adding
|
|
255
|
+
# the appropriate number of newlines to mimic it's original position in
|
|
256
|
+
# the workbox.
|
|
257
|
+
txt = '\n' * lineNum + txt
|
|
258
|
+
|
|
259
|
+
# execute the code and print the results to the console
|
|
260
|
+
title = self.__workbox_trace_title__(selection=True)
|
|
261
|
+
self.__console__().executeString(
|
|
262
|
+
txt, filename=title, echoResult=True, truncate=truncate
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def __file_monitoring_enabled__(self):
|
|
266
|
+
"""Returns True if this workbox supports file monitoring.
|
|
267
|
+
This allows the editor to update its text if the linked
|
|
268
|
+
file is changed on disk."""
|
|
269
|
+
return self.window().fileMonitoringEnabled(self.__filename__())
|
|
270
|
+
|
|
271
|
+
def __set_file_monitoring_enabled__(self, state):
|
|
272
|
+
"""Enables/Disables open file change monitoring. If enabled, A dialog will pop
|
|
273
|
+
up when ever the open file is changed externally. If file monitoring is
|
|
274
|
+
disabled in the IDE settings it will be ignored.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
bool:
|
|
278
|
+
"""
|
|
279
|
+
# if file monitoring is enabled and we have a file name then set up the file
|
|
280
|
+
# monitoring
|
|
281
|
+
self.window().setFileMonitoringEnabled(self.__filename__(), state)
|
|
282
|
+
|
|
283
|
+
def __filename__(self):
|
|
284
|
+
"""The workboxes filename (ie linked file), if any
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
str: The workboxes filename (ie linked file), if any
|
|
288
|
+
"""
|
|
289
|
+
return self._filename
|
|
290
|
+
|
|
291
|
+
def __set_filename__(self, filename):
|
|
292
|
+
"""Set this workboxes linked filename to the provided filename
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
filename (str): The filename to link to
|
|
296
|
+
"""
|
|
297
|
+
self._filename = filename
|
|
298
|
+
|
|
299
|
+
def __tempfile__(self):
|
|
300
|
+
"""The workboxes defined tempfile, if any.
|
|
301
|
+
This property is now obsolete, but retained to more easily facilitate if
|
|
302
|
+
a user needs to revert to PrEditor version before the workbox overhaul.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
str: The workboxes filename (ie linked file), if any
|
|
306
|
+
"""
|
|
307
|
+
return self._tempfile
|
|
308
|
+
|
|
309
|
+
def __set_tempfile__(self, filename):
|
|
310
|
+
"""Set this workboxes tempfile to the provided filename
|
|
311
|
+
|
|
312
|
+
This property is now obsolete, but retained to more easily facilitate if
|
|
313
|
+
a user needs to revert to PrEditor version before the workbox overhaul.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
filename (str): The filename to link to
|
|
317
|
+
"""
|
|
318
|
+
self._tempfile = filename
|
|
319
|
+
|
|
320
|
+
def __font__(self):
|
|
321
|
+
raise NotImplementedError("Mixin method not overridden.")
|
|
322
|
+
|
|
323
|
+
def __set_font__(self, font):
|
|
324
|
+
raise NotImplementedError("Mixin method not overridden.")
|
|
325
|
+
|
|
326
|
+
def __group_tab_index__(self):
|
|
327
|
+
"""Returns the group and editor indexes if this editor is being used in
|
|
328
|
+
a GroupTabWidget.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
group, editor: The index of the group tab and the index of the
|
|
332
|
+
editor's tab under the group tab. -1 is returned for both if
|
|
333
|
+
this isn't parent to a GroupTabWidget.
|
|
334
|
+
"""
|
|
335
|
+
group = editor = -1
|
|
336
|
+
|
|
337
|
+
# This widget's parent should be a stacked widget and we can get the
|
|
338
|
+
# editors index from that
|
|
339
|
+
stack = self.parent()
|
|
340
|
+
if stack and isinstance(stack, QStackedWidget):
|
|
341
|
+
editor = stack.indexOf(self)
|
|
342
|
+
else:
|
|
343
|
+
return -1, -1
|
|
344
|
+
|
|
345
|
+
# The parent of the stacked widget should be a tab widget, get its parent
|
|
346
|
+
editor_tab = stack.parent()
|
|
347
|
+
if not editor_tab:
|
|
348
|
+
return -1, -1
|
|
349
|
+
|
|
350
|
+
# This should be a stacked widget under a tab widget, we can get group
|
|
351
|
+
# from it without needing to get its parent.
|
|
352
|
+
stack = editor_tab.parent()
|
|
353
|
+
if stack and isinstance(stack, QStackedWidget):
|
|
354
|
+
group = stack.indexOf(editor_tab)
|
|
355
|
+
|
|
356
|
+
return group, editor
|
|
357
|
+
|
|
358
|
+
def __workbox_trace_title__(self, selection=False):
|
|
359
|
+
title = "WorkboxSelection" if selection else "Workbox"
|
|
360
|
+
group, editor = self.__group_tab_index__()
|
|
361
|
+
if group == -1 or editor == -1:
|
|
362
|
+
return '<{}>'.format(title)
|
|
363
|
+
else:
|
|
364
|
+
name = self.__workbox_name__()
|
|
365
|
+
return '<{}>:{}'.format(title, name)
|
|
366
|
+
|
|
367
|
+
def __workbox_name__(self, workbox=None):
|
|
368
|
+
"""Returns the name for this workbox or a given workbox.
|
|
369
|
+
The name is the group tab text and the workbox tab text joined by a `/`"""
|
|
370
|
+
workbox = workbox if workbox else self
|
|
371
|
+
workboxTAB = self.window().uiWorkboxTAB
|
|
372
|
+
group_name = None
|
|
373
|
+
workbox_name = None
|
|
374
|
+
|
|
375
|
+
grouped_tab_widget = workbox.__tab_widget__()
|
|
376
|
+
if grouped_tab_widget is None:
|
|
377
|
+
return WorkboxName("", "")
|
|
378
|
+
|
|
379
|
+
if workbox:
|
|
380
|
+
for group_idx in range(workboxTAB.count()):
|
|
381
|
+
# If a previous iteration determine workbox_name, bust out
|
|
382
|
+
if workbox_name:
|
|
383
|
+
break
|
|
384
|
+
# Check if current group is the workboxes parent group
|
|
385
|
+
cur_group_widget = workboxTAB.widget(group_idx)
|
|
386
|
+
if cur_group_widget == grouped_tab_widget:
|
|
387
|
+
group_name = workboxTAB.tabText(group_idx)
|
|
388
|
+
|
|
389
|
+
# Found the group, now find workbox
|
|
390
|
+
for workbox_idx in range(cur_group_widget.count()):
|
|
391
|
+
cur_workbox_widget = cur_group_widget.widget(workbox_idx)
|
|
392
|
+
if cur_workbox_widget == workbox:
|
|
393
|
+
workbox_name = cur_group_widget.tabText(workbox_idx)
|
|
394
|
+
break
|
|
395
|
+
else:
|
|
396
|
+
groupedTabBar = grouped_tab_widget.tabBar()
|
|
397
|
+
|
|
398
|
+
idx = -1
|
|
399
|
+
for idx in range(grouped_tab_widget.count()):
|
|
400
|
+
if grouped_tab_widget.widget(idx) == workbox:
|
|
401
|
+
break
|
|
402
|
+
workbox_name = groupedTabBar.tabText(idx)
|
|
403
|
+
|
|
404
|
+
group_tab_widget = grouped_tab_widget.tab_widget()
|
|
405
|
+
groupTabBar = group_tab_widget.tabBar()
|
|
406
|
+
idx = -1
|
|
407
|
+
for idx in range(group_tab_widget.count()):
|
|
408
|
+
if group_tab_widget.widget(idx) == grouped_tab_widget:
|
|
409
|
+
break
|
|
410
|
+
group_name = groupTabBar.tabText(idx)
|
|
411
|
+
|
|
412
|
+
# If both found, construct workbox name
|
|
413
|
+
if group_name and workbox_name:
|
|
414
|
+
name = WorkboxName(group_name, workbox_name)
|
|
415
|
+
else:
|
|
416
|
+
name = WorkboxName("", "")
|
|
417
|
+
return name
|
|
418
|
+
|
|
419
|
+
def __goto_line__(self, line):
|
|
420
|
+
raise NotImplementedError("Mixin method not overridden.")
|
|
421
|
+
|
|
422
|
+
def __indentations_use_tabs__(self):
|
|
423
|
+
raise NotImplementedError("Mixin method not overridden.")
|
|
424
|
+
|
|
425
|
+
def __set_indentations_use_tabs__(self, state):
|
|
426
|
+
raise NotImplementedError("Mixin method not overridden.")
|
|
427
|
+
|
|
428
|
+
def __insert_text__(self, txt):
|
|
429
|
+
raise NotImplementedError("Mixin method not overridden.")
|
|
430
|
+
|
|
431
|
+
def __load__(self, filename):
|
|
432
|
+
"""Load the given filename. If this method is overridden in a subclass,
|
|
433
|
+
to do extra functionality, make sure to also call this method, ie
|
|
434
|
+
super().__load__().
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
filename (str): The file to load
|
|
438
|
+
"""
|
|
439
|
+
if filename and Path(filename).is_file():
|
|
440
|
+
self._encoding, text = self.__open_file__(filename)
|
|
441
|
+
self.__set_text__(text)
|
|
442
|
+
self.__set_file_monitoring_enabled__(True)
|
|
443
|
+
self.__set_filename__(filename)
|
|
444
|
+
|
|
445
|
+
# Determine new workbox name so we can store it
|
|
446
|
+
cur_workbox_name = self.__workbox_name__()
|
|
447
|
+
group_name = cur_workbox_name.group
|
|
448
|
+
new_name = Path(filename).name
|
|
449
|
+
new_workbox_name = WorkboxName(group_name, new_name)
|
|
450
|
+
self.__set_last_workbox_name__(new_workbox_name)
|
|
451
|
+
|
|
452
|
+
else:
|
|
453
|
+
self.__set_filename__("")
|
|
454
|
+
|
|
455
|
+
self.__set_last_saved_text__(self.__text__())
|
|
456
|
+
|
|
457
|
+
def __margins_font__(self):
|
|
458
|
+
raise NotImplementedError("Mixin method not overridden.")
|
|
459
|
+
|
|
460
|
+
def __lines__(self):
|
|
461
|
+
"""A list of all the lines of text contained in this workbox.
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
list: A list of all the lines of text contained in this workbox.
|
|
465
|
+
"""
|
|
466
|
+
txt = self.__text__()
|
|
467
|
+
eol = self.__detect_eol__(txt)
|
|
468
|
+
lines = txt.split(eol.value)
|
|
469
|
+
return lines
|
|
470
|
+
|
|
471
|
+
def __num_lines__(self):
|
|
472
|
+
"""The number of lines contained in this workbox.
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
int: The number of lines contained in this workbox.
|
|
476
|
+
"""
|
|
477
|
+
num_lines = len(self.__lines__())
|
|
478
|
+
return num_lines
|
|
479
|
+
|
|
480
|
+
def __detect_eol__(self, text):
|
|
481
|
+
"""Determine the eol (end-of-line) type for this file, such as Windows,
|
|
482
|
+
Linux or Mac.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
text (str): The text for which to determine eol characters.
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
EolTypes: The determined eol type.
|
|
489
|
+
"""
|
|
490
|
+
newlineN = text.find('\n')
|
|
491
|
+
newlineR = text.find('\r')
|
|
492
|
+
if newlineN != -1 and newlineR != -1:
|
|
493
|
+
if newlineN == newlineR + 1:
|
|
494
|
+
# CR LF Windows
|
|
495
|
+
return EolTypes.EolWindows
|
|
496
|
+
if newlineN != -1 and newlineR != -1:
|
|
497
|
+
if newlineN < newlineR:
|
|
498
|
+
# First return is a LF
|
|
499
|
+
return EolTypes.EolUnix
|
|
500
|
+
else:
|
|
501
|
+
# first return is a CR
|
|
502
|
+
return EolTypes.EolMac
|
|
503
|
+
if newlineN != -1:
|
|
504
|
+
return EolTypes.EolUnix
|
|
505
|
+
if sys.platform == 'win32':
|
|
506
|
+
return EolTypes.EolWindows
|
|
507
|
+
return EolTypes.EolUnix
|
|
508
|
+
|
|
509
|
+
def __set_margins_font__(self, font):
|
|
510
|
+
raise NotImplementedError("Mixin method not overridden.")
|
|
511
|
+
|
|
512
|
+
def __marker_add__(self, line):
|
|
513
|
+
raise NotImplementedError("Mixin method not overridden.")
|
|
514
|
+
|
|
515
|
+
def __marker_clear_all__(self):
|
|
516
|
+
raise NotImplementedError("Mixin method not overridden.")
|
|
517
|
+
|
|
518
|
+
def __set_workbox_title__(self, title):
|
|
519
|
+
"""Set the tab-text on the grouped widget tab for this workbox.
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
title (str): The text to put on the grouped tab's tabText.
|
|
523
|
+
"""
|
|
524
|
+
_group_idx, editor_idx = self.__group_tab_index__()
|
|
525
|
+
|
|
526
|
+
tab_widget = self.__tab_widget__()
|
|
527
|
+
if tab_widget is not None:
|
|
528
|
+
tab_widget.tabBar().setTabText(editor_idx, title)
|
|
529
|
+
|
|
530
|
+
def __maybe_reload_file__(self):
|
|
531
|
+
"""Reload this workbox's linked file."""
|
|
532
|
+
# Loading the file too quickly misses any changes
|
|
533
|
+
time.sleep(0.1)
|
|
534
|
+
font = self.__font__()
|
|
535
|
+
|
|
536
|
+
choice = self.__linked_file_changed__()
|
|
537
|
+
if choice is True:
|
|
538
|
+
# First save unsaved changes, so user can get it from a previous
|
|
539
|
+
# version is desired.
|
|
540
|
+
self.__save_prefs__(saveLinkedFile=False, resetLastInfos=False)
|
|
541
|
+
|
|
542
|
+
# Load the file
|
|
543
|
+
self.__load__(self.__filename__())
|
|
544
|
+
|
|
545
|
+
# Reset the font
|
|
546
|
+
self.__set_font__(font)
|
|
547
|
+
return choice
|
|
548
|
+
|
|
549
|
+
def __single_messagebox__(self, title, message):
|
|
550
|
+
"""Display a messagebox, but only once, in case this is triggered by a
|
|
551
|
+
signal which gets received multiple times.
|
|
552
|
+
|
|
553
|
+
Args:
|
|
554
|
+
title (str): The title for the messagebox
|
|
555
|
+
message (str): The descriptive text explaining the situation to the
|
|
556
|
+
user, which requires the messagebox.
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
choice (bool): Whether the user accepted the dialog or not.
|
|
560
|
+
"""
|
|
561
|
+
|
|
562
|
+
tup = (title, message)
|
|
563
|
+
if tup in self.shownDialogs:
|
|
564
|
+
return None
|
|
565
|
+
self.shownDialogs.append(tup)
|
|
566
|
+
|
|
567
|
+
buttons = QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
568
|
+
result = QMessageBox.question(self.window(), title, message, buttons)
|
|
569
|
+
self.shownDialogs.remove(tup)
|
|
570
|
+
|
|
571
|
+
return result == QMessageBox.StandardButton.Yes
|
|
572
|
+
|
|
573
|
+
def __linked_file_changed__(self):
|
|
574
|
+
"""If a file was modified or deleted this method
|
|
575
|
+
is called when Open File Monitoring is enabled. Returns True if the file
|
|
576
|
+
was updated or left open
|
|
577
|
+
|
|
578
|
+
Returns:
|
|
579
|
+
bool:
|
|
580
|
+
"""
|
|
581
|
+
filename = self.__filename__()
|
|
582
|
+
if not Path(filename).is_file():
|
|
583
|
+
# The file was deleted, ask the user if they still want to keep the file in
|
|
584
|
+
# the editor.
|
|
585
|
+
|
|
586
|
+
title = 'File Removed...'
|
|
587
|
+
msg = f'File: {filename} has been deleted or renamed.\nKeep file in editor?'
|
|
588
|
+
|
|
589
|
+
if not self.__prompt_on_linked_change__():
|
|
590
|
+
choice = True
|
|
591
|
+
else:
|
|
592
|
+
choice = self.__single_messagebox__(title, msg)
|
|
593
|
+
|
|
594
|
+
if choice is False:
|
|
595
|
+
logger.debug(
|
|
596
|
+
'The file was deleted, removing document from editor',
|
|
597
|
+
)
|
|
598
|
+
group_idx, editor_idx = self.__group_tab_index__()
|
|
599
|
+
|
|
600
|
+
self.__set_filename__("")
|
|
601
|
+
|
|
602
|
+
tab_widget = self.__tab_widget__()
|
|
603
|
+
if tab_widget is not None:
|
|
604
|
+
tab_widget.close_tab(editor_idx, ask=False)
|
|
605
|
+
return False
|
|
606
|
+
|
|
607
|
+
if (not self.__prompt_on_linked_change__()) or not self.__is_dirty__():
|
|
608
|
+
choice = True
|
|
609
|
+
else:
|
|
610
|
+
title = 'Reload File...'
|
|
611
|
+
workbox_name = self.__workbox_name__()
|
|
612
|
+
msg = (
|
|
613
|
+
f"The linked file in workbox\n\n{workbox_name}\n\nhas been changed "
|
|
614
|
+
"externally.\n\nReload from disk?"
|
|
615
|
+
)
|
|
616
|
+
choice = self.__single_messagebox__(title, msg)
|
|
617
|
+
|
|
618
|
+
return choice
|
|
619
|
+
|
|
620
|
+
def __remove_selected_text__(self):
|
|
621
|
+
raise NotImplementedError("Mixin method not overridden.")
|
|
622
|
+
|
|
623
|
+
def __save__(self):
|
|
624
|
+
"""Save this workbox's linked file.
|
|
625
|
+
|
|
626
|
+
Returns:
|
|
627
|
+
saved (bool): Whether the file was saved
|
|
628
|
+
"""
|
|
629
|
+
saved = self.__save_as__(self.__filename__())
|
|
630
|
+
if saved:
|
|
631
|
+
self.__set_last_saved_text__(self.__text__())
|
|
632
|
+
self.__set_last_workbox_name__(self.__workbox_name__())
|
|
633
|
+
return saved
|
|
634
|
+
|
|
635
|
+
def __save_as__(self, filename='', directory=''):
|
|
636
|
+
"""Save as provided filename, or self.__filename__(). If this method is
|
|
637
|
+
overridden to add functionality, make sure to still call this method.
|
|
638
|
+
|
|
639
|
+
Args:
|
|
640
|
+
filename (str, optional): The filename to save as
|
|
641
|
+
directory (str, optional): A directory to open the dialog at.
|
|
642
|
+
|
|
643
|
+
Returns:
|
|
644
|
+
saved (bool): Whether the file has been saved
|
|
645
|
+
"""
|
|
646
|
+
# Disable file watching so workbox doesn't reload and scroll to the top
|
|
647
|
+
self.__set_file_monitoring_enabled__(False)
|
|
648
|
+
if not filename:
|
|
649
|
+
filename = self.__filename__() or directory
|
|
650
|
+
filename, extFilter = Qt_py.QtCompat.QFileDialog.getSaveFileName(
|
|
651
|
+
self.window(), 'Save File as...', filename
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
if filename:
|
|
655
|
+
# Save the file to disk
|
|
656
|
+
try:
|
|
657
|
+
txt = self.__text__()
|
|
658
|
+
self.__write_file__(filename, txt, encoding=self._encoding)
|
|
659
|
+
self.__set_filename__(filename)
|
|
660
|
+
self.__set_last_workbox_name__(self.__workbox_name__())
|
|
661
|
+
self.__set_last_saved_text__(txt)
|
|
662
|
+
except PermissionError as error:
|
|
663
|
+
logger.debug('An error occurred while saving')
|
|
664
|
+
QMessageBox.question(
|
|
665
|
+
self.window(),
|
|
666
|
+
'Error saving file...',
|
|
667
|
+
'There was a error saving the file. Error: {}'.format(error),
|
|
668
|
+
QMessageBox.StandardButton.Ok,
|
|
669
|
+
)
|
|
670
|
+
return False
|
|
671
|
+
|
|
672
|
+
# Turn file watching back on.
|
|
673
|
+
self.__set_file_monitoring_enabled__(True)
|
|
674
|
+
return True
|
|
675
|
+
return False
|
|
676
|
+
|
|
677
|
+
def __selected_text__(self, start_of_line=False, selectText=False):
|
|
678
|
+
"""Returns selected text or the current line of text, plus the line
|
|
679
|
+
number of the begining of selection / cursor position.
|
|
680
|
+
|
|
681
|
+
If text is selected, it is returned. If nothing is selected, returns the
|
|
682
|
+
entire line of text the cursor is currently on.
|
|
683
|
+
|
|
684
|
+
Args:
|
|
685
|
+
start_of_line (bool, optional): If text is selected, include any
|
|
686
|
+
leading text from the first line of the selection.
|
|
687
|
+
selectText (bool): If expanding to the entire line from the cursor,
|
|
688
|
+
indicates whether to select that line of text
|
|
689
|
+
|
|
690
|
+
Returns:
|
|
691
|
+
str: The requested text
|
|
692
|
+
line (int): plus the line number of the beginning of selection / cursor
|
|
693
|
+
position.
|
|
694
|
+
"""
|
|
695
|
+
raise NotImplementedError("Mixin method not overridden.")
|
|
696
|
+
|
|
697
|
+
def __tab_width__(self):
|
|
698
|
+
raise NotImplementedError("Mixin method not overridden.")
|
|
699
|
+
|
|
700
|
+
def __set_tab_width__(self, width):
|
|
701
|
+
raise NotImplementedError("Mixin method not overridden.")
|
|
702
|
+
|
|
703
|
+
def __text__(self):
|
|
704
|
+
"""Returns the text in this widget
|
|
705
|
+
|
|
706
|
+
Returns:
|
|
707
|
+
str: Returns the text in this widget
|
|
708
|
+
"""
|
|
709
|
+
raise NotImplementedError("Mixin method not overridden.")
|
|
710
|
+
|
|
711
|
+
def __set_text__(self, txt):
|
|
712
|
+
"""Replace all of the current text with txt. This method can be overridden
|
|
713
|
+
by sub-classes to accommodate that widget's text-setting method. Most
|
|
714
|
+
likely should also set self._is_loaded=True.
|
|
715
|
+
"""
|
|
716
|
+
self.setText(txt)
|
|
717
|
+
self._is_loaded = True
|
|
718
|
+
|
|
719
|
+
def __text_part__(self, lineNum=None, start=None, end=None):
|
|
720
|
+
"""Returns the text in this widget, possibly limited in scope.
|
|
721
|
+
|
|
722
|
+
Note: Only pass line, or (start and end) to this method.
|
|
723
|
+
|
|
724
|
+
Args:
|
|
725
|
+
lineNum (int, optional): Limit the returned scope to just this line number.
|
|
726
|
+
start (int, optional): Limit the scope to text between this and end.
|
|
727
|
+
end (int, optional): Limit the scope to text between start and this.
|
|
728
|
+
|
|
729
|
+
Returns:
|
|
730
|
+
str: The requested text.
|
|
731
|
+
"""
|
|
732
|
+
if lineNum is not None:
|
|
733
|
+
return self.__lines__()[lineNum]
|
|
734
|
+
elif (start is None) != (end is None):
|
|
735
|
+
raise ValueError('You must pass start and end if you pass either.')
|
|
736
|
+
elif start is not None:
|
|
737
|
+
return self.__text__()[start:end]
|
|
738
|
+
return self.__text__()
|
|
739
|
+
|
|
740
|
+
def __is_dirty__(self):
|
|
741
|
+
"""Returns if this workbox has unsaved changes, either to it's contents
|
|
742
|
+
or it's name.
|
|
743
|
+
|
|
744
|
+
Returns:
|
|
745
|
+
is_dirty (bool): Whether or not this workbox has unsaved changes
|
|
746
|
+
"""
|
|
747
|
+
is_dirty = (
|
|
748
|
+
self.__text__() != self.__last_saved_text__()
|
|
749
|
+
or self.__workbox_name__(workbox=self) != self.__last_workbox_name__()
|
|
750
|
+
)
|
|
751
|
+
return is_dirty
|
|
752
|
+
|
|
753
|
+
def __is_missing_linked_file__(self):
|
|
754
|
+
"""Determine if this workbox is linked to a file which is missing on disk.
|
|
755
|
+
|
|
756
|
+
Returns:
|
|
757
|
+
bool: Whether this workbox is linked to a file which is missing on
|
|
758
|
+
disk.
|
|
759
|
+
"""
|
|
760
|
+
missing = False
|
|
761
|
+
filename = self.__filename__()
|
|
762
|
+
if filename:
|
|
763
|
+
missing = not Path(filename).is_file()
|
|
764
|
+
return missing
|
|
765
|
+
|
|
766
|
+
@classmethod
|
|
767
|
+
def __unix_end_lines__(cls, txt):
|
|
768
|
+
"""Replaces all windows and then mac line endings with unix line endings."""
|
|
769
|
+
return txt.replace('\r\n', '\n').replace('\r', '\n')
|
|
770
|
+
|
|
771
|
+
def __save_prefs__(
|
|
772
|
+
self,
|
|
773
|
+
current=None,
|
|
774
|
+
force=False,
|
|
775
|
+
saveLinkedFile=True,
|
|
776
|
+
resetLastInfos=True,
|
|
777
|
+
):
|
|
778
|
+
ret = {}
|
|
779
|
+
|
|
780
|
+
# Hopefully the alphabetical sorting of this dict is preserved in py3
|
|
781
|
+
# to make it easy to diff the json pref file if ever required.
|
|
782
|
+
|
|
783
|
+
workbox_id = self.__workbox_id__()
|
|
784
|
+
if current is not None:
|
|
785
|
+
ret['current'] = current
|
|
786
|
+
ret['filename'] = self.__filename__()
|
|
787
|
+
ret['name'] = self.__workbox_name__().workbox
|
|
788
|
+
ret['workbox_id'] = workbox_id
|
|
789
|
+
if self._tempfile:
|
|
790
|
+
ret['tempfile'] = self._tempfile
|
|
791
|
+
|
|
792
|
+
if self._backup_file:
|
|
793
|
+
ret['backup_file'] = get_relative_path(self.core_name, self._backup_file)
|
|
794
|
+
|
|
795
|
+
if not self._is_loaded:
|
|
796
|
+
return ret
|
|
797
|
+
|
|
798
|
+
fullpath = get_full_path(
|
|
799
|
+
self.core_name, workbox_id, backup_file=self._backup_file
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
time_str = None
|
|
803
|
+
if self._changed_by_instance:
|
|
804
|
+
time_str = self.window().latestTimeStrsForBoxesChangedViaInstance.get(
|
|
805
|
+
workbox_id, None
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
if self._changed_saved:
|
|
809
|
+
self.window().latestTimeStrsForBoxesChangedViaInstance.pop(workbox_id, None)
|
|
810
|
+
self._changed_saved = False
|
|
811
|
+
|
|
812
|
+
backup_exists = self._backup_file and Path(fullpath).is_file()
|
|
813
|
+
if self.__is_dirty__() or not backup_exists or force:
|
|
814
|
+
full_path = create_stamped_path(
|
|
815
|
+
self.core_name, workbox_id, time_str=time_str
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
full_path = str(full_path)
|
|
819
|
+
self.__write_file__(full_path, self.__text__(), encoding=self._encoding)
|
|
820
|
+
|
|
821
|
+
self._backup_file = get_relative_path(self.core_name, full_path)
|
|
822
|
+
ret['backup_file'] = self._backup_file
|
|
823
|
+
|
|
824
|
+
if time_str:
|
|
825
|
+
self._changed_saved = True
|
|
826
|
+
|
|
827
|
+
if time_str:
|
|
828
|
+
self.__set_changed_by_instance__(False)
|
|
829
|
+
if self.window().boxesOrphanedViaInstance.pop(workbox_id, None):
|
|
830
|
+
self.__set_orphaned_by_instance__(False)
|
|
831
|
+
|
|
832
|
+
# If workbox is linked to file on disk, save it
|
|
833
|
+
if self.__filename__() and saveLinkedFile:
|
|
834
|
+
self.__save__()
|
|
835
|
+
ret['workbox_id'] = workbox_id
|
|
836
|
+
|
|
837
|
+
if resetLastInfos:
|
|
838
|
+
self.__set_last_workbox_name__(self.__workbox_name__())
|
|
839
|
+
self.__set_last_saved_text__(self.__text__())
|
|
840
|
+
|
|
841
|
+
self.workboxSaved.emit()
|
|
842
|
+
|
|
843
|
+
return ret
|
|
844
|
+
|
|
845
|
+
@classmethod
|
|
846
|
+
def __create_workbox_id__(cls, core_name):
|
|
847
|
+
"""Creates a __workbox_id__ to store this editors text contents stored
|
|
848
|
+
in workbox_dir."""
|
|
849
|
+
with tempfile.NamedTemporaryFile(
|
|
850
|
+
prefix="workbox_",
|
|
851
|
+
dir=get_prefs_dir(core_name=core_name),
|
|
852
|
+
delete=True,
|
|
853
|
+
) as fle:
|
|
854
|
+
name = fle.name
|
|
855
|
+
|
|
856
|
+
return os.path.basename(name)
|
|
857
|
+
|
|
858
|
+
def __workbox_id__(self):
|
|
859
|
+
"""Returns this workbox's workbox_id
|
|
860
|
+
|
|
861
|
+
Returns:
|
|
862
|
+
workbox_id (str)
|
|
863
|
+
"""
|
|
864
|
+
return self._workbox_id
|
|
865
|
+
|
|
866
|
+
def __set_workbox_id__(self, workbox_id):
|
|
867
|
+
"""Set this workbox's workbox_id to the provided workbox_id
|
|
868
|
+
|
|
869
|
+
Args:
|
|
870
|
+
workbox_id (str): The workbox_id to set on this workbox
|
|
871
|
+
"""
|
|
872
|
+
self._workbox_id = workbox_id
|
|
873
|
+
|
|
874
|
+
def __backup_file__(self):
|
|
875
|
+
"""Returns this workbox's backup file
|
|
876
|
+
|
|
877
|
+
Returns:
|
|
878
|
+
_backup_file (str)
|
|
879
|
+
"""
|
|
880
|
+
return self._backup_file
|
|
881
|
+
|
|
882
|
+
def __set_backup_file__(self, filename):
|
|
883
|
+
"""Set this workbox's backup file to the provided filename
|
|
884
|
+
|
|
885
|
+
Args:
|
|
886
|
+
filename (str): The filename to set this workbox's backup_file to.
|
|
887
|
+
"""
|
|
888
|
+
self._backup_file = filename
|
|
889
|
+
|
|
890
|
+
def __set_changed_by_instance__(self, state):
|
|
891
|
+
"""Set whether this workbox has been determined to have been changed by
|
|
892
|
+
a secondary PrEditor instance (in the same core).
|
|
893
|
+
|
|
894
|
+
Args:
|
|
895
|
+
state (bool): Whether this workbox has been determined to have been
|
|
896
|
+
changed by a secondary PrEditor instance being saved.
|
|
897
|
+
"""
|
|
898
|
+
self._changed_by_instance = state
|
|
899
|
+
|
|
900
|
+
def __changed_by_instance__(self):
|
|
901
|
+
"""Returns whether this workbox has been determined to have been changed by
|
|
902
|
+
a secondary PrEditor instance (in the same core).
|
|
903
|
+
|
|
904
|
+
Returns:
|
|
905
|
+
changed_by_instance (bool): Whether this workbox has been determined
|
|
906
|
+
to have been changed by a secondary PrEditor instance being saved.
|
|
907
|
+
"""
|
|
908
|
+
return self._changed_by_instance
|
|
909
|
+
|
|
910
|
+
def __set_orphaned_by_instance__(self, state):
|
|
911
|
+
"""Set whether this workbox has been determined to have been orphaned by
|
|
912
|
+
a secondary PrEditor instance (in the same core).
|
|
913
|
+
|
|
914
|
+
Args:
|
|
915
|
+
state (bool): Whether this workbox has been determined to have been
|
|
916
|
+
orphaned by a secondary PrEditor instance being saved.
|
|
917
|
+
"""
|
|
918
|
+
self._orphaned_by_instance = state
|
|
919
|
+
|
|
920
|
+
def __orphaned_by_instance__(self):
|
|
921
|
+
"""Returns whether this workbox has been determined to have been orphaned by
|
|
922
|
+
a secondary PrEditor instance (in the same core).
|
|
923
|
+
|
|
924
|
+
Returns:
|
|
925
|
+
changed_by_instance (bool): Whether this workbox has been determined
|
|
926
|
+
to have been orphaned by a secondary PrEditor instance being saved.
|
|
927
|
+
"""
|
|
928
|
+
return self._orphaned_by_instance
|
|
929
|
+
|
|
930
|
+
def __determine_been_changed_by_instance__(self):
|
|
931
|
+
"""Determine whether this workbox has been changed by a secondary PrEditor
|
|
932
|
+
instance saving it's prefs. It sets the internal property
|
|
933
|
+
self._changed_by_instance to indicate the result.
|
|
934
|
+
"""
|
|
935
|
+
workbox_id = self.__workbox_id__()
|
|
936
|
+
if not workbox_id:
|
|
937
|
+
workbox_id = self.__create_workbox_id__(self.core_name)
|
|
938
|
+
self.__set_workbox_id__(workbox_id)
|
|
939
|
+
|
|
940
|
+
if workbox_id in self.window().latestTimeStrsForBoxesChangedViaInstance:
|
|
941
|
+
self.window().latestTimeStrsForBoxesChangedViaInstance.get(workbox_id)
|
|
942
|
+
self._changed_by_instance = True
|
|
943
|
+
else:
|
|
944
|
+
self._changed_by_instance = False
|
|
945
|
+
|
|
946
|
+
def __get_workbox_version_text__(self, filename, versionType):
|
|
947
|
+
"""Get the text of this workboxes previously saved versions. It's based
|
|
948
|
+
on versionType, which can be First, Previous, Next, SecondToLast, or Last
|
|
949
|
+
|
|
950
|
+
Args:
|
|
951
|
+
filename (str): Description
|
|
952
|
+
versionType (prefs.VersionTypes): Enum describing which version to
|
|
953
|
+
fetch
|
|
954
|
+
|
|
955
|
+
Returns:
|
|
956
|
+
txt, filepath, idx, count (str, str, int, int): The found files' text,
|
|
957
|
+
it's filepath, the index of this file in the stack of files, and
|
|
958
|
+
the total count of files for this workbox.
|
|
959
|
+
"""
|
|
960
|
+
backup_file = get_full_path(
|
|
961
|
+
self.core_name, self.__workbox_id__(), backup_file=self._backup_file
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
filepath, idx, count = get_backup_version_info(
|
|
965
|
+
self.core_name, filename, versionType, backup_file
|
|
966
|
+
)
|
|
967
|
+
txt = ""
|
|
968
|
+
if filepath and Path(filepath).is_file():
|
|
969
|
+
_encoding, txt = self.__open_file__(str(filepath))
|
|
970
|
+
|
|
971
|
+
return txt, filepath, idx, count
|
|
972
|
+
|
|
973
|
+
def __load_workbox_version_text__(self, versionType):
|
|
974
|
+
"""Get the text of this workboxes previously saved versions, and set it
|
|
975
|
+
in the workbox. It's based on versionType, which can be First, Previous,
|
|
976
|
+
Next, SecondToLast, or Last
|
|
977
|
+
|
|
978
|
+
Args:
|
|
979
|
+
versionType (prefs.VersionTypes): Enum describing which version to
|
|
980
|
+
fetch
|
|
981
|
+
|
|
982
|
+
Returns:
|
|
983
|
+
filename, idx, count (str, int, int): The found files' filepath, the
|
|
984
|
+
index of this file in the stack of files, and the total count of
|
|
985
|
+
files for this workbox.
|
|
986
|
+
"""
|
|
987
|
+
data = self.__get_workbox_version_text__(self.__workbox_id__(), versionType)
|
|
988
|
+
txt, filepath, idx, count = data
|
|
989
|
+
|
|
990
|
+
if filepath:
|
|
991
|
+
filepath = get_relative_path(self.core_name, filepath)
|
|
992
|
+
|
|
993
|
+
self._backup_file = str(filepath)
|
|
994
|
+
|
|
995
|
+
self.__set_text__(txt)
|
|
996
|
+
|
|
997
|
+
tab_widget = self.__tab_widget__()
|
|
998
|
+
if tab_widget is not None:
|
|
999
|
+
tab_widget.tabBar().update()
|
|
1000
|
+
|
|
1001
|
+
filename = Path(filepath).name
|
|
1002
|
+
return filename, idx, count
|
|
1003
|
+
|
|
1004
|
+
@classmethod
|
|
1005
|
+
def __open_file__(cls, filename, strict=True):
|
|
1006
|
+
"""Open a file and try to detect the text encoding it was saved as.
|
|
1007
|
+
|
|
1008
|
+
Returns:
|
|
1009
|
+
encoding(str): The detected encoding, Defaults to "utf-8" if unable
|
|
1010
|
+
to detect encoding.
|
|
1011
|
+
text(str): The contents of the file decoded to a str.
|
|
1012
|
+
"""
|
|
1013
|
+
with open(filename, "rb") as f:
|
|
1014
|
+
text_bytes = f.read()
|
|
1015
|
+
|
|
1016
|
+
try:
|
|
1017
|
+
# If possible to decode as utf-8 use it as the encoding
|
|
1018
|
+
text = text_bytes.decode("utf-8")
|
|
1019
|
+
return "utf-8", text
|
|
1020
|
+
except UnicodeDecodeError:
|
|
1021
|
+
pass
|
|
1022
|
+
|
|
1023
|
+
# Otherwise, attempt to detect source encoding and convert to utf-8
|
|
1024
|
+
encoding = charset_normalizer.detect(text_bytes)['encoding'] or 'utf-8'
|
|
1025
|
+
try:
|
|
1026
|
+
text = text_bytes.decode(encoding)
|
|
1027
|
+
except UnicodeDecodeError as e:
|
|
1028
|
+
if strict:
|
|
1029
|
+
raise UnicodeDecodeError( # noqa: B904
|
|
1030
|
+
e.encoding,
|
|
1031
|
+
e.object,
|
|
1032
|
+
e.start,
|
|
1033
|
+
e.end,
|
|
1034
|
+
f"{e.reason}, Filename: {filename}",
|
|
1035
|
+
)
|
|
1036
|
+
encoding = 'utf-8'
|
|
1037
|
+
text = text_bytes.decode(encoding, errors="ignore")
|
|
1038
|
+
return encoding, text
|
|
1039
|
+
|
|
1040
|
+
@classmethod
|
|
1041
|
+
def __write_file__(cls, filename, txt=None, encoding=None, toUnixEOL=True):
|
|
1042
|
+
"""Write the provided text to the provided filename
|
|
1043
|
+
|
|
1044
|
+
Args:
|
|
1045
|
+
filename (str): The filename to write to
|
|
1046
|
+
txt (str, optional): The text to write to file, or self__text__()
|
|
1047
|
+
encoding (str, optional): The name of the encoding to use
|
|
1048
|
+
toUnixEOL (bool, optional): Whether to force line endings to
|
|
1049
|
+
unix-style. Typically, we do this for regular workboxes, but
|
|
1050
|
+
not for linked files, so we aren't changing a file on disk's
|
|
1051
|
+
line-endings.
|
|
1052
|
+
"""
|
|
1053
|
+
if toUnixEOL:
|
|
1054
|
+
txt = cls.__unix_end_lines__(txt)
|
|
1055
|
+
with io.open(filename, 'w', newline='\n', encoding=encoding) as fle:
|
|
1056
|
+
fle.write(txt)
|
|
1057
|
+
|
|
1058
|
+
def __show__(self):
|
|
1059
|
+
if self._is_loaded:
|
|
1060
|
+
return
|
|
1061
|
+
|
|
1062
|
+
self._is_loaded = True
|
|
1063
|
+
count = None
|
|
1064
|
+
filename = self.__filename__()
|
|
1065
|
+
if filename and Path(filename).is_file():
|
|
1066
|
+
self.__load__(filename)
|
|
1067
|
+
return
|
|
1068
|
+
else:
|
|
1069
|
+
core_name = self.window().name
|
|
1070
|
+
versionType = VersionTypes.Last
|
|
1071
|
+
filepath, idx, count = get_backup_version_info(
|
|
1072
|
+
core_name, self.__workbox_id__(), versionType, ""
|
|
1073
|
+
)
|
|
1074
|
+
|
|
1075
|
+
if count:
|
|
1076
|
+
self.__load_workbox_version_text__(VersionTypes.Last)
|
|
1077
|
+
self.__set_last_saved_text__(self.__text__())
|
|
1078
|
+
|
|
1079
|
+
self.__set_last_workbox_name__()
|
|
1080
|
+
self.__tab_widget__().tabBar().updateColorsAndToolTips()
|
|
1081
|
+
|
|
1082
|
+
def process_shortcut(self, event, run=True):
|
|
1083
|
+
"""Check for workbox shortcuts and optionally call them.
|
|
1084
|
+
|
|
1085
|
+
Args:
|
|
1086
|
+
event (QEvent): The keyPressEvent to process.
|
|
1087
|
+
run (bool, optional): Run the expected action if possible.
|
|
1088
|
+
|
|
1089
|
+
Returns:
|
|
1090
|
+
str or False: Returns False if the key press was not handled, indicating
|
|
1091
|
+
that the subclass needs to handle it(or call super). If a known
|
|
1092
|
+
shortcut was detected, a string indicating the action is returned
|
|
1093
|
+
after running the action if enabled and supported.
|
|
1094
|
+
|
|
1095
|
+
Known actions:
|
|
1096
|
+
__exec_selected__: If the user pressed Shift + Return or pressed the
|
|
1097
|
+
number pad enter key calling `__exec_selected__`.
|
|
1098
|
+
"""
|
|
1099
|
+
|
|
1100
|
+
# Number pad enter, or Shift + Return pressed, execute selected
|
|
1101
|
+
# Ctrl+ Shift+Return pressed, execute selected without truncating output
|
|
1102
|
+
if run:
|
|
1103
|
+
# Collect what was pressed
|
|
1104
|
+
key = event.key()
|
|
1105
|
+
modifiers = event.modifiers()
|
|
1106
|
+
|
|
1107
|
+
# Determine which relevant combos are pressed
|
|
1108
|
+
ret = key == Qt.Key.Key_Return
|
|
1109
|
+
enter = key == Qt.Key.Key_Enter
|
|
1110
|
+
shift = modifiers == Qt.KeyboardModifier.ShiftModifier
|
|
1111
|
+
ctrlShift = (
|
|
1112
|
+
modifiers
|
|
1113
|
+
== Qt.KeyboardModifier.ControlModifier
|
|
1114
|
+
| Qt.KeyboardModifier.ShiftModifier
|
|
1115
|
+
)
|
|
1116
|
+
|
|
1117
|
+
# Determine which actions to take
|
|
1118
|
+
evalTrunc = enter or (ret and shift)
|
|
1119
|
+
evalNoTrunc = ret and ctrlShift
|
|
1120
|
+
|
|
1121
|
+
# See if shortcut for Open Most Recent Workbox is pressed
|
|
1122
|
+
openRecentWorkbox = ctrlShift and key == Qt.Key.Key_T
|
|
1123
|
+
|
|
1124
|
+
if evalTrunc:
|
|
1125
|
+
# Execute with truncation
|
|
1126
|
+
self.window().execSelected()
|
|
1127
|
+
elif evalNoTrunc:
|
|
1128
|
+
# Execute without truncation
|
|
1129
|
+
self.window().execSelected(truncate=False)
|
|
1130
|
+
|
|
1131
|
+
elif openRecentWorkbox:
|
|
1132
|
+
self.window().openMostRecentlyClosedWorkbox()
|
|
1133
|
+
|
|
1134
|
+
if evalTrunc or evalNoTrunc:
|
|
1135
|
+
if self.window().uiAutoPromptCHK.isChecked():
|
|
1136
|
+
self.__console__().startInputLine()
|
|
1137
|
+
return '__exec_selected__'
|
|
1138
|
+
else:
|
|
1139
|
+
return False
|