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.
- preditor/__init__.py +322 -0
- preditor/__main__.py +13 -0
- preditor/about_module.py +161 -0
- preditor/cli.py +192 -0
- preditor/config.py +302 -0
- preditor/contexts.py +119 -0
- preditor/cores/__init__.py +0 -0
- preditor/cores/core.py +20 -0
- preditor/dccs/maya/PrEditor_maya.mod +2 -0
- preditor/dccs/maya/plug-ins/PrEditor_maya.py +110 -0
- preditor/debug.py +144 -0
- preditor/delayable_engine/__init__.py +302 -0
- preditor/delayable_engine/delayables.py +85 -0
- preditor/enum.py +728 -0
- preditor/excepthooks.py +131 -0
- preditor/gui/__init__.py +93 -0
- preditor/gui/app.py +160 -0
- preditor/gui/codehighlighter.py +209 -0
- preditor/gui/completer.py +226 -0
- preditor/gui/console.py +867 -0
- preditor/gui/dialog.py +178 -0
- preditor/gui/drag_tab_bar.py +190 -0
- preditor/gui/editor_chooser.py +57 -0
- preditor/gui/errordialog.py +68 -0
- preditor/gui/find_files.py +125 -0
- preditor/gui/fuzzy_search/__init__.py +0 -0
- preditor/gui/fuzzy_search/fuzzy_search.py +93 -0
- preditor/gui/group_tab_widget/__init__.py +325 -0
- preditor/gui/group_tab_widget/grouped_tab_menu.py +35 -0
- preditor/gui/group_tab_widget/grouped_tab_models.py +108 -0
- preditor/gui/group_tab_widget/grouped_tab_widget.py +78 -0
- preditor/gui/group_tab_widget/one_tab_widget.py +54 -0
- preditor/gui/level_buttons.py +343 -0
- preditor/gui/logger_window_handler.py +48 -0
- preditor/gui/logger_window_plugin.py +32 -0
- preditor/gui/loggerwindow.py +1385 -0
- preditor/gui/newtabwidget.py +69 -0
- preditor/gui/set_text_editor_path_dialog.py +59 -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 +1105 -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 +389 -0
- preditor/gui/workbox_text_edit.py +137 -0
- preditor/gui/workboxwidget.py +298 -0
- preditor/logging_config.py +52 -0
- preditor/osystem.py +401 -0
- preditor/plugins.py +118 -0
- preditor/prefs.py +74 -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/settings.ini +25 -0
- preditor/resource/stylesheet/Bright.css +65 -0
- preditor/resource/stylesheet/Dark.css +199 -0
- preditor/scintilla/__init__.py +22 -0
- preditor/scintilla/delayables/__init__.py +11 -0
- preditor/scintilla/delayables/smart_highlight.py +94 -0
- preditor/scintilla/delayables/spell_check.py +173 -0
- preditor/scintilla/documenteditor.py +2038 -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 +21 -0
- preditor/scintilla/lexers/javascriptlexer.py +25 -0
- preditor/scintilla/lexers/maxscriptlexer.py +234 -0
- preditor/scintilla/lexers/mellexer.py +368 -0
- preditor/scintilla/lexers/mulexer.py +32 -0
- preditor/scintilla/lexers/pythonlexer.py +41 -0
- preditor/scintilla/ui/finddialog.ui +160 -0
- preditor/settings.py +71 -0
- preditor/stream/__init__.py +80 -0
- preditor/stream/director.py +73 -0
- preditor/stream/manager.py +74 -0
- preditor/streamhandler_helper.py +46 -0
- preditor/utils/__init__.py +0 -0
- preditor/utils/cute.py +30 -0
- preditor/utils/stylesheets.py +54 -0
- preditor/utils/text_search.py +342 -0
- preditor/version.py +21 -0
- preditor/weakref.py +363 -0
- preditor-1.0.0.dist-info/METADATA +224 -0
- preditor-1.0.0.dist-info/RECORD +158 -0
- preditor-1.0.0.dist-info/WHEEL +5 -0
- preditor-1.0.0.dist-info/entry_points.txt +18 -0
- preditor-1.0.0.dist-info/licenses/LICENSE +165 -0
- 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)
|