PrEditor 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. preditor/__init__.py +315 -0
  2. preditor/__main__.py +13 -0
  3. preditor/about_module.py +165 -0
  4. preditor/cli.py +192 -0
  5. preditor/config.py +318 -0
  6. preditor/constants.py +13 -0
  7. preditor/contexts.py +210 -0
  8. preditor/cores/__init__.py +0 -0
  9. preditor/cores/core.py +20 -0
  10. preditor/dccs/.hab.json +10 -0
  11. preditor/dccs/maya/PrEditor_maya.mod +1 -0
  12. preditor/dccs/maya/README.md +22 -0
  13. preditor/dccs/maya/plug-ins/PrEditor_maya.py +141 -0
  14. preditor/dccs/studiomax/PackageContents.xml +32 -0
  15. preditor/dccs/studiomax/PrEditor-PrEditor_Show.mcr +8 -0
  16. preditor/dccs/studiomax/README.md +17 -0
  17. preditor/dccs/studiomax/preditor.ms +16 -0
  18. preditor/dccs/studiomax/preditor_menu.mnx +7 -0
  19. preditor/debug.py +149 -0
  20. preditor/delayable_engine/__init__.py +302 -0
  21. preditor/delayable_engine/delayables.py +85 -0
  22. preditor/enum.py +728 -0
  23. preditor/excepthooks.py +165 -0
  24. preditor/gui/__init__.py +56 -0
  25. preditor/gui/app.py +163 -0
  26. preditor/gui/codehighlighter.py +289 -0
  27. preditor/gui/completer.py +237 -0
  28. preditor/gui/console.py +605 -0
  29. preditor/gui/console_base.py +911 -0
  30. preditor/gui/dialog.py +181 -0
  31. preditor/gui/drag_tab_bar.py +625 -0
  32. preditor/gui/editor_chooser.py +57 -0
  33. preditor/gui/errordialog.py +69 -0
  34. preditor/gui/find_files.py +137 -0
  35. preditor/gui/fuzzy_search/__init__.py +0 -0
  36. preditor/gui/fuzzy_search/fuzzy_search.py +97 -0
  37. preditor/gui/group_tab_widget/__init__.py +0 -0
  38. preditor/gui/group_tab_widget/group_tab_widget.py +528 -0
  39. preditor/gui/group_tab_widget/grouped_tab_menu.py +35 -0
  40. preditor/gui/group_tab_widget/grouped_tab_models.py +107 -0
  41. preditor/gui/group_tab_widget/grouped_tab_widget.py +223 -0
  42. preditor/gui/group_tab_widget/one_tab_widget.py +96 -0
  43. preditor/gui/level_buttons.py +358 -0
  44. preditor/gui/logger_window_handler.py +77 -0
  45. preditor/gui/logger_window_plugin.py +35 -0
  46. preditor/gui/loggerwindow.py +2405 -0
  47. preditor/gui/newtabwidget.py +69 -0
  48. preditor/gui/output_console.py +11 -0
  49. preditor/gui/qtdesigner/__init__.py +21 -0
  50. preditor/gui/qtdesigner/_log_plugin.py +29 -0
  51. preditor/gui/qtdesigner/console_base_plugin.py +48 -0
  52. preditor/gui/qtdesigner/console_predit_plugin.py +48 -0
  53. preditor/gui/set_text_editor_path_dialog.py +61 -0
  54. preditor/gui/status_label.py +99 -0
  55. preditor/gui/suggest_path_quotes_dialog.py +50 -0
  56. preditor/gui/ui/editor_chooser.ui +93 -0
  57. preditor/gui/ui/errordialog.ui +74 -0
  58. preditor/gui/ui/find_files.ui +140 -0
  59. preditor/gui/ui/loggerwindow.ui +1909 -0
  60. preditor/gui/ui/set_text_editor_path_dialog.ui +189 -0
  61. preditor/gui/ui/suggest_path_quotes_dialog.ui +225 -0
  62. preditor/gui/window.py +161 -0
  63. preditor/gui/workbox_mixin.py +1139 -0
  64. preditor/gui/workbox_text_edit.py +136 -0
  65. preditor/gui/workboxwidget.py +315 -0
  66. preditor/logging_config.py +55 -0
  67. preditor/osystem.py +401 -0
  68. preditor/plugins.py +118 -0
  69. preditor/prefs.py +381 -0
  70. preditor/resource/environment_variables.html +26 -0
  71. preditor/resource/error_mail.html +85 -0
  72. preditor/resource/error_mail_inline.html +41 -0
  73. preditor/resource/img/README.md +17 -0
  74. preditor/resource/img/arrow_forward.png +0 -0
  75. preditor/resource/img/check-bold.png +0 -0
  76. preditor/resource/img/chevron-down.png +0 -0
  77. preditor/resource/img/chevron-up.png +0 -0
  78. preditor/resource/img/close-thick.png +0 -0
  79. preditor/resource/img/comment-edit.png +0 -0
  80. preditor/resource/img/content-copy.png +0 -0
  81. preditor/resource/img/content-cut.png +0 -0
  82. preditor/resource/img/content-duplicate.png +0 -0
  83. preditor/resource/img/content-paste.png +0 -0
  84. preditor/resource/img/content-save.png +0 -0
  85. preditor/resource/img/debug_disabled.png +0 -0
  86. preditor/resource/img/eye-check.png +0 -0
  87. preditor/resource/img/file-plus.png +0 -0
  88. preditor/resource/img/file-remove.png +0 -0
  89. preditor/resource/img/format-align-left.png +0 -0
  90. preditor/resource/img/format-letter-case-lower.png +0 -0
  91. preditor/resource/img/format-letter-case-upper.png +0 -0
  92. preditor/resource/img/format-letter-case.svg +1 -0
  93. preditor/resource/img/information.png +0 -0
  94. preditor/resource/img/logging_critical.png +0 -0
  95. preditor/resource/img/logging_custom.png +0 -0
  96. preditor/resource/img/logging_debug.png +0 -0
  97. preditor/resource/img/logging_error.png +0 -0
  98. preditor/resource/img/logging_info.png +0 -0
  99. preditor/resource/img/logging_not_set.png +0 -0
  100. preditor/resource/img/logging_warning.png +0 -0
  101. preditor/resource/img/marker.png +0 -0
  102. preditor/resource/img/play.png +0 -0
  103. preditor/resource/img/playlist-play.png +0 -0
  104. preditor/resource/img/plus-minus-variant.png +0 -0
  105. preditor/resource/img/preditor.ico +0 -0
  106. preditor/resource/img/preditor.png +0 -0
  107. preditor/resource/img/preditor.psd +0 -0
  108. preditor/resource/img/preditor.svg +44 -0
  109. preditor/resource/img/regex.svg +1 -0
  110. preditor/resource/img/restart.svg +1 -0
  111. preditor/resource/img/skip-forward-outline.png +0 -0
  112. preditor/resource/img/skip-next-outline.png +0 -0
  113. preditor/resource/img/skip-next.png +0 -0
  114. preditor/resource/img/skip-previous.png +0 -0
  115. preditor/resource/img/subdirectory-arrow-right.png +0 -0
  116. preditor/resource/img/text-search-variant.png +0 -0
  117. preditor/resource/img/warning-big.png +0 -0
  118. preditor/resource/lang/python.json +30 -0
  119. preditor/resource/pref_updates/pref_updates.json +17 -0
  120. preditor/resource/settings.ini +25 -0
  121. preditor/resource/stylesheet/Bright.css +76 -0
  122. preditor/resource/stylesheet/Dark.css +210 -0
  123. preditor/scintilla/__init__.py +40 -0
  124. preditor/scintilla/delayables/__init__.py +11 -0
  125. preditor/scintilla/delayables/smart_highlight.py +97 -0
  126. preditor/scintilla/delayables/spell_check.py +174 -0
  127. preditor/scintilla/documenteditor.py +1924 -0
  128. preditor/scintilla/finddialog.py +68 -0
  129. preditor/scintilla/lang/__init__.py +80 -0
  130. preditor/scintilla/lang/config/bash.ini +15 -0
  131. preditor/scintilla/lang/config/batch.ini +14 -0
  132. preditor/scintilla/lang/config/cpp.ini +19 -0
  133. preditor/scintilla/lang/config/css.ini +19 -0
  134. preditor/scintilla/lang/config/eyeonscript.ini +17 -0
  135. preditor/scintilla/lang/config/html.ini +21 -0
  136. preditor/scintilla/lang/config/javascript.ini +24 -0
  137. preditor/scintilla/lang/config/lua.ini +16 -0
  138. preditor/scintilla/lang/config/maxscript.ini +20 -0
  139. preditor/scintilla/lang/config/mel.ini +18 -0
  140. preditor/scintilla/lang/config/mu.ini +22 -0
  141. preditor/scintilla/lang/config/nsi.ini +19 -0
  142. preditor/scintilla/lang/config/perl.ini +19 -0
  143. preditor/scintilla/lang/config/puppet.ini +19 -0
  144. preditor/scintilla/lang/config/python.ini +28 -0
  145. preditor/scintilla/lang/config/ruby.ini +19 -0
  146. preditor/scintilla/lang/config/sql.ini +7 -0
  147. preditor/scintilla/lang/config/xml.ini +21 -0
  148. preditor/scintilla/lang/config/yaml.ini +18 -0
  149. preditor/scintilla/lang/language.py +240 -0
  150. preditor/scintilla/lexers/__init__.py +0 -0
  151. preditor/scintilla/lexers/cpplexer.py +22 -0
  152. preditor/scintilla/lexers/javascriptlexer.py +27 -0
  153. preditor/scintilla/lexers/maxscriptlexer.py +235 -0
  154. preditor/scintilla/lexers/mellexer.py +369 -0
  155. preditor/scintilla/lexers/mulexer.py +33 -0
  156. preditor/scintilla/lexers/pythonlexer.py +42 -0
  157. preditor/scintilla/ui/finddialog.ui +160 -0
  158. preditor/settings.py +71 -0
  159. preditor/stream/__init__.py +72 -0
  160. preditor/stream/console_handler.py +169 -0
  161. preditor/stream/director.py +144 -0
  162. preditor/stream/manager.py +97 -0
  163. preditor/streamhandler_helper.py +46 -0
  164. preditor/utils/__init__.py +191 -0
  165. preditor/utils/call_stack.py +86 -0
  166. preditor/utils/cute.py +106 -0
  167. preditor/utils/stylesheets.py +54 -0
  168. preditor/utils/text_search.py +338 -0
  169. preditor/version.py +34 -0
  170. preditor/weakref.py +363 -0
  171. preditor-2.1.0.dist-info/METADATA +308 -0
  172. preditor-2.1.0.dist-info/RECORD +179 -0
  173. preditor-2.1.0.dist-info/WHEEL +5 -0
  174. preditor-2.1.0.dist-info/entry_points.txt +19 -0
  175. preditor-2.1.0.dist-info/licenses/LICENSE +165 -0
  176. preditor-2.1.0.dist-info/top_level.txt +3 -0
  177. tests/encodings/test_ecoding.py +33 -0
  178. tests/find_files/test_find_files.py +74 -0
  179. tests/ide/test_delayable_engine.py +171 -0
@@ -0,0 +1,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