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,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])
|