PrEditor 1.0.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.

Potentially problematic release.


This version of PrEditor might be problematic. Click here for more details.

Files changed (158) hide show
  1. preditor/__init__.py +322 -0
  2. preditor/__main__.py +13 -0
  3. preditor/about_module.py +161 -0
  4. preditor/cli.py +192 -0
  5. preditor/config.py +302 -0
  6. preditor/contexts.py +119 -0
  7. preditor/cores/__init__.py +0 -0
  8. preditor/cores/core.py +20 -0
  9. preditor/dccs/maya/PrEditor_maya.mod +2 -0
  10. preditor/dccs/maya/plug-ins/PrEditor_maya.py +110 -0
  11. preditor/debug.py +144 -0
  12. preditor/delayable_engine/__init__.py +302 -0
  13. preditor/delayable_engine/delayables.py +85 -0
  14. preditor/enum.py +728 -0
  15. preditor/excepthooks.py +131 -0
  16. preditor/gui/__init__.py +93 -0
  17. preditor/gui/app.py +160 -0
  18. preditor/gui/codehighlighter.py +209 -0
  19. preditor/gui/completer.py +226 -0
  20. preditor/gui/console.py +867 -0
  21. preditor/gui/dialog.py +178 -0
  22. preditor/gui/drag_tab_bar.py +190 -0
  23. preditor/gui/editor_chooser.py +57 -0
  24. preditor/gui/errordialog.py +68 -0
  25. preditor/gui/find_files.py +125 -0
  26. preditor/gui/fuzzy_search/__init__.py +0 -0
  27. preditor/gui/fuzzy_search/fuzzy_search.py +93 -0
  28. preditor/gui/group_tab_widget/__init__.py +325 -0
  29. preditor/gui/group_tab_widget/grouped_tab_menu.py +35 -0
  30. preditor/gui/group_tab_widget/grouped_tab_models.py +108 -0
  31. preditor/gui/group_tab_widget/grouped_tab_widget.py +78 -0
  32. preditor/gui/group_tab_widget/one_tab_widget.py +54 -0
  33. preditor/gui/level_buttons.py +343 -0
  34. preditor/gui/logger_window_handler.py +48 -0
  35. preditor/gui/logger_window_plugin.py +32 -0
  36. preditor/gui/loggerwindow.py +1385 -0
  37. preditor/gui/newtabwidget.py +69 -0
  38. preditor/gui/set_text_editor_path_dialog.py +59 -0
  39. preditor/gui/status_label.py +99 -0
  40. preditor/gui/suggest_path_quotes_dialog.py +50 -0
  41. preditor/gui/ui/editor_chooser.ui +93 -0
  42. preditor/gui/ui/errordialog.ui +74 -0
  43. preditor/gui/ui/find_files.ui +140 -0
  44. preditor/gui/ui/loggerwindow.ui +1105 -0
  45. preditor/gui/ui/set_text_editor_path_dialog.ui +189 -0
  46. preditor/gui/ui/suggest_path_quotes_dialog.ui +225 -0
  47. preditor/gui/window.py +161 -0
  48. preditor/gui/workbox_mixin.py +389 -0
  49. preditor/gui/workbox_text_edit.py +137 -0
  50. preditor/gui/workboxwidget.py +298 -0
  51. preditor/logging_config.py +52 -0
  52. preditor/osystem.py +401 -0
  53. preditor/plugins.py +118 -0
  54. preditor/prefs.py +74 -0
  55. preditor/resource/environment_variables.html +26 -0
  56. preditor/resource/error_mail.html +85 -0
  57. preditor/resource/error_mail_inline.html +41 -0
  58. preditor/resource/img/README.md +17 -0
  59. preditor/resource/img/arrow_forward.png +0 -0
  60. preditor/resource/img/check-bold.png +0 -0
  61. preditor/resource/img/chevron-down.png +0 -0
  62. preditor/resource/img/chevron-up.png +0 -0
  63. preditor/resource/img/close-thick.png +0 -0
  64. preditor/resource/img/comment-edit.png +0 -0
  65. preditor/resource/img/content-copy.png +0 -0
  66. preditor/resource/img/content-cut.png +0 -0
  67. preditor/resource/img/content-duplicate.png +0 -0
  68. preditor/resource/img/content-paste.png +0 -0
  69. preditor/resource/img/content-save.png +0 -0
  70. preditor/resource/img/debug_disabled.png +0 -0
  71. preditor/resource/img/eye-check.png +0 -0
  72. preditor/resource/img/file-plus.png +0 -0
  73. preditor/resource/img/file-remove.png +0 -0
  74. preditor/resource/img/format-align-left.png +0 -0
  75. preditor/resource/img/format-letter-case-lower.png +0 -0
  76. preditor/resource/img/format-letter-case-upper.png +0 -0
  77. preditor/resource/img/format-letter-case.svg +1 -0
  78. preditor/resource/img/information.png +0 -0
  79. preditor/resource/img/logging_critical.png +0 -0
  80. preditor/resource/img/logging_custom.png +0 -0
  81. preditor/resource/img/logging_debug.png +0 -0
  82. preditor/resource/img/logging_error.png +0 -0
  83. preditor/resource/img/logging_info.png +0 -0
  84. preditor/resource/img/logging_not_set.png +0 -0
  85. preditor/resource/img/logging_warning.png +0 -0
  86. preditor/resource/img/marker.png +0 -0
  87. preditor/resource/img/play.png +0 -0
  88. preditor/resource/img/playlist-play.png +0 -0
  89. preditor/resource/img/plus-minus-variant.png +0 -0
  90. preditor/resource/img/preditor.ico +0 -0
  91. preditor/resource/img/preditor.png +0 -0
  92. preditor/resource/img/preditor.psd +0 -0
  93. preditor/resource/img/preditor.svg +44 -0
  94. preditor/resource/img/regex.svg +1 -0
  95. preditor/resource/img/restart.svg +1 -0
  96. preditor/resource/img/skip-forward-outline.png +0 -0
  97. preditor/resource/img/skip-next-outline.png +0 -0
  98. preditor/resource/img/skip-next.png +0 -0
  99. preditor/resource/img/skip-previous.png +0 -0
  100. preditor/resource/img/subdirectory-arrow-right.png +0 -0
  101. preditor/resource/img/text-search-variant.png +0 -0
  102. preditor/resource/img/warning-big.png +0 -0
  103. preditor/resource/lang/python.json +30 -0
  104. preditor/resource/settings.ini +25 -0
  105. preditor/resource/stylesheet/Bright.css +65 -0
  106. preditor/resource/stylesheet/Dark.css +199 -0
  107. preditor/scintilla/__init__.py +22 -0
  108. preditor/scintilla/delayables/__init__.py +11 -0
  109. preditor/scintilla/delayables/smart_highlight.py +94 -0
  110. preditor/scintilla/delayables/spell_check.py +173 -0
  111. preditor/scintilla/documenteditor.py +2038 -0
  112. preditor/scintilla/finddialog.py +68 -0
  113. preditor/scintilla/lang/__init__.py +80 -0
  114. preditor/scintilla/lang/config/bash.ini +15 -0
  115. preditor/scintilla/lang/config/batch.ini +14 -0
  116. preditor/scintilla/lang/config/cpp.ini +19 -0
  117. preditor/scintilla/lang/config/css.ini +19 -0
  118. preditor/scintilla/lang/config/eyeonscript.ini +17 -0
  119. preditor/scintilla/lang/config/html.ini +21 -0
  120. preditor/scintilla/lang/config/javascript.ini +24 -0
  121. preditor/scintilla/lang/config/lua.ini +16 -0
  122. preditor/scintilla/lang/config/maxscript.ini +20 -0
  123. preditor/scintilla/lang/config/mel.ini +18 -0
  124. preditor/scintilla/lang/config/mu.ini +22 -0
  125. preditor/scintilla/lang/config/nsi.ini +19 -0
  126. preditor/scintilla/lang/config/perl.ini +19 -0
  127. preditor/scintilla/lang/config/puppet.ini +19 -0
  128. preditor/scintilla/lang/config/python.ini +28 -0
  129. preditor/scintilla/lang/config/ruby.ini +19 -0
  130. preditor/scintilla/lang/config/sql.ini +7 -0
  131. preditor/scintilla/lang/config/xml.ini +21 -0
  132. preditor/scintilla/lang/config/yaml.ini +18 -0
  133. preditor/scintilla/lang/language.py +240 -0
  134. preditor/scintilla/lexers/__init__.py +0 -0
  135. preditor/scintilla/lexers/cpplexer.py +21 -0
  136. preditor/scintilla/lexers/javascriptlexer.py +25 -0
  137. preditor/scintilla/lexers/maxscriptlexer.py +234 -0
  138. preditor/scintilla/lexers/mellexer.py +368 -0
  139. preditor/scintilla/lexers/mulexer.py +32 -0
  140. preditor/scintilla/lexers/pythonlexer.py +41 -0
  141. preditor/scintilla/ui/finddialog.ui +160 -0
  142. preditor/settings.py +71 -0
  143. preditor/stream/__init__.py +80 -0
  144. preditor/stream/director.py +73 -0
  145. preditor/stream/manager.py +74 -0
  146. preditor/streamhandler_helper.py +46 -0
  147. preditor/utils/__init__.py +0 -0
  148. preditor/utils/cute.py +30 -0
  149. preditor/utils/stylesheets.py +54 -0
  150. preditor/utils/text_search.py +342 -0
  151. preditor/version.py +21 -0
  152. preditor/weakref.py +363 -0
  153. preditor-1.0.0.dist-info/METADATA +224 -0
  154. preditor-1.0.0.dist-info/RECORD +158 -0
  155. preditor-1.0.0.dist-info/WHEEL +5 -0
  156. preditor-1.0.0.dist-info/entry_points.txt +18 -0
  157. preditor-1.0.0.dist-info/licenses/LICENSE +165 -0
  158. preditor-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,93 @@
