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,528 @@
1
+ from __future__ import absolute_import
2
+
3
+ from pathlib import Path
4
+
5
+ from Qt.QtCore import Qt
6
+ from Qt.QtWidgets import QHBoxLayout, QMessageBox, QSizePolicy, QToolButton, QWidget
7
+
8
+ from ...prefs import VersionTypes, get_backup_version_info
9
+ from ..drag_tab_bar import DragTabBar
10
+ from ..workbox_text_edit import WorkboxTextEdit
11
+ from .grouped_tab_menu import GroupTabMenu
12
+ from .grouped_tab_widget import GroupedTabWidget
13
+ from .one_tab_widget import OneTabWidget
14
+
15
+ DEFAULT_STYLE_SHEET = """
16
+ /* Make the two buttons in the GroupTabWidget take up the
17
+ same horizontal space as the GroupedTabWidget's buttons.
18
+ GroupTabWidget>QTabBar::tab{
19
+ max-height: 1.5em;
20
+ }*/
21
+ /* We have an icon, no need to show the menu indicator */
22
+ #group_tab_widget_menu_btn::menu-indicator{
23
+ width: 0px;
24
+ }
25
+ /* The GroupedTabWidget has a single button, make it take
26
+ the same space as the GroupTabWidget buttons. */
27
+ GroupedTabWidget>QToolButton,GroupTabWidget>QWidget{
28
+ width: 3em;
29
+ }
30
+ """
31
+
32
+
33
+ class GroupTabWidget(OneTabWidget):
34
+ """A QTabWidget where each tab contains another tab widget, allowing users
35
+ to group code editors. It has a corner button to add a new tab, and a menu
36
+ allowing users to quickly focus on any tab in the entire group.
37
+ """
38
+
39
+ def __init__(self, editor_kwargs=None, core_name=None, *args, **kwargs):
40
+ super(GroupTabWidget, self).__init__(*args, **kwargs)
41
+ DragTabBar.install_tab_widget(self, 'group_tab_widget')
42
+ self.editor_kwargs = editor_kwargs
43
+ self.editor_cls = WorkboxTextEdit
44
+ self.core_name = core_name
45
+ self.setStyleSheet(DEFAULT_STYLE_SHEET)
46
+
47
+ self.default_title = 'Group01'
48
+
49
+ corner = QWidget(self)
50
+ lyt = QHBoxLayout(corner)
51
+ lyt.setSpacing(0)
52
+ lyt.setContentsMargins(0, 5, 0, 0)
53
+
54
+ corner.uiNewTabBTN = QToolButton(corner)
55
+ corner.uiNewTabBTN.setObjectName('group_tab_widget_new_btn')
56
+ corner.uiNewTabBTN.setText('+')
57
+ corner.uiNewTabBTN.released.connect(lambda: self.add_new_tab(None))
58
+
59
+ lyt.addWidget(corner.uiNewTabBTN)
60
+
61
+ corner.uiMenuBTN = QToolButton(corner)
62
+ corner.uiMenuBTN.setText('\u2630')
63
+ corner.uiMenuBTN.setObjectName('group_tab_widget_menu_btn')
64
+ corner.uiMenuBTN.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
65
+ corner.uiCornerMENU = GroupTabMenu(self, parent=corner.uiMenuBTN)
66
+ corner.uiMenuBTN.setMenu(corner.uiCornerMENU)
67
+
68
+ self.adjustSizePolicy(corner)
69
+ self.adjustSizePolicy(corner.uiNewTabBTN)
70
+ self.adjustSizePolicy(corner.uiMenuBTN)
71
+ self.adjustSizePolicy(corner.uiCornerMENU)
72
+
73
+ lyt.addWidget(corner.uiMenuBTN)
74
+
75
+ self.uiCornerBTN = corner
76
+ self.setCornerWidget(self.uiCornerBTN, Qt.Corner.TopRightCorner)
77
+
78
+ def adjustSizePolicy(self, button):
79
+ sp = button.sizePolicy()
80
+ sp.setVerticalPolicy(QSizePolicy.Policy.Preferred)
81
+ button.setSizePolicy(sp)
82
+
83
+ def add_new_tab(self, group, title=None, prefs=None):
84
+ """Adds a new tab to the requested group, creating the group if the group
85
+ doesn't exist.
86
+
87
+ Args:
88
+ group: The group to add a new tab to. This can be an int index of an
89
+ existing tab, or the name of the group and it will create the group
90
+ if needed. If None is passed it will add a new tab `Group {last+1}`.
91
+ If True is passed, then the current group tab is used.
92
+ title (str, optional): The name to give the newly created tab inside
93
+ the group.
94
+
95
+ Returns:
96
+ GroupedTabWidget: The tab group for this group.
97
+ WorkboxMixin: The new text editor.
98
+ """
99
+ if not group:
100
+ group = self.get_next_available_tab_name()
101
+ elif group is True:
102
+ group = self.currentIndex()
103
+
104
+ parent = None
105
+ if isinstance(group, int):
106
+ group_title = self.tabText(group)
107
+ parent = self.widget(group)
108
+ elif isinstance(group, str):
109
+ group_title = group
110
+ index = self.index_for_text(group)
111
+ if index != -1:
112
+ parent = self.widget(index)
113
+
114
+ if not parent:
115
+ parent, group_title = self.default_tab(group_title, prefs)
116
+ self.addTab(parent, group_title)
117
+
118
+ # Create the first editor tab and make it visible
119
+ editor = parent.add_new_editor(title, prefs)
120
+ self.setCurrentIndex(self.indexOf(parent))
121
+ self.window().focusToWorkbox()
122
+ self.tabBar().setFont(self.window().font())
123
+ return parent, editor
124
+
125
+ def all_widgets(self):
126
+ """A generator yielding information about every widget under every group.
127
+
128
+ Yields:
129
+ widget, group tab name, widget tab name, group tab index, widget tab index
130
+ """
131
+ for group_index in range(self.count()):
132
+ group_name = self.tabText(group_index)
133
+
134
+ tab_widget = self.widget(group_index)
135
+ for tab_index in range(tab_widget.count()):
136
+ tab_name = tab_widget.tabText(tab_index)
137
+ yield tab_widget.widget(
138
+ tab_index
139
+ ), group_name, tab_name, group_index, tab_index
140
+
141
+ def close_current_tab(self):
142
+ """Convenient method to close the currently open editor tab prompting
143
+ the user to confirm closing."""
144
+ editor_tab = self.currentWidget()
145
+ editor_tab.close_tab(editor_tab.currentIndex())
146
+
147
+ def close_tab(self, index):
148
+ ret = QMessageBox.question(
149
+ self,
150
+ 'Close all editors under this tab?',
151
+ 'Are you sure you want to close all tabs under the "{}" tab?'.format(
152
+ self.tabText(index)
153
+ ),
154
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel,
155
+ )
156
+ if ret == QMessageBox.StandardButton.Yes:
157
+ self.store_closed_workboxes(index)
158
+ super(GroupTabWidget, self).close_tab(index)
159
+
160
+ def store_closed_workboxes(self, index):
161
+ """Store all the workbox names in group tab being closed.
162
+
163
+ Args:
164
+ index (int): The index of the group being closed
165
+ """
166
+ group = self.widget(index)
167
+
168
+ for idx in range(group.count()):
169
+ workbox = group.widget(idx)
170
+
171
+ # Save the workbox first, so we can possibly restore it later.
172
+ workbox.__save_prefs__(saveLinkedFile=False)
173
+
174
+ self.parent().window().addRecentlyClosedWorkbox(workbox)
175
+
176
+ def current_groups_widget(self):
177
+ """Returns the current widget of the currently selected group or None."""
178
+ editor_tab = self.currentWidget()
179
+ if editor_tab:
180
+ return editor_tab.currentWidget()
181
+
182
+ def default_tab(self, title=None, prefs=None):
183
+ title = title or self.default_title
184
+ widget = GroupedTabWidget(
185
+ parent=self,
186
+ editor_kwargs=self.editor_kwargs,
187
+ editor_cls=self.editor_cls,
188
+ core_name=self.core_name,
189
+ )
190
+ return widget, title
191
+
192
+ def get_next_available_tab_name(self, name=None):
193
+ """Get the next available tab name, providing a default if needed.
194
+
195
+ Args:
196
+ name (str, optional): The name for which to get the next available
197
+ name.
198
+
199
+ Returns:
200
+ str: The determined next available tab name
201
+ """
202
+ if name is None:
203
+ name = self.default_title
204
+ return super().get_next_available_tab_name(name)
205
+
206
+ def append_orphan_workboxes_to_prefs(self, prefs, existing_by_group):
207
+ """If prefs are saved in a different PrEditor instance (in this same core)
208
+ there may be a workbox which is either:
209
+ - new in this instance
210
+ - removed in the saved other instance
211
+ Any of these workboxes are 'orphaned'. Rather than just deleting it, we
212
+ alert the user, so that work can be saved.
213
+
214
+ We also add any orphan workboxes to the window's boxesOrphanedViaInstance
215
+ dict, in the form `workbox_id: workbox`.
216
+
217
+ Args:
218
+ prefs (dict): The 'workboxes' section of the PrEditor prefs
219
+ existing_by_group (dict): The existing workbox's info (as returned
220
+ by self.all_widgets(), by group.
221
+
222
+ Returns:
223
+ prefs (dict): The 'workboxes' section of the PrEditor prefs, updated
224
+ """
225
+ groups = prefs.get("groups")
226
+ for group_name, workbox_infos in existing_by_group.items():
227
+ prefs_group = None
228
+ for temp_group in groups:
229
+ temp_name = temp_group.get("name")
230
+ if temp_name == group_name:
231
+ prefs_group = temp_group
232
+ break
233
+
234
+ # If the orphan's group doesn't yet exist, we prepare to make it
235
+ new_group = None
236
+ if not prefs_group:
237
+ new_group = dict(name=group_name, tabs=[])
238
+
239
+ cur_group = prefs_group or new_group
240
+ cur_tabs = cur_group.get("tabs")
241
+
242
+ for workbox_info in workbox_infos:
243
+ # Create workbox_dict
244
+ workbox = workbox_info[0]
245
+ name = workbox_info[2]
246
+
247
+ workbox_id = workbox.__workbox_id__()
248
+
249
+ workbox_dict = dict(
250
+ name=name,
251
+ workbox_id=workbox_id,
252
+ filename=workbox.__filename__(),
253
+ backup_file=workbox.__backup_file__(),
254
+ orphaned_by_instance=True,
255
+ )
256
+
257
+ self.window().boxesOrphanedViaInstance[workbox_id] = workbox
258
+
259
+ cur_tabs.append(workbox_dict)
260
+ if new_group:
261
+ groups.append(cur_group)
262
+ return prefs
263
+
264
+ def restore_prefs(self, prefs):
265
+ """Adds tab groups and tabs, restoring the selected tabs. If a tab is
266
+ linked to a file that no longer exists, will not be added. Restores the
267
+ current tab for each group and the current group of tabs. If a current
268
+ tab is no longer valid, it will default to the first tab.
269
+
270
+ Preference schema:
271
+ ```json
272
+ {
273
+ "groups": [
274
+ {
275
+ // Name of the group tab. [Required]
276
+ "name": "My Group",
277
+ // This group should be the active group. First in list wins.
278
+ "current": true,
279
+ "tabs": [
280
+ {
281
+ // If filename is not null, this file is loaded
282
+ "filename": "C:\\temp\\invalid_asdfdfd.py",
283
+ // Name of the editor's tab [Optional]
284
+ "name": "invalid_asdfdfd.py",
285
+ "workbox_id": null
286
+ },
287
+ {
288
+ // This tab should be active for the group.
289
+ "current": true,
290
+ "filename": null,
291
+ "name": "Workbox",
292
+ // If workbox_id is not null, this file is loaded.
293
+ // Ignored if filename is not null.
294
+ "workbox_id": "workbox_2yrwctco_a.py"
295
+ }
296
+ ]
297
+ }
298
+ ]
299
+ }
300
+ ```
301
+ """
302
+ selected_workbox_id = None
303
+ current_workbox = self.window().current_workbox()
304
+ if current_workbox:
305
+ selected_workbox_id = current_workbox.__workbox_id__()
306
+
307
+ # When re-running restore_prefs (ie after another instance saved
308
+ # workboxes, and we are reloading them here, get the workbox_ids of all
309
+ # workboxes defined in prefs
310
+ pref_workbox_ids = []
311
+ for group in prefs.get('groups', []):
312
+ for tab in group.get('tabs', []):
313
+ pref_workbox_ids.append(tab.get("workbox_id", None))
314
+
315
+ # Collect data about workboxes which already exist (if we are re-running
316
+ # this method after workboxes exist, ie another PrEditor instance has
317
+ # changed contents and we are now matching those changes.
318
+ existing_by_id = {}
319
+ existing_by_group = {}
320
+ for workbox_info in list(self.all_widgets()):
321
+ workbox = workbox_info[0]
322
+ workbox_id = workbox.__workbox_id__()
323
+ group_name = workbox_info[1]
324
+ existing_by_id[workbox.__workbox_id__()] = workbox_info
325
+
326
+ # If we had a workbox, but what we are about to load doesn't include
327
+ # it, add it back in so it will be shown.
328
+ if workbox_id not in pref_workbox_ids:
329
+ existing_by_group.setdefault(group_name, []).append(workbox_info)
330
+
331
+ prefs = self.append_orphan_workboxes_to_prefs(prefs, existing_by_group)
332
+
333
+ self.clear()
334
+
335
+ current_group = None
336
+ workboxes_missing_id = []
337
+ for group in prefs.get('groups', []):
338
+ current_tab = None
339
+ tab_widget = None
340
+
341
+ group_name = group['name']
342
+ group_name = self.get_next_available_tab_name(group_name)
343
+
344
+ for tab in group.get('tabs', []):
345
+ # Only add this tab if, there is data on disk to load. The user can
346
+ # open multiple instances of PrEditor using the same prefs. The
347
+ # json pref data represents the last time the prefs were saved.
348
+ # Each editor's contents are saved to individual files on disk.
349
+ # When a editor tab is closed, the temp file is removed, not on
350
+ # preferences save.
351
+ # By not restoring tabs for deleted files we prevent accidentally
352
+ # restoring a tab with empty text.
353
+
354
+ loadable = False
355
+ name = tab['name']
356
+
357
+ # Support legacy arg for emergency backwards compatibility
358
+ tempfile = tab.get('tempfile', None)
359
+ # Get various possible saved filepaths.
360
+ filename = tab.get('filename', "")
361
+ if filename:
362
+ if Path(filename).is_file():
363
+ loadable = True
364
+
365
+ workbox_id = tab.get('workbox_id', None)
366
+ # If user went back to before PrEditor used workbox_id, and
367
+ # back, the workbox may not be loadable. First, try to recover
368
+ # it from the backup_file. If not recoverable, collect and
369
+ # notify user.
370
+ if workbox_id is None:
371
+ bak_file = tab.get('backup_file', None)
372
+ if bak_file:
373
+ workbox_id = str(Path(bak_file).parent)
374
+ elif not tempfile:
375
+ missing_name = f"{group_name}/{name}"
376
+ workboxes_missing_id.append(missing_name)
377
+ continue
378
+
379
+ orphaned_by_instance = tab.get('orphaned_by_instance', False)
380
+
381
+ # See if there are any workbox backups available
382
+ backup_file, _, count = get_backup_version_info(
383
+ self.window().name, workbox_id, VersionTypes.Last, ""
384
+ )
385
+ if count:
386
+ loadable = True
387
+ if not loadable:
388
+ continue
389
+
390
+ # There is a file on disk, add the tab, creating the group
391
+ # tab if it hasn't already been created.
392
+ prefs = dict(
393
+ workbox_id=workbox_id,
394
+ filename=filename,
395
+ backup_file=backup_file,
396
+ existing_editor_info=existing_by_id.pop(workbox_id, None),
397
+ orphaned_by_instance=orphaned_by_instance,
398
+ tempfile=tempfile,
399
+ )
400
+ tab_widget, editor = self.add_new_tab(
401
+ group_name, title=name, prefs=prefs
402
+ )
403
+
404
+ editor.__set_last_workbox_name__(editor.__workbox_name__())
405
+ editor.__determine_been_changed_by_instance__()
406
+
407
+ # If more than one tab in this group is listed as current, only
408
+ # respect the first
409
+ if current_tab is None and tab.get('current'):
410
+ current_tab = tab_widget.indexOf(editor)
411
+
412
+ # If there were no files to load, this tab was not added and there
413
+ # we don't need to restore the current tab for this group
414
+ if tab_widget is None:
415
+ continue
416
+
417
+ # Restore the current tab for this group
418
+ if current_tab is None:
419
+ # If there is no longer a current tab, default to the first tab
420
+ current_tab = 0
421
+ tab_widget.setCurrentIndex(current_tab)
422
+
423
+ # Which tab group is the active one? If more than one tab in this
424
+ # group is listed as current, only respect the first.
425
+ if current_group is None and group.get('current'):
426
+ current_group = self.indexOf(tab_widget)
427
+
428
+ if selected_workbox_id:
429
+ for widget_info in self.all_widgets():
430
+ widget, _, _, group_idx, tab_idx = widget_info
431
+ if widget.__workbox_id__() == selected_workbox_id:
432
+ self.setCurrentIndex(group_idx)
433
+ grouped = self.widget(group_idx)
434
+ grouped.setCurrentIndex(tab_idx)
435
+ break
436
+
437
+ # If any workboxes could not be loaded because they had no stored
438
+ # workbox_id, notify user. This likely only happens if user goes back
439
+ # to older PrEditor, and back.
440
+ if workboxes_missing_id:
441
+ suffix = "" if len(workboxes_missing_id) == 1 else "es"
442
+ workboxes_missing_id.insert(0, "")
443
+ missing_names = "\n\t".join(workboxes_missing_id)
444
+ msg = (
445
+ f"The following workbox{suffix} somehow did not have a "
446
+ f"workbox_id stored, and therefore could not be loaded:"
447
+ f"{missing_names}"
448
+ )
449
+ print(msg)
450
+
451
+ # Restore the current group for this widget
452
+ if current_group is None:
453
+ # If there is no longer a current tab, default to the first tab
454
+ current_group = 0
455
+ self.setCurrentIndex(current_group)
456
+
457
+ def save_prefs(self, prefs=None):
458
+ groups = []
459
+ if prefs is None:
460
+ prefs = {}
461
+
462
+ prefs['groups'] = groups
463
+ current_group = self.currentIndex()
464
+ for i in range(self.count()):
465
+ tabs = []
466
+ group = {}
467
+ # Hopefully the alphabetical sorting of this dict is preserved in py3
468
+ # to make it easy to diff the json pref file if ever required.
469
+ if i == current_group:
470
+ group['current'] = True
471
+ group['name'] = self.tabText(i)
472
+ group['tabs'] = tabs
473
+
474
+ tab_widget = self.widget(i)
475
+ current_editor = tab_widget.currentIndex()
476
+ for j in range(tab_widget.count()):
477
+ current = True if j == current_editor else None
478
+ workbox = tab_widget.widget(j)
479
+ tabs.append(workbox.__save_prefs__(current=current))
480
+
481
+ groups.append(group)
482
+
483
+ return prefs
484
+
485
+ def set_current_groups_from_index(self, group, editor):
486
+ """Make the specified indexes the current widget and return it. If the
487
+ indexes are out of range the current widget is not changed.
488
+
489
+ Args:
490
+ group (int): The index of the group tab to make current.
491
+ editor (int): The index of the editor under the group tab to
492
+ make current.
493
+
494
+ Returns:
495
+ QWidget: The current widget after applying.
496
+ """
497
+ self.setCurrentIndex(group)
498
+ tab_widget = self.currentWidget()
499
+ tab_widget.setCurrentIndex(editor)
500
+ return tab_widget.currentWidget()
501
+
502
+ def set_current_groups_from_workbox(self, workbox):
503
+ """Make the specified workbox the current widget. If the workbox is not
504
+ found, the current widget is not changed.
505
+
506
+ Args:
507
+ workbox (WorkboxMixin): The workbox to make current.
508
+
509
+ Returns:
510
+ success (bool): Whether the workbox was found and made the current
511
+ widget
512
+ """
513
+ workbox_infos = self.all_widgets()
514
+ found_info = None
515
+ for workbox_info in workbox_infos:
516
+ if workbox_info[0] == workbox:
517
+ found_info = workbox_info
518
+ break
519
+ if found_info:
520
+ workbox = workbox_info[0]
521
+ group_idx = workbox_info[-2]
522
+ editor_idx = workbox_info[-1]
523
+
524
+ self.setCurrentIndex(group_idx)
525
+ tab_widget = self.currentWidget()
526
+ tab_widget.setCurrentIndex(editor_idx)
527
+
528
+ return bool(found_info)
@@ -0,0 +1,35 @@
1
+ from __future__ import absolute_import
2
+
3
+ from preditor.gui.level_buttons import LazyMenu
4
+
5
+
6
+ class GroupTabMenu(LazyMenu):
7
+ """A menu listing all tabs of GroupTabWidget and their child GroupedTabWidget
8
+ tabs. When selecting one of the GroupedTabWidget tab, it will make that tab
9
+ the current tab and give it focus.
10
+ """
11
+
12
+ def __init__(self, manager, parent=None):
13
+ super(GroupTabMenu, self).__init__(parent=parent)
14
+ self.manager = manager
15
+ self.triggered.connect(self.focus_tab)
16
+
17
+ def refresh(self):
18
+ self.clear()
19
+ for group in range(self.manager.count()):
20
+ # Create a "header" for the group tabs
21
+ self.addSeparator()
22
+ act = self.addAction(self.manager.tabText(group))
23
+ act.setEnabled(False)
24
+ self.addSeparator()
25
+
26
+ # Add all of this group tab's tabs
27
+ tab_widget = self.manager.widget(group)
28
+ for index in range(tab_widget.count()):
29
+ act = self.addAction(' {}'.format(tab_widget.tabText(index)))
30
+ act.setProperty('info', (group, index))
31
+
32
+ def focus_tab(self, action):
33
+ group, editor = action.property('info')
34
+ widget = self.manager.set_current_groups_from_index(group, editor)
35
+ widget.setFocus()
@@ -0,0 +1,107 @@
1
+ from __future__ import absolute_import
2
+
3
+ import re
4
+
5
+ from Qt.QtCore import QSortFilterProxyModel, Qt
6
+ from Qt.QtGui import QStandardItem, QStandardItemModel
7
+
8
+
9
+ class GroupTabItemModel(QStandardItemModel):
10
+ GroupIndexRole = Qt.ItemDataRole.UserRole + 1
11
+ TabIndexRole = GroupIndexRole + 1
12
+
13
+ def __init__(self, manager, *args, **kwargs):
14
+ super(GroupTabItemModel, self).__init__(*args, **kwargs)
15
+ self.manager = manager
16
+
17
+ def workbox_indexes_from_model_index(self, index):
18
+ """Returns the group_index and tab_index for the provided QModelIndex"""
19
+ return (
20
+ index.data(self.GroupIndexRole),
21
+ index.data(self.TabIndexRole),
22
+ )
23
+
24
+ def pathFromIndex(self, index):
25
+ parts = [""]
26
+ while index.isValid():
27
+ parts.append(self.data(index, Qt.ItemDataRole.DisplayRole))
28
+ index = index.parent()
29
+ if len(parts) == 1:
30
+ return ""
31
+ return "/".join([x for x in parts[::-1] if x])
32
+
33
+
34
+ class GroupTabTreeItemModel(GroupTabItemModel):
35
+ def process(self):
36
+ root = self.invisibleRootItem()
37
+ current_group = self.manager.currentIndex()
38
+ current_tab = self.manager.currentWidget().currentIndex()
39
+
40
+ prev_group = -1
41
+ all_widgets = self.manager.all_widgets()
42
+ for _, group_name, tab_name, group_index, tab_index in all_widgets:
43
+ if prev_group != group_index:
44
+ group_item = QStandardItem(group_name)
45
+ group_item.setData(group_index, self.GroupIndexRole)
46
+ root.appendRow(group_item)
47
+ prev_group = group_index
48
+
49
+ tab_item = QStandardItem(tab_name)
50
+ tab_item.setData(group_index, self.GroupIndexRole)
51
+ tab_item.setData(tab_index, self.TabIndexRole)
52
+ group_item.appendRow(tab_item)
53
+ if group_index == current_group and tab_index == current_tab:
54
+ self.original_model_index = self.indexFromItem(tab_item)
55
+
56
+
57
+ class GroupTabListItemModel(GroupTabItemModel):
58
+ def flags(self, index):
59
+ return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable
60
+
61
+ def process(self):
62
+ root = self.invisibleRootItem()
63
+ current_group = self.manager.currentIndex()
64
+ current_tab = self.manager.currentWidget().currentIndex()
65
+
66
+ all_widgets = self.manager.all_widgets()
67
+ for _, group_name, tab_name, group_index, tab_index in all_widgets:
68
+ tab_item = QStandardItem('/'.join((group_name, tab_name)))
69
+ tab_item.setData(group_index, self.GroupIndexRole)
70
+ tab_item.setData(tab_index, self.TabIndexRole)
71
+ root.appendRow(tab_item)
72
+ if group_index == current_group and tab_index == current_tab:
73
+ self.original_model_index = self.indexFromItem(tab_item)
74
+
75
+
76
+ class GroupTabFuzzyFilterProxyModel(QSortFilterProxyModel):
77
+ """Implements a fuzzy search filter proxy model."""
78
+
79
+ def __init__(self, parent=None):
80
+ super(GroupTabFuzzyFilterProxyModel, self).__init__(parent=parent)
81
+ self._fuzzy_regex = None
82
+
83
+ def setFuzzySearch(self, search):
84
+ search = '.*'.join(search)
85
+ # search = '.*{}.*'.format(search)
86
+ self._fuzzy_regex = re.compile(search, re.I)
87
+ self.invalidateFilter()
88
+
89
+ def filterAcceptsRow(self, sourceRow, sourceParent):
90
+ if self.filterKeyColumn() == 0 and self._fuzzy_regex:
91
+ index = self.sourceModel().index(sourceRow, 0, sourceParent)
92
+ data = self.sourceModel().data(index)
93
+ ret = bool(self._fuzzy_regex.search(data))
94
+ return ret
95
+
96
+ return super(GroupTabFuzzyFilterProxyModel, self).filterAcceptsRow(
97
+ sourceRow, sourceParent
98
+ )
99
+
100
+ def pathFromIndex(self, index):
101
+ parts = [""]
102
+ while index.isValid():
103
+ parts.append(self.data(index, Qt.ItemDataRole.DisplayRole))
104
+ index = index.parent()
105
+ if len(parts) == 1:
106
+ return ""
107
+ return "/".join([x for x in parts[::-1] if x])