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,625 @@
|
|
|
1
|
+
from __future__ import absolute_import
|
|
2
|
+
|
|
3
|
+
from enum import IntEnum
|
|
4
|
+
from functools import partial
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import six
|
|
8
|
+
from Qt.QtCore import QByteArray, QMimeData, QPoint, QRect, Qt
|
|
9
|
+
from Qt.QtGui import QColor, QCursor, QDrag, QPixmap, QRegion
|
|
10
|
+
from Qt.QtWidgets import (
|
|
11
|
+
QApplication,
|
|
12
|
+
QFileDialog,
|
|
13
|
+
QInputDialog,
|
|
14
|
+
QMenu,
|
|
15
|
+
QSizePolicy,
|
|
16
|
+
QTabBar,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from preditor import osystem
|
|
20
|
+
|
|
21
|
+
from ..gui import handleMenuHovered
|
|
22
|
+
from ..utils import Truncate
|
|
23
|
+
from ..utils.cute import QtPropertyInit
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TabStates(IntEnum):
|
|
27
|
+
"""Nice names for the Tab states for coloring"""
|
|
28
|
+
|
|
29
|
+
Normal = 0
|
|
30
|
+
Linked = 1
|
|
31
|
+
Changed = 2
|
|
32
|
+
ChangedLinked = 3
|
|
33
|
+
Orphaned = 4
|
|
34
|
+
OrphanedLinked = 5
|
|
35
|
+
Dirty = 6
|
|
36
|
+
DirtyLinked = 7
|
|
37
|
+
MissingLinked = 8
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DragTabBar(QTabBar):
|
|
41
|
+
"""A QTabBar that allows you to drag and drop its tabs to other DragTabBar's
|
|
42
|
+
while still allowing you to move tabs normally.
|
|
43
|
+
|
|
44
|
+
In most cases you should use `install_tab_widget` to create and add this TabBar
|
|
45
|
+
to a QTabWidget. It takes care of enabling usability features of QTabWidget's.
|
|
46
|
+
|
|
47
|
+
Based on code by ARussel: https://forum.qt.io/post/420469
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
# the normalColor is set to an invalid color name. When QTabBar.setTabTextColor
|
|
52
|
+
# is called with an invalid color name, it reverts to the QTabBar foreground
|
|
53
|
+
# role instead
|
|
54
|
+
normalColor = QColor("invalidcolor")
|
|
55
|
+
|
|
56
|
+
# These Qt Properties can be customized using style sheets.
|
|
57
|
+
linkedColor = QtPropertyInit('_linkedColor', QColor("grey"))
|
|
58
|
+
missingLinkedColor = QtPropertyInit('_missingLinkedColor', QColor("grey"))
|
|
59
|
+
dirtyColor = QtPropertyInit('_dirtyColor', QColor("grey"))
|
|
60
|
+
dirtyLinkedColor = QtPropertyInit('_dirtyLinkedColor', QColor("grey"))
|
|
61
|
+
changedColor = QtPropertyInit('_changedColor', QColor("grey"))
|
|
62
|
+
changedLinkedColor = QtPropertyInit('_changedLinkedColor', QColor("grey"))
|
|
63
|
+
orphanedColor = QtPropertyInit('_orphanedColor', QColor("grey"))
|
|
64
|
+
orphanedLinkedColor = QtPropertyInit('_orphanedLinkedColor', QColor("grey"))
|
|
65
|
+
|
|
66
|
+
def __init__(self, parent=None, mime_type='DragTabBar'):
|
|
67
|
+
super(DragTabBar, self).__init__(parent=parent)
|
|
68
|
+
self.setAcceptDrops(True)
|
|
69
|
+
self.setMouseTracking(True)
|
|
70
|
+
self._mime_data = None
|
|
71
|
+
self._context_menu_tab = -1
|
|
72
|
+
self.mime_type = mime_type
|
|
73
|
+
|
|
74
|
+
self.fg_color_map = {}
|
|
75
|
+
self.bg_color_map = {}
|
|
76
|
+
|
|
77
|
+
def updateColorMap(self):
|
|
78
|
+
"""This cannot be called during __init__, otherwise all bg colors will
|
|
79
|
+
be default, and not read from the style sheet. So instead, the first
|
|
80
|
+
time we need self.bg_color_map, we check if it has values, and call this
|
|
81
|
+
method if it doesn't.
|
|
82
|
+
"""
|
|
83
|
+
self.bg_color_map = {
|
|
84
|
+
TabStates.Normal: self.normalColor,
|
|
85
|
+
TabStates.Changed: self.changedColor,
|
|
86
|
+
TabStates.ChangedLinked: self.changedLinkedColor,
|
|
87
|
+
TabStates.Orphaned: self.orphanedColor,
|
|
88
|
+
TabStates.OrphanedLinked: self.orphanedLinkedColor,
|
|
89
|
+
TabStates.Dirty: self.dirtyColor,
|
|
90
|
+
TabStates.DirtyLinked: self.dirtyLinkedColor,
|
|
91
|
+
TabStates.Linked: self.linkedColor,
|
|
92
|
+
TabStates.MissingLinked: self.missingLinkedColor,
|
|
93
|
+
}
|
|
94
|
+
self.fg_color_map = {
|
|
95
|
+
"0": "white",
|
|
96
|
+
"1": "black",
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
def getColorAndToolTip(self, index):
|
|
100
|
+
"""Determine the color and tooltip based on the state of the workbox.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
index (int): The index of the tab holding the workbox
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
color, toolTip (QColor, str): The QColor and toolTip string to apply
|
|
107
|
+
to the tab being painted
|
|
108
|
+
"""
|
|
109
|
+
state = TabStates.Normal
|
|
110
|
+
toolTip = ""
|
|
111
|
+
if self.parent():
|
|
112
|
+
widget = self.parent().widget(index)
|
|
113
|
+
|
|
114
|
+
filename = None
|
|
115
|
+
if hasattr(widget, "__filename__"):
|
|
116
|
+
filename = widget.__filename__()
|
|
117
|
+
|
|
118
|
+
if widget.__changed_by_instance__():
|
|
119
|
+
if filename:
|
|
120
|
+
state = TabStates.ChangedLinked
|
|
121
|
+
toolTip = (
|
|
122
|
+
"Linked workbox has been updated by saving in another "
|
|
123
|
+
"PrEditor and has had unsaved changes auto-saved to a"
|
|
124
|
+
" previous version.\nAccess with Ctrl-Alt-[ shortcut."
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
state = TabStates.Changed
|
|
128
|
+
toolTip = (
|
|
129
|
+
"Workbox has been updated by saving in another PrEditor "
|
|
130
|
+
"instance, and has had it's unsaved changes auto-saved to "
|
|
131
|
+
"a previous version.\nAccess with Ctrl-Alt-[ shortcut."
|
|
132
|
+
)
|
|
133
|
+
elif widget.__orphaned_by_instance__():
|
|
134
|
+
if filename:
|
|
135
|
+
state = TabStates.OrphanedLinked
|
|
136
|
+
toolTip = (
|
|
137
|
+
"Linked workbox is either newly added, or orphaned by "
|
|
138
|
+
"being removed in another PrEditor instance and saved."
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
state = TabStates.Orphaned
|
|
142
|
+
toolTip = (
|
|
143
|
+
"Workbox is either newly added, or orphaned by "
|
|
144
|
+
"being removed in another PrEditor instance and saved."
|
|
145
|
+
)
|
|
146
|
+
elif widget.__is_dirty__():
|
|
147
|
+
if filename:
|
|
148
|
+
state = TabStates.DirtyLinked
|
|
149
|
+
toolTip = "Linked workbox has unsaved changes."
|
|
150
|
+
else:
|
|
151
|
+
state = TabStates.Dirty
|
|
152
|
+
toolTip = "Workbox has unsaved changes, or it's name has changed."
|
|
153
|
+
elif widget.__is_missing_linked_file__():
|
|
154
|
+
state = TabStates.MissingLinked
|
|
155
|
+
toolTip = "Linked file is missing"
|
|
156
|
+
elif hasattr(widget, "__filename__") and widget.__filename__():
|
|
157
|
+
state = TabStates.Linked
|
|
158
|
+
toolTip = "Linked to file on disk"
|
|
159
|
+
|
|
160
|
+
if hasattr(widget, "__filename__"):
|
|
161
|
+
filename = widget.__filename__()
|
|
162
|
+
if filename:
|
|
163
|
+
toolTip += "\nfilename: {}".format(filename)
|
|
164
|
+
|
|
165
|
+
window = self.window()
|
|
166
|
+
if window.uiExtraTooltipInfoCHK.isChecked():
|
|
167
|
+
|
|
168
|
+
if hasattr(widget, "__workbox_id__"):
|
|
169
|
+
workbox_id = widget.__workbox_id__()
|
|
170
|
+
if toolTip:
|
|
171
|
+
toolTip += "\n\n"
|
|
172
|
+
toolTip += workbox_id
|
|
173
|
+
|
|
174
|
+
toolTip += "\nis dirty: {}".format(widget.__is_dirty__())
|
|
175
|
+
toolTip += "\nstate: {}".format(state.name)
|
|
176
|
+
|
|
177
|
+
if hasattr(widget, "__backup_file__"):
|
|
178
|
+
backup_file = widget.__backup_file__()
|
|
179
|
+
toolTip += "\nBackup file: {}".format(backup_file)
|
|
180
|
+
|
|
181
|
+
if hasattr(widget, "_changed_by_instance"):
|
|
182
|
+
_changed_by_instance = widget._changed_by_instance
|
|
183
|
+
toolTip += "\nHas been changed by instance: {}".format(
|
|
184
|
+
_changed_by_instance
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if hasattr(widget, "__orphaned_by_instance__"):
|
|
188
|
+
__orphaned_by_instance__ = widget.__orphaned_by_instance__()
|
|
189
|
+
toolTip += "\n __orphaned_by_instance__: {}".format(
|
|
190
|
+
__orphaned_by_instance__
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if hasattr(widget, "_changed_saved"):
|
|
194
|
+
_changed_saved = widget._changed_saved
|
|
195
|
+
toolTip += "\n _changed_saved: {}".format(_changed_saved)
|
|
196
|
+
|
|
197
|
+
if hasattr(widget, "__last_workbox_name__"):
|
|
198
|
+
last_workbox_name = widget.__last_workbox_name__()
|
|
199
|
+
toolTip += "\nlast_workbox_name: {}".format(last_workbox_name)
|
|
200
|
+
|
|
201
|
+
if hasattr(widget, "__last_saved_text__"):
|
|
202
|
+
last_saved_text = widget.__last_saved_text__()
|
|
203
|
+
last_saved_text = Truncate(last_saved_text).lines()
|
|
204
|
+
toolTip += "\nlast_saved_text: \n{}".format(last_saved_text)
|
|
205
|
+
|
|
206
|
+
color = self.bg_color_map.get(state)
|
|
207
|
+
return color, toolTip
|
|
208
|
+
|
|
209
|
+
def updateColorAndToolTip(self, index):
|
|
210
|
+
"""Update the color and tooltip for the tab at index, based of various
|
|
211
|
+
factors about the workbox.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
index (int): The index of the tab to color, and possibly, set toolTip
|
|
215
|
+
"""
|
|
216
|
+
self.updateColorMap()
|
|
217
|
+
|
|
218
|
+
color, toolTip = self.getColorAndToolTip(index)
|
|
219
|
+
self.setTabTextColor(index, color)
|
|
220
|
+
self.setTabToolTip(index, toolTip)
|
|
221
|
+
|
|
222
|
+
def updateColorsAndToolTips(self):
|
|
223
|
+
"""Update the color and tooltip for all the tabs in this tabBar. Also,
|
|
224
|
+
update the tabBar above it.
|
|
225
|
+
"""
|
|
226
|
+
for index in range(self.count()):
|
|
227
|
+
self.updateColorAndToolTip(index)
|
|
228
|
+
|
|
229
|
+
parentIdx = self.window().indexOfWorkboxOrTabGroup(self.parent())
|
|
230
|
+
if parentIdx is not None:
|
|
231
|
+
tabBar = self.parent().__tab_widget__().tabBar()
|
|
232
|
+
tabBar.updateColorAndToolTip(parentIdx)
|
|
233
|
+
|
|
234
|
+
def mouseMoveEvent(self, event): # noqa: N802
|
|
235
|
+
if not self._mime_data:
|
|
236
|
+
return super(DragTabBar, self).mouseMoveEvent(event)
|
|
237
|
+
|
|
238
|
+
# Check if the mouse has moved outside of the widget, if not, let
|
|
239
|
+
# the QTabBar handle the internal tab movement.
|
|
240
|
+
event_pos = event.pos()
|
|
241
|
+
global_pos = self.mapToGlobal(event_pos)
|
|
242
|
+
bar_geo = QRect(self.mapToGlobal(self.pos()), self.size())
|
|
243
|
+
inside = bar_geo.contains(global_pos)
|
|
244
|
+
if inside:
|
|
245
|
+
return super(DragTabBar, self).mouseMoveEvent(event)
|
|
246
|
+
|
|
247
|
+
# The user has moved the tab outside of the QTabBar, remove the tab from
|
|
248
|
+
# this tab bar and store it in the MimeData, initiating a drag event.
|
|
249
|
+
widget = self._mime_data.property('widget')
|
|
250
|
+
tab_index = self.parentWidget().indexOf(widget)
|
|
251
|
+
self.parentWidget().removeTab(tab_index)
|
|
252
|
+
pos_in_tab = self.mapFromGlobal(global_pos)
|
|
253
|
+
drag = QDrag(self)
|
|
254
|
+
drag.setMimeData(self._mime_data)
|
|
255
|
+
drag.setPixmap(self._mime_data.imageData())
|
|
256
|
+
drag.setHotSpot(event_pos - pos_in_tab)
|
|
257
|
+
cursor = QCursor(Qt.CursorShape.OpenHandCursor)
|
|
258
|
+
drag.setDragCursor(cursor.pixmap(), Qt.DropAction.MoveAction)
|
|
259
|
+
action = drag.exec(Qt.DropAction.MoveAction)
|
|
260
|
+
# If the user didn't successfully add this to a new tab widget, restore
|
|
261
|
+
# the tab to the original location.
|
|
262
|
+
if action == Qt.DropAction.IgnoreAction:
|
|
263
|
+
original_tab_index = self._mime_data.property('original_tab_index')
|
|
264
|
+
self.parentWidget().insertTab(
|
|
265
|
+
original_tab_index, widget, self._mime_data.text()
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
self._mime_data = None
|
|
269
|
+
|
|
270
|
+
def mousePressEvent(self, event): # noqa: N802
|
|
271
|
+
if event.button() == Qt.MouseButton.LeftButton and not self._mime_data:
|
|
272
|
+
tab_index = self.tabAt(event.pos())
|
|
273
|
+
|
|
274
|
+
# While we don't remove the tab on mouse press, capture its tab image
|
|
275
|
+
# and attach it to the mouse. This also stores info needed to handle
|
|
276
|
+
# moving the tab to a new QTabWidget, and undoing the move if the
|
|
277
|
+
# user cancels the drop.
|
|
278
|
+
tab_rect = self.tabRect(tab_index)
|
|
279
|
+
pixmap = QPixmap(tab_rect.size())
|
|
280
|
+
self.render(pixmap, QPoint(), QRegion(tab_rect))
|
|
281
|
+
|
|
282
|
+
self._mime_data = QMimeData()
|
|
283
|
+
self._mime_data.setData(self.mime_type, QByteArray())
|
|
284
|
+
self._mime_data.setText(self.tabText(tab_index))
|
|
285
|
+
self._mime_data.setProperty('original_tab_index', tab_index)
|
|
286
|
+
self._mime_data.setImageData(pixmap)
|
|
287
|
+
widget = self.parentWidget().widget(tab_index)
|
|
288
|
+
self._mime_data.setProperty('widget', widget)
|
|
289
|
+
|
|
290
|
+
# By default if there are no tabs, the tab bar is hidden. This
|
|
291
|
+
# prevents users from re-adding tabs to the tab bar as only it
|
|
292
|
+
# accepts the tab drops. This preserves the tab bar height
|
|
293
|
+
# after it was drawn with a tab so it should automatically stay
|
|
294
|
+
# the same visual height.
|
|
295
|
+
if not self.minimumHeight():
|
|
296
|
+
self.setMinimumHeight(self.height())
|
|
297
|
+
|
|
298
|
+
super(DragTabBar, self).mousePressEvent(event)
|
|
299
|
+
|
|
300
|
+
def mouseReleaseEvent(self, event): # noqa: N802
|
|
301
|
+
self._mime_data = None
|
|
302
|
+
super(DragTabBar, self).mouseReleaseEvent(event)
|
|
303
|
+
|
|
304
|
+
def dragEnterEvent(self, event): # noqa: N802
|
|
305
|
+
# if event.mimeData().hasFormat(self.mime_type):
|
|
306
|
+
event.accept()
|
|
307
|
+
|
|
308
|
+
def dragLeaveEvent(self, event): # noqa: N802
|
|
309
|
+
event.accept()
|
|
310
|
+
|
|
311
|
+
def dragMoveEvent(self, event): # noqa: N802
|
|
312
|
+
# If this is not a tab of the same mime type, make the tab under the mouse
|
|
313
|
+
# the current tab so users can easily drop inside that tab.
|
|
314
|
+
if not event.mimeData().hasFormat(self.mime_type):
|
|
315
|
+
event.accept()
|
|
316
|
+
tab_index = self.tabAt(event.pos())
|
|
317
|
+
if tab_index == -1:
|
|
318
|
+
tab_index = self.count() - 1
|
|
319
|
+
if self.currentIndex() != tab_index:
|
|
320
|
+
self.setCurrentIndex(tab_index)
|
|
321
|
+
|
|
322
|
+
def dropEvent(self, event): # noqa: N802
|
|
323
|
+
if not event.mimeData().hasFormat(self.mime_type):
|
|
324
|
+
return
|
|
325
|
+
if event.source().parentWidget() == self:
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
event.setDropAction(Qt.DropAction.MoveAction)
|
|
329
|
+
event.accept()
|
|
330
|
+
counter = self.count()
|
|
331
|
+
|
|
332
|
+
mime_data = event.mimeData()
|
|
333
|
+
if counter == 0:
|
|
334
|
+
self.parent().addTab(mime_data.property('widget'), mime_data.text())
|
|
335
|
+
else:
|
|
336
|
+
self.parent().insertTab(
|
|
337
|
+
counter + 1, mime_data.property('widget'), mime_data.text()
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
def rename_tab(self):
|
|
341
|
+
"""Used by the tab_menu to rename the tab at index `_context_menu_tab`."""
|
|
342
|
+
if self._context_menu_tab != -1:
|
|
343
|
+
current = self.tabText(self._context_menu_tab)
|
|
344
|
+
msg = 'Rename the {} tab to (new name must be unique):'.format(current)
|
|
345
|
+
|
|
346
|
+
name, success = QInputDialog.getText(self, 'Rename Tab', msg, text=current)
|
|
347
|
+
name = self.parent().get_next_available_tab_name(name)
|
|
348
|
+
if not name.strip():
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
if success:
|
|
352
|
+
self.setTabText(self._context_menu_tab, name)
|
|
353
|
+
self.updateColorsAndToolTips()
|
|
354
|
+
|
|
355
|
+
def tab_menu(self, pos, popup=True):
|
|
356
|
+
"""Creates the custom context menu for the tab bar. To customize the menu
|
|
357
|
+
call super setting `popup=False`. This will return the menu for
|
|
358
|
+
customization and you will then need to call popup on the menu.
|
|
359
|
+
|
|
360
|
+
This method sets the tab index the user right clicked on in the variable
|
|
361
|
+
`_context_menu_tab`. This can be used in the triggered QAction methods."""
|
|
362
|
+
|
|
363
|
+
index = self.tabAt(pos)
|
|
364
|
+
self._context_menu_tab = index
|
|
365
|
+
if self._context_menu_tab == -1:
|
|
366
|
+
return
|
|
367
|
+
menu = QMenu(self)
|
|
368
|
+
menu.setFont(self.window().font())
|
|
369
|
+
menu.hovered.connect(handleMenuHovered)
|
|
370
|
+
|
|
371
|
+
grouped_tab = self.parentWidget()
|
|
372
|
+
workbox = grouped_tab.widget(self._context_menu_tab)
|
|
373
|
+
|
|
374
|
+
# Show File-related actions depending if filename already set. Don't include
|
|
375
|
+
# Rename if the workbox is linked to a file.
|
|
376
|
+
if hasattr(workbox, '__filename__'):
|
|
377
|
+
if not workbox.__filename__():
|
|
378
|
+
act = menu.addAction('Rename')
|
|
379
|
+
tip = "Rename this tab."
|
|
380
|
+
act.setToolTip(tip)
|
|
381
|
+
act.triggered.connect(self.rename_tab)
|
|
382
|
+
|
|
383
|
+
act = menu.addAction('Link File')
|
|
384
|
+
tip = (
|
|
385
|
+
"Choose an existing file on disk to link this workbox to.\n"
|
|
386
|
+
"The current workbox contents will be replaced, but will be "
|
|
387
|
+
"backed up, accessible with Ctrl+Alt+Left"
|
|
388
|
+
)
|
|
389
|
+
act.setToolTip(tip)
|
|
390
|
+
act.triggered.connect(partial(self.link_file, workbox))
|
|
391
|
+
|
|
392
|
+
act = menu.addAction('Save and Link File')
|
|
393
|
+
tip = (
|
|
394
|
+
"Choose a filename to save the workbox contents to, and "
|
|
395
|
+
"link to that file."
|
|
396
|
+
)
|
|
397
|
+
act.setToolTip(tip)
|
|
398
|
+
act.triggered.connect(partial(self.save_and_link_file, workbox))
|
|
399
|
+
else:
|
|
400
|
+
if Path(workbox.__filename__()).is_file():
|
|
401
|
+
act = menu.addAction('Explore File')
|
|
402
|
+
tip = "Open a file explorer at the linked file's location"
|
|
403
|
+
act.setToolTip(tip)
|
|
404
|
+
act.triggered.connect(partial(self.explore_file, workbox))
|
|
405
|
+
|
|
406
|
+
act = menu.addAction('Unlink File')
|
|
407
|
+
tip = (
|
|
408
|
+
"Disconnect the link to the file on disk. The workbox "
|
|
409
|
+
"contents will remain unchanged."
|
|
410
|
+
)
|
|
411
|
+
act.setToolTip(tip)
|
|
412
|
+
act.triggered.connect(partial(self.unlink_file, workbox))
|
|
413
|
+
|
|
414
|
+
act = menu.addAction('Save As')
|
|
415
|
+
tip = (
|
|
416
|
+
"Save contents as a new file and link to it. If you "
|
|
417
|
+
"choose an existing file, that file's contents will be "
|
|
418
|
+
"overwritten."
|
|
419
|
+
)
|
|
420
|
+
act.setToolTip(tip)
|
|
421
|
+
act.triggered.connect(partial(self.save_and_link_file, workbox))
|
|
422
|
+
|
|
423
|
+
act = menu.addAction('Copy Filename')
|
|
424
|
+
tip = "Copy this workbox's filename to the clipboard."
|
|
425
|
+
act.setToolTip(tip)
|
|
426
|
+
act.triggered.connect(partial(self.copyFilename, workbox))
|
|
427
|
+
else:
|
|
428
|
+
act = menu.addAction('Explore File')
|
|
429
|
+
tip = "Open a file explorer at the linked file's location"
|
|
430
|
+
act.setToolTip(tip)
|
|
431
|
+
act.triggered.connect(partial(self.explore_file, workbox))
|
|
432
|
+
|
|
433
|
+
act = menu.addAction('Save As and Link File')
|
|
434
|
+
tip = (
|
|
435
|
+
"Save contents as a new file and link to it. If you "
|
|
436
|
+
"choose an existing file, that file's contents will be "
|
|
437
|
+
"overwritten."
|
|
438
|
+
)
|
|
439
|
+
act.setToolTip(tip)
|
|
440
|
+
act.triggered.connect(partial(self.save_and_link_file, workbox))
|
|
441
|
+
|
|
442
|
+
act = menu.addAction('Relink File')
|
|
443
|
+
tip = (
|
|
444
|
+
"Current linked file is not found. Choose a new file "
|
|
445
|
+
"to link to."
|
|
446
|
+
)
|
|
447
|
+
act.setToolTip(tip)
|
|
448
|
+
act.triggered.connect(
|
|
449
|
+
partial(self.link_file, workbox, saveLinkedFile=False)
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
act = menu.addAction('Unlink File')
|
|
453
|
+
tip = (
|
|
454
|
+
"Disconnect the link to the file on disk. The workbox "
|
|
455
|
+
"contents will remain unchanged."
|
|
456
|
+
)
|
|
457
|
+
act.setToolTip(tip)
|
|
458
|
+
act.triggered.connect(partial(self.unlink_file, workbox))
|
|
459
|
+
else:
|
|
460
|
+
act = menu.addAction('Rename')
|
|
461
|
+
tip = "Rename this tab."
|
|
462
|
+
act.setToolTip(tip)
|
|
463
|
+
act.triggered.connect(self.rename_tab)
|
|
464
|
+
|
|
465
|
+
act = menu.addAction('Copy Workbox Name')
|
|
466
|
+
tip = "Copy this workbox's name to the clipboard."
|
|
467
|
+
act.setToolTip(tip)
|
|
468
|
+
act.triggered.connect(partial(self.copy_workbox_name, workbox, index))
|
|
469
|
+
|
|
470
|
+
act = menu.addAction('Copy Workbox Id')
|
|
471
|
+
tip = "Copy this workbox's id to the clipboard."
|
|
472
|
+
act.setToolTip(tip)
|
|
473
|
+
act.triggered.connect(partial(self.copy_workbox_id, workbox, index))
|
|
474
|
+
|
|
475
|
+
if popup:
|
|
476
|
+
menu.popup(self.mapToGlobal(pos))
|
|
477
|
+
|
|
478
|
+
return menu
|
|
479
|
+
|
|
480
|
+
def link_file(self, workbox, saveLinkedFile=True):
|
|
481
|
+
"""Link the given workbox to a file on disk.
|
|
482
|
+
|
|
483
|
+
Args:
|
|
484
|
+
workbox (WorkboxMixin): The workbox contained in the clicked tab
|
|
485
|
+
"""
|
|
486
|
+
filename = workbox.__filename__()
|
|
487
|
+
filename, _other = QFileDialog.getOpenFileName(directory=filename)
|
|
488
|
+
if filename and Path(filename).is_file():
|
|
489
|
+
workbox.__set_file_monitoring_enabled__(False)
|
|
490
|
+
|
|
491
|
+
# First, save any unsaved text
|
|
492
|
+
workbox.__save_prefs__(saveLinkedFile=saveLinkedFile)
|
|
493
|
+
|
|
494
|
+
# Now, load file
|
|
495
|
+
workbox.__load__(filename)
|
|
496
|
+
workbox.__set_filename__(filename)
|
|
497
|
+
workbox.__set_file_monitoring_enabled__(True)
|
|
498
|
+
|
|
499
|
+
name = Path(filename).name
|
|
500
|
+
self.setTabText(self._context_menu_tab, name)
|
|
501
|
+
self.updateColorsAndToolTips()
|
|
502
|
+
self.update()
|
|
503
|
+
self.window().setWorkboxFontBasedOnConsole(workbox=workbox)
|
|
504
|
+
|
|
505
|
+
workbox.__save_prefs__(saveLinkedFile=False, force=True)
|
|
506
|
+
|
|
507
|
+
def save_and_link_file(self, workbox):
|
|
508
|
+
"""Save the given workbox as a file on disk, and link the workbox to it.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
workbox (WorkboxMixin): The workbox contained in the clicked tab
|
|
512
|
+
"""
|
|
513
|
+
filename = workbox.__filename__()
|
|
514
|
+
if filename and Path(filename).is_file():
|
|
515
|
+
workbox.__set_file_monitoring_enabled__(False)
|
|
516
|
+
directory = six.text_type(Path(filename).parent) if filename else ""
|
|
517
|
+
success = workbox.__save_as__(directory=directory)
|
|
518
|
+
if not success:
|
|
519
|
+
return
|
|
520
|
+
|
|
521
|
+
# Workbox
|
|
522
|
+
filename = workbox.__filename__()
|
|
523
|
+
workbox.__set_last_saved_text__(workbox.__text__())
|
|
524
|
+
workbox.__set_file_monitoring_enabled__(True)
|
|
525
|
+
name = Path(filename).name
|
|
526
|
+
|
|
527
|
+
self.setTabText(self._context_menu_tab, name)
|
|
528
|
+
self.updateColorsAndToolTips()
|
|
529
|
+
self.update()
|
|
530
|
+
self.window().setWorkboxFontBasedOnConsole(workbox=workbox)
|
|
531
|
+
workbox.__set_last_workbox_name__(workbox.__workbox_name__())
|
|
532
|
+
|
|
533
|
+
def explore_file(self, workbox):
|
|
534
|
+
"""Open a system file explorer at the path of the linked file.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
workbox (WorkboxMixin): The workbox contained in the clicked tab
|
|
538
|
+
"""
|
|
539
|
+
path = Path(workbox.__filename__())
|
|
540
|
+
if path.exists():
|
|
541
|
+
osystem.explore(str(path))
|
|
542
|
+
elif path.parent.exists():
|
|
543
|
+
osystem.explore(str(path.parent))
|
|
544
|
+
|
|
545
|
+
def unlink_file(self, workbox):
|
|
546
|
+
"""Disconnect a file link.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
workbox (WorkboxMixin): The workbox contained in the clicked tab
|
|
550
|
+
"""
|
|
551
|
+
workbox.__set_file_monitoring_enabled__(False)
|
|
552
|
+
workbox.__set_filename__("")
|
|
553
|
+
name = self.parent().default_title
|
|
554
|
+
self.setTabText(self._context_menu_tab, name)
|
|
555
|
+
self.updateColorsAndToolTips()
|
|
556
|
+
|
|
557
|
+
def copyFilename(self, workbox):
|
|
558
|
+
"""Copy the given workbox's filename to the clipboard
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
workbox (WorkboxMixin): The workbox for which to provide the filename
|
|
562
|
+
"""
|
|
563
|
+
filename = workbox.__filename__()
|
|
564
|
+
QApplication.clipboard().setText(filename)
|
|
565
|
+
|
|
566
|
+
def copy_workbox_name(self, workbox, index):
|
|
567
|
+
"""Copy the workbox name to clipboard.
|
|
568
|
+
|
|
569
|
+
Args:
|
|
570
|
+
workbox (WorkboxMixin): The workbox contained in the clicked tab
|
|
571
|
+
index (index): The index of the clicked tab
|
|
572
|
+
"""
|
|
573
|
+
try:
|
|
574
|
+
name = workbox.__workbox_name__()
|
|
575
|
+
except AttributeError:
|
|
576
|
+
group = self.parent().widget(index)
|
|
577
|
+
curIndex = group.currentIndex()
|
|
578
|
+
workbox = group.widget(curIndex)
|
|
579
|
+
name = workbox.__workbox_name__()
|
|
580
|
+
QApplication.clipboard().setText(name)
|
|
581
|
+
|
|
582
|
+
def copy_workbox_id(self, workbox, index):
|
|
583
|
+
"""Copy the workbox id to clipboard.
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
workbox (WorkboxMixin): The workbox contained in the clicked tab
|
|
587
|
+
index (index): The index of the clicked tab
|
|
588
|
+
"""
|
|
589
|
+
try:
|
|
590
|
+
workbox_id = workbox.__workbox_id__()
|
|
591
|
+
except AttributeError:
|
|
592
|
+
group = self.parent().widget(index)
|
|
593
|
+
curIndex = group.currentIndex()
|
|
594
|
+
workbox = group.widget(curIndex)
|
|
595
|
+
workbox_id = workbox.__workbox_id__()
|
|
596
|
+
QApplication.clipboard().setText(workbox_id)
|
|
597
|
+
|
|
598
|
+
@classmethod
|
|
599
|
+
def install_tab_widget(cls, tab_widget, mime_type='DragTabBar', menu=True):
|
|
600
|
+
"""Creates and returns a instance of DragTabBar and installs it on the
|
|
601
|
+
QTabWidget. This enables movable tabs, and enables document mode.
|
|
602
|
+
Document mode makes the tab bar expand to the size of the QTabWidget so
|
|
603
|
+
drag drop operations are more intuitive.
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
tab_widget (QTabWidget): The QTabWidget to install the tab bar on.
|
|
607
|
+
mime_data (str, optional): This TabBar will only accept tab drop
|
|
608
|
+
operations with this mime type.
|
|
609
|
+
menu (bool, optional): Install a custom context menu on the bar bar.
|
|
610
|
+
Override `tab_menu` to customize the menu.
|
|
611
|
+
"""
|
|
612
|
+
bar = cls(tab_widget, mime_type=mime_type)
|
|
613
|
+
tab_widget.setTabBar(bar)
|
|
614
|
+
tab_widget.setMovable(True)
|
|
615
|
+
tab_widget.setDocumentMode(True)
|
|
616
|
+
|
|
617
|
+
sizePolicy = tab_widget.sizePolicy()
|
|
618
|
+
sizePolicy.setVerticalPolicy(QSizePolicy.Policy.Preferred)
|
|
619
|
+
tab_widget.setSizePolicy(sizePolicy)
|
|
620
|
+
|
|
621
|
+
if menu:
|
|
622
|
+
bar.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
623
|
+
bar.customContextMenuRequested.connect(bar.tab_menu)
|
|
624
|
+
|
|
625
|
+
return bar
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import absolute_import
|
|
2
|
+
|
|
3
|
+
from Qt.QtCore import Slot
|
|
4
|
+
from Qt.QtGui import QIcon
|
|
5
|
+
from Qt.QtWidgets import QWidget
|
|
6
|
+
|
|
7
|
+
from .. import plugins, resourcePath
|
|
8
|
+
from ..gui import loadUi
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EditorChooser(QWidget):
|
|
12
|
+
"""A widget that lets the user choose from a a list of available editors."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, parent=None, editor_name=None):
|
|
15
|
+
super(EditorChooser, self).__init__(parent=parent)
|
|
16
|
+
loadUi(__file__, self)
|
|
17
|
+
icon = QIcon(resourcePath('img/warning-big.png'))
|
|
18
|
+
self.uiWarningIconLBL.setPixmap(icon.pixmap(icon.availableSizes()[0]))
|
|
19
|
+
if editor_name:
|
|
20
|
+
self.set_editor_name(editor_name)
|
|
21
|
+
|
|
22
|
+
def editor_name(self):
|
|
23
|
+
return self.uiWorkboxEditorDDL.currentText()
|
|
24
|
+
|
|
25
|
+
def set_editor_name(self, name):
|
|
26
|
+
index = self.uiWorkboxEditorDDL.findText(name)
|
|
27
|
+
if index == -1:
|
|
28
|
+
self.uiWorkboxEditorDDL.addItem(name)
|
|
29
|
+
index = self.uiWorkboxEditorDDL.findText(name)
|
|
30
|
+
self.uiWorkboxEditorDDL.setCurrentIndex(index)
|
|
31
|
+
|
|
32
|
+
@Slot()
|
|
33
|
+
def refresh(self):
|
|
34
|
+
warning = "Choose an editor to enable Workboxs."
|
|
35
|
+
editor_name = self.editor_name()
|
|
36
|
+
if editor_name:
|
|
37
|
+
_, editor = plugins.editor(editor_name)
|
|
38
|
+
warning = editor._warning_text
|
|
39
|
+
self.uiWarningIconLBL.setVisible(bool(warning))
|
|
40
|
+
self.uiWarningTextLBL.setVisible(bool(warning))
|
|
41
|
+
self.uiWarningTextLBL.setText(warning)
|
|
42
|
+
|
|
43
|
+
def refresh_editors(self):
|
|
44
|
+
current = self.editor_name()
|
|
45
|
+
self.uiWorkboxEditorDDL.blockSignals(True)
|
|
46
|
+
self.uiWorkboxEditorDDL.clear()
|
|
47
|
+
for name, _ in sorted(set(plugins.editors())):
|
|
48
|
+
self.uiWorkboxEditorDDL.addItem(name)
|
|
49
|
+
|
|
50
|
+
self.uiWorkboxEditorDDL.setCurrentIndex(
|
|
51
|
+
self.uiWorkboxEditorDDL.findText(current)
|
|
52
|
+
)
|
|
53
|
+
self.uiWorkboxEditorDDL.blockSignals(False)
|
|
54
|
+
|
|
55
|
+
def showEvent(self, event): # noqa: N802
|
|
56
|
+
super(EditorChooser, self).showEvent(event)
|
|
57
|
+
self.refresh_editors()
|