1
+ from __future__ import absolute_import
2
+
3
+ from functools import partial
4
+
5
+ from Qt.QtCore import QModelIndex, QPoint, Qt, Signal
6
+ from Qt.QtWidgets import QFrame, QLineEdit, QListView, QShortcut, QVBoxLayout
7
+
8
+ from ..group_tab_widget.grouped_tab_models import GroupTabFuzzyFilterProxyModel
9
+
10
+
11
+ class FuzzySearch(QFrame):
12
+ canceled = Signal("QModelIndex")
13
+ """Passes the original QModelIndex for the tab that was selected when the
14
+ widget was first shown. This lets you reset back to the orignal state."""
15
+ highlighted = Signal("QModelIndex")
16
+ """Emitted when the user navitages to the given index, but hasn't selected."""
17
+ selected = Signal("QModelIndex")
18
+ """Emitted when the user selects a item."""
19
+
20
+ def __init__(self, model, parent=None, **kwargs):
21
+ super(FuzzySearch, self).__init__(parent=parent, **kwargs)
22
+ self.y_offset = 100
23
+ self.setMinimumSize(400, 200)
24
+ self.uiCloseSCT = QShortcut(
25
+ Qt.Key_Escape, self, context=Qt.WidgetWithChildrenShortcut
26
+ )
27
+ self.uiCloseSCT.activated.connect(self._canceled)
28
+
29
+ self.uiUpSCT = QShortcut(Qt.Key_Up, self, context=Qt.WidgetWithChildrenShortcut)
30
+ self.uiUpSCT.activated.connect(partial(self.increment_selection, -1))
31
+ self.uiDownSCT = QShortcut(
32
+ Qt.Key_Down, self, context=Qt.WidgetWithChildrenShortcut
33
+ )
34
+ self.uiDownSCT.activated.connect(partial(self.increment_selection, 1))
35
+
36
+ lyt = QVBoxLayout(self)
37
+ self.uiLineEDIT = QLineEdit(parent=self)
38
+ self.uiLineEDIT.textChanged.connect(self.update_completer)
39
+ self.uiLineEDIT.returnPressed.connect(self.activated)
40
+ lyt.addWidget(self.uiLineEDIT)
41
+ self.uiResultsLIST = QListView(self)
42
+ self.uiResultsLIST.activated.connect(self.activated)
43
+ self.proxy_model = GroupTabFuzzyFilterProxyModel(self)
44
+ self.proxy_model.setSourceModel(model)
45
+ self.uiResultsLIST.setModel(self.proxy_model)
46
+ lyt.addWidget(self.uiResultsLIST)
47
+
48
+ self.original_model_index = model.original_model_index
49
+
50
+ def activated(self):
51
+ current = self.uiResultsLIST.currentIndex()
52
+ self.selected.emit(current)
53
+ self.hide()
54
+
55
+ def increment_selection(self, direction):
56
+ current = self.uiResultsLIST.currentIndex()
57
+ col = 0
58
+ row = 0
59
+ if current.isValid():
60
+ col = current.column()
61
+ row = current.row() + direction
62
+ new = self.uiResultsLIST.model().index(row, col)
63
+ self.uiResultsLIST.setCurrentIndex(new)
64
+ self.highlighted.emit(new)
65
+
66
+ def update_completer(self, wildcard):
67
+ if wildcard:
68
+ if not self.uiResultsLIST.currentIndex().isValid():
69
+ new = self.uiResultsLIST.model().index(0, 0)
70
+ self.uiResultsLIST.setCurrentIndex(new)
71
+ else:
72
+ self.uiResultsLIST.clearSelection()
73
+ self.uiResultsLIST.setCurrentIndex(QModelIndex())
74
+ self.proxy_model.setFuzzySearch(wildcard)
75
+ self.highlighted.emit(self.uiResultsLIST.currentIndex())
76
+
77
+ def _canceled(self):
78
+ # Restore the original tab as the user didn't choose the new tab
79
+ self.canceled.emit(self.original_model_index)
80
+ self.hide()
81
+
82
+ def reposition(self):
83
+ pgeo = self.parent().geometry()
84
+ geo = self.geometry()
85
+ center = QPoint(pgeo.width() // 2, 0)
86
+ geo.moveCenter(center)
87
+ geo.setY(self.y_offset)
88
+ self.setGeometry(geo)
89
+
90
+ def popup(self):
91
+ self.show()
92
+ self.reposition()
93
+ self.uiLineEDIT.setFocus(Qt.PopupFocusReason)
@@ -0,0 +1,325 @@
1
+ from __future__ import absolute_import
2
+
3
+ import os
4
+ import re
5
+
6
+ from Qt.QtCore import Qt
7
+ from Qt.QtGui import QIcon
8
+ from Qt.QtWidgets import QHBoxLayout, QMessageBox, QToolButton, QWidget
9
+
10
+ from ... import resourcePath
11
+ from ...prefs import prefs_path
12
+ from ..drag_tab_bar import DragTabBar
13
+ from ..workbox_text_edit import WorkboxTextEdit
14
+ from .grouped_tab_menu import GroupTabMenu
15
+ from .grouped_tab_widget import GroupedTabWidget
16
+ from .one_tab_widget import OneTabWidget
17
+
18
+ DEFAULT_STYLE_SHEET = """
19
+ /* Make the two buttons in the GroupTabWidget take up the
20
+ same horizontal space as the GroupedTabWidget's buttons. */
21
+ GroupTabWidget>QTabBar::tab{
22
+ max-height: 1.5em;
23
+ }
24
+ /* We have an icon, no need to show the menu indicator */
25
+ #group_tab_widget_menu_btn::menu-indicator{
26
+ width: 0px;
27
+ }
28
+ /* The GroupedTabWidget has a single button, make it take
29
+ the same space as the GroupTabWidget buttons. */
30
+ GroupedTabWidget>QToolButton,GroupTabWidget>QWidget{
31
+ width: 3em;
32
+ }
33
+ """
34
+
35
+
36
+ class GroupTabWidget(OneTabWidget):
37
+ """A QTabWidget where each tab contains another tab widget, allowing users
38
+ to group code editors. It has a corner button to add a new tab, and a menu
39
+ allowing users to quickly focus on any tab in the entire group.
40
+ """
41
+
42
+ def __init__(self, editor_kwargs=None, core_name=None, *args, **kwargs):
43
+ super(GroupTabWidget, self).__init__(*args, **kwargs)
44
+ DragTabBar.install_tab_widget(self, 'group_tab_widget')
45
+ self.editor_kwargs = editor_kwargs
46
+ self.editor_cls = WorkboxTextEdit
47
+ self.core_name = core_name
48
+ self.setStyleSheet(DEFAULT_STYLE_SHEET)
49
+ corner = QWidget(self)
50
+ lyt = QHBoxLayout(corner)
51
+ lyt.setSpacing(0)
52
+ lyt.setContentsMargins(0, 0, 0, 0)
53
+
54
+ corner.uiNewTabBTN = QToolButton(corner)
55
+ corner.uiNewTabBTN.setObjectName('group_tab_widget_new_btn')
56
+ corner.uiNewTabBTN.setText('+')
57
+ corner.uiNewTabBTN.setIcon(QIcon(resourcePath('img/file-plus.png')))
58
+ corner.uiNewTabBTN.released.connect(lambda: self.add_new_tab(None))
59
+ lyt.addWidget(corner.uiNewTabBTN)
60
+
61
+ corner.uiMenuBTN = QToolButton(corner)
62
+ corner.uiMenuBTN.setIcon(QIcon(resourcePath('img/chevron-down.png')))
63
+ corner.uiMenuBTN.setObjectName('group_tab_widget_menu_btn')
64
+ corner.uiMenuBTN.setPopupMode(QToolButton.InstantPopup)
65
+ corner.uiCornerMENU = GroupTabMenu(self, parent=corner.uiMenuBTN)
66
+ corner.uiMenuBTN.setMenu(corner.uiCornerMENU)
67
+ lyt.addWidget(corner.uiMenuBTN)
68
+
69
+ self.uiCornerBTN = corner
70
+ self.setCornerWidget(self.uiCornerBTN, Qt.TopRightCorner)
71
+
72
+ def add_new_tab(self, group, title="Workbox", group_fmt=None):
73
+ """Adds a new tab to the requested group, creating the group if the group
74
+ doesn't exist.
75
+
76
+ Args:
77
+ group: The group to add a new tab to. This can be an int index of an
78
+ existing tab, or the name of the group and it will create the group
79
+ if needed. If None is passed it will add a new tab `Group {last+1}`.
80
+ If True is passed, then the current group tab is used.
81
+ title (str, optional): The name to give the newly created tab inside
82
+ the group.
83
+ group_fmt(str, optional): If None is passed to group, this string is
84
+ used to search for existing tabs to calculate the last number
85
+ and generate the new group tab name.
86
+
87
+ Returns:
88
+ GroupedTabWidget: The tab group for this group.
89
+ WorkboxMixin: The new text editor.
90
+ """
91
+ parent = None
92
+ if not group:
93
+ if group_fmt is None:
94
+ group_fmt = r'Group {}'
95
+ last = 0
96
+ for i in range(self.count()):
97
+ match = re.match(group_fmt.format(r'(\d+)'), self.tabText(i))
98
+ if match:
99
+ last = max(last, int(match.group(1)))
100
+ group = group_fmt.format(last + 1)
101
+ elif group is True:
102
+ group = self.currentIndex()
103
+ if isinstance(group, int):
104
+ group_title = self.tabText(group)
105
+ parent = self.widget(group)
106
+ elif isinstance(group, str):
107
+ group_title = group
108
+ index = self.index_for_text(group)
109
+ if index != -1:
110
+ parent = self.widget(index)
111
+
112
+ if not parent:
113
+ parent, group_title = self.default_tab(group_title)
114
+ self.addTab(parent, group_title)
115
+
116
+ # Create the first editor tab and make it visible
117
+ editor = parent.add_new_editor(title)
118
+ self.setCurrentIndex(self.indexOf(parent))
119
+ self.window().focusToWorkbox()
120
+ return parent, editor
121
+
122
+ def all_widgets(self):
123
+ """A generator yielding information about every widget under every group.
124
+
125
+ Yields:
126
+ widget, group tab name, widget tab name, group tab index, widget tab index
127
+ """
128
+ for group_index in range(self.count()):
129
+ group_name = self.tabText(group_index)
130
+
131
+ tab_widget = self.widget(group_index)
132
+ for tab_index in range(tab_widget.count()):
133
+ tab_name = tab_widget.tabText(tab_index)
134
+ yield tab_widget.widget(
135
+ tab_index
136
+ ), group_name, tab_name, group_index, tab_index
137
+
138
+ def close_current_tab(self):
139
+ """Convenient method to close the currently open editor tab prompting
140
+ the user to confirm closing."""
141
+ editor_tab = self.currentWidget()
142
+ editor_tab.close_tab(editor_tab.currentIndex())
143
+
144
+ def close_tab(self, index):
145
+ ret = QMessageBox.question(
146
+ self,
147
+ 'Close all editors under this tab?',
148
+ 'Are you sure you want to close all tabs under the "{}" tab?'.format(
149
+ self.tabText(self.currentIndex())
150
+ ),
151
+ QMessageBox.Yes | QMessageBox.Cancel,
152
+ )
153
+ if ret == QMessageBox.Yes:
154
+ # Clean up all temp files created by this group's editors if they
155
+ # are not using actual saved files.
156
+ tab_widget = self.widget(self.currentIndex())
157
+ for index in range(tab_widget.count()):
158
+ editor = tab_widget.widget(index)
159
+ editor.__remove_tempfile__()
160
+
161
+ super(GroupTabWidget, self).close_tab(self.currentIndex())
162
+
163
+ def current_groups_widget(self):
164
+ """Returns the current widget of the currently selected group or None."""
165
+ editor_tab = self.currentWidget()
166
+ if editor_tab:
167
+ return editor_tab.currentWidget()
168
+
169
+ def default_tab(self, title='Group 1'):
170
+ widget = GroupedTabWidget(
171
+ parent=self,
172
+ editor_kwargs=self.editor_kwargs,
173
+ editor_cls=self.editor_cls,
174
+ core_name=self.core_name,
175
+ )
176
+ return widget, title
177
+
178
+ def restore_prefs(self, prefs):
179
+ """Adds tab groups and tabs, restoring the selected tabs. If a tab is
180
+ linked to a file that no longer exists, will not be added. Restores the
181
+ current tab for each group and the current group of tabs. If a current
182
+ tab is no longer valid, it will default to the first tab.
183
+
184
+ Preference schema:
185
+ ```json
186
+ {
187
+ "groups": [
188
+ {
189
+ // Name of the group tab. [Required]
190
+ "name": "My Group",
191
+ // This group should be the active group. First in list wins.
192
+ "current": true,
193
+ "tabs": [
194
+ {
195
+ // If filename is not null, this file is loaded
196
+ "filename": "C:\\temp\\invalid_asdfdfd.py",
197
+ // Name of the editor's tab [Optional]
198
+ "name": "invalid_asdfdfd.py",
199
+ "tempfile": null
200
+ },
201
+ {
202
+ // This tab should be active for the group.
203
+ "current": true,
204
+ "filename": null,
205
+ "name": "Workbox",
206
+ // If tempfile is not null, this file is loaded.
207
+ // Ignored if filename is not null.
208
+ "tempfile": "workbox_2yrwctco_a.py"
209
+ }
210
+ ]
211
+ }
212
+ ]
213
+ }
214
+ ```
215
+ """
216
+
217
+ self.clear()
218
+
219
+ workbox_dir = prefs_path('workboxes', core_name=self.core_name)
220
+ current_group = None
221
+ for group in prefs.get('groups', []):
222
+ current_tab = None
223
+ group_name = group['name']
224
+ tab_widget = None
225
+
226
+ for tab in group.get('tabs', []):
227
+ # Only add this tab if, there is data on disk to load. The user can
228
+ # open multiple instances of PrEditor using the same prefs. The
229
+ # json pref data represents the last time the prefs were saved.
230
+ # Each editor's contents are saved to individual files on disk.
231
+ # When a editor tab is closed, the temp file is removed, not on
232
+ # preferences save.
233
+ # By not restoring tabs for deleted files we prevent accidentally
234
+ # restoring a tab with empty text.
235
+ filename = tab.get('filename')
236
+ temp_name = tab.get('tempfile')
237
+ if filename:
238
+ if not os.path.exists(filename):
239
+ continue
240
+ if not temp_name:
241
+ continue
242
+ temp_name = os.path.join(workbox_dir, temp_name)
243
+ if not os.path.exists(temp_name):
244
+ continue
245
+
246
+ # There is a file on disk, add the tab, creating the group
247
+ # tab if it hasn't already been created.
248
+ name = tab['name']
249
+ tab_widget, editor = self.add_new_tab(group_name, name)
250
+ editor.__restore_prefs__(tab)
251
+
252
+ # If more than one tab in this group is listed as current, only
253
+ # respect the first
254
+ if current_tab is None and tab.get('current'):
255
+ current_tab = tab_widget.indexOf(editor)
256
+
257
+ # If there were no files to load, this tab was not added and there
258
+ # we don't need to restore the current tab for this group
259
+ if tab_widget is None:
260
+ continue
261
+
262
+ # Restore the current tab for this group
263
+ if current_tab is None:
264
+ # If there is no longer a current tab, default to the first tab
265
+ current_tab = 0
266
+ tab_widget.setCurrentIndex(current_tab)
267
+
268
+ # Which tab group is the active one? If more than one tab in this
269
+ # group is listed as current, only respect the first.
270
+ if current_group is None and group.get('current'):
271
+ current_group = self.indexOf(tab_widget)
272
+
273
+ # Restore the current group for this widget
274
+ if current_group is None:
275
+ # If there is no longer a current tab, default to the first tab
276
+ current_group = 0
277
+ self.setCurrentIndex(current_group)
278
+
279
+ def save_prefs(self, prefs=None):
280
+ groups = []
281
+ if prefs is None:
282
+ prefs = {}
283
+
284
+ prefs['groups'] = groups
285
+ current_group = self.currentIndex()
286
+ for i in range(self.count()):
287
+ tabs = []
288
+ group = {}
289
+ # Hopefully the alphabetical sorting of this dict is preserved in py3
290
+ # to make it easy to diff the json pref file if ever required.
291
+ if i == current_group:
292
+ group['current'] = True
293
+ group['name'] = self.tabText(i)
294
+ group['tabs'] = tabs
295
+
296
+ tab_widget = self.widget(i)
297
+ current_editor = tab_widget.currentIndex()
298
+ for j in range(tab_widget.count()):
299
+ current = True if j == current_editor else None
300
+ tabs.append(
301
+ tab_widget.widget(j).__save_prefs__(
302
+ name=tab_widget.tabText(j), current=current
303
+ )
304
+ )
305
+
306
+ groups.append(group)
307
+
308
+ return prefs
309
+
310
+ def set_current_groups_from_index(self, group, editor):
311
+ """Make the specified indexes the current widget and return it. If the
312
+ indexes are out of range the current widget is not changed.
313
+
314
+ Args:
315
+ group (int): The index of the group tab to make current.
316
+ editor (int): The index of the editor under the group tab to
317
+ make current.
318
+
319
+ Returns:
320
+ QWidget: The current widget after applying.
321
+ """
322
+ self.setCurrentIndex(group)
323
+ tab_widget = self.currentWidget()
324
+ tab_widget.setCurrentIndex(editor)
325
+ return tab_widget.currentWidget()
@@ -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,108 @@
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.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.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.ItemIsEnabled | Qt.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
+
92
+ index = self.sourceModel().index(sourceRow, 0, sourceParent)
93
+ data = self.sourceModel().data(index)
94
+ ret = bool(self._fuzzy_regex.search(data))
95
+ return ret
96
+
97
+ return super(GroupTabFuzzyFilterProxyModel, self).filterAcceptsRow(
98
+ sourceRow, sourceParent
99
+ )
100
+
101
+ def pathFromIndex(self, index):
102
+ parts = [""]
103
+ while index.isValid():
104
+ parts.append(self.data(index, Qt.DisplayRole))
105
+ index = index.parent()
106
+ if len(parts) == 1:
107
+ return ""
108
+ return "/".join([x for x in parts[::-1] if x])
@@ -0,0 +1,78 @@
1
+ from __future__ import absolute_import
2
+
3
+ from Qt.QtCore import Qt
4
+ from Qt.QtGui import QIcon
5
+ from Qt.QtWidgets import QMessageBox, QToolButton
6
+
7
+ from ... import resourcePath
8
+ from ..drag_tab_bar import DragTabBar
9
+ from ..workbox_text_edit import WorkboxTextEdit
10
+ from .one_tab_widget import OneTabWidget
11
+
12
+
13
+ class GroupedTabWidget(OneTabWidget):
14
+ def __init__(self, editor_kwargs, editor_cls=None, core_name=None, *args, **kwargs):
15
+ super(GroupedTabWidget, self).__init__(*args, **kwargs)
16
+ DragTabBar.install_tab_widget(self, 'grouped_tab_widget')
17
+ self.editor_kwargs = editor_kwargs
18
+ if editor_cls is None:
19
+ editor_cls = WorkboxTextEdit
20
+ self.editor_cls = editor_cls
21
+ self.core_name = core_name
22
+ self.currentChanged.connect(self.tab_shown)
23
+
24
+ self.uiCornerBTN = QToolButton(self)
25
+ self.uiCornerBTN.setText('+')
26
+ self.uiCornerBTN.setIcon(QIcon(resourcePath('img/file-plus.png')))
27
+ self.uiCornerBTN.released.connect(lambda: self.add_new_editor())
28
+ self.setCornerWidget(self.uiCornerBTN, Qt.TopRightCorner)
29
+
30
+ def add_new_editor(self, title="Workbox"):
31
+ editor, title = self.default_tab(title)
32
+ index = self.addTab(editor, title)
33
+ self.setCurrentIndex(index)
34
+ return editor
35
+
36
+ def addTab(self, *args, **kwargs): # noqa: N802
37
+ ret = super(GroupedTabWidget, self).addTab(*args, **kwargs)
38
+ self.update_closable_tabs()
39
+ return ret
40
+
41
+ def close_tab(self, index):
42
+ if self.count() == 1:
43
+ msg = "You have to leave at least one tab open."
44
+ QMessageBox.critical(self, 'Tab can not be closed.', msg, QMessageBox.Ok)
45
+ return
46
+ ret = QMessageBox.question(
47
+ self,
48
+ 'Donate to the cause?',
49
+ "Would you like to donate this tabs contents to the /dev/null fund "
50
+ "for wayward code?",
51
+ QMessageBox.Yes | QMessageBox.Cancel,
52
+ )
53
+ if ret == QMessageBox.Yes:
54
+ # If the tab was saved to a temp file, remove it from disk
55
+ editor = self.widget(index)
56
+ editor.__remove_tempfile__()
57
+
58
+ super(GroupedTabWidget, self).close_tab(index)
59
+
60
+ def default_tab(self, title='Workbox'):
61
+ kwargs = self.editor_kwargs if self.editor_kwargs else {}
62
+ editor = self.editor_cls(parent=self, core_name=self.core_name, **kwargs)
63
+ return editor, title
64
+
65
+ def showEvent(self, event): # noqa: N802
66
+ super(GroupedTabWidget, self).showEvent(event)
67
+ self.tab_shown(self.currentIndex())
68
+
69
+ def tab_shown(self, index):
70
+ editor = self.widget(index)
71
+ if editor and editor.isVisible():
72
+ editor.__show__()
73
+
74
+ if hasattr(self.window(), "setWorkboxFontBasedOnConsole"):
75
+ self.window().setWorkboxFontBasedOnConsole()
76
+
77
+ def update_closable_tabs(self):
78
+ self.setTabsClosable(self.count() != 1)
@@ -0,0 +1,54 @@
1
+ from __future__ import absolute_import
2
+
3
+ from Qt.QtWidgets import QTabWidget
4
+
5
+
6
+ class OneTabWidget(QTabWidget):
7
+ """A QTabWidget that shows the close button only if there is more than one
8
+ tab. If something removes the last tab, it will add a default tab if the
9
+ default_tab method is implemented on a subclass. This is also used to create
10
+ the first tab on showEvent.
11
+
12
+ Subclasses can implement a `default_tab()` method. This should return the
13
+ widget to add and the title of the tab to create if implemented.
14
+ """
15
+
16
+ def __init__(self, *args, **kwargs):
17
+ super(OneTabWidget, self).__init__(*args, **kwargs)
18
+ self.tabCloseRequested.connect(self.close_tab)
19
+
20
+ def addTab(self, *args, **kwargs): # noqa: N802
21
+ ret = super(OneTabWidget, self).addTab(*args, **kwargs)
22
+ self.update_closable_tabs()
23
+ return ret
24
+
25
+ def close_tab(self, index):
26
+ self.removeTab(index)
27
+ self.update_closable_tabs()
28
+
29
+ def index_for_text(self, text):
30
+ """Return the index of the tab with this text. Returns -1 if not found"""
31
+ for i in range(self.count()):
32
+ if self.tabText(i) == text:
33
+ return i
34
+ return -1
35
+
36
+ def insertTab(self, *args, **kwargs): # noqa: N802
37
+ ret = super(OneTabWidget, self).insertTab(*args, **kwargs)
38
+ self.update_closable_tabs()
39
+ return ret
40
+
41
+ def removeTab(self, index): # noqa: N802
42
+ super(OneTabWidget, self).removeTab(index)
43
+ if hasattr(self, 'default_tab') and not self.count():
44
+ self.addTab(*self.default_tab())
45
+ self.update_closable_tabs()
46
+
47
+ def showEvent(self, event): # noqa: N802
48
+ super(OneTabWidget, self).showEvent(event)
49
+ # Force the creation of a default tab if defined
50
+ if hasattr(self, 'default_tab') and not self.count():
51
+ self.addTab(*self.default_tab())
52
+
53
+ def update_closable_tabs(self):
54
+ self.setTabsClosable(self.count() != 1)