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,223 @@
|
|
|
1
|
+
from __future__ import absolute_import
|
|
2
|
+
|
|
3
|
+
from Qt.QtCore import Qt
|
|
4
|
+
from Qt.QtWidgets import QMessageBox, QToolButton
|
|
5
|
+
|
|
6
|
+
from ...prefs import VersionTypes
|
|
7
|
+
from ..drag_tab_bar import DragTabBar
|
|
8
|
+
from ..workbox_text_edit import WorkboxTextEdit
|
|
9
|
+
from .one_tab_widget import OneTabWidget
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GroupedTabWidget(OneTabWidget):
|
|
13
|
+
def __init__(self, editor_kwargs, editor_cls=None, core_name=None, *args, **kwargs):
|
|
14
|
+
super(GroupedTabWidget, self).__init__(*args, **kwargs)
|
|
15
|
+
DragTabBar.install_tab_widget(self, 'grouped_tab_widget')
|
|
16
|
+
self.editor_kwargs = editor_kwargs
|
|
17
|
+
if editor_cls is None:
|
|
18
|
+
editor_cls = WorkboxTextEdit
|
|
19
|
+
self.editor_cls = editor_cls
|
|
20
|
+
self.core_name = core_name
|
|
21
|
+
self.currentChanged.connect(self.tab_shown)
|
|
22
|
+
|
|
23
|
+
self.uiCornerBTN = QToolButton(self)
|
|
24
|
+
self.uiCornerBTN.setText('+')
|
|
25
|
+
self.uiCornerBTN.released.connect(lambda: self.add_new_editor())
|
|
26
|
+
self.setCornerWidget(self.uiCornerBTN, Qt.Corner.TopRightCorner)
|
|
27
|
+
|
|
28
|
+
self.default_title = "Workbox01"
|
|
29
|
+
|
|
30
|
+
def __tab_widget__(self):
|
|
31
|
+
"""Return the tab widget which contains this group
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
GroupTabWidget: The tab widget which contains this group
|
|
35
|
+
"""
|
|
36
|
+
return self.parent().parent()
|
|
37
|
+
|
|
38
|
+
def __changed_by_instance__(self):
|
|
39
|
+
"""Returns if any of this groups editors have been changed by another
|
|
40
|
+
PrEditor instance's prefs save.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
changed (bool)
|
|
44
|
+
"""
|
|
45
|
+
changed = False
|
|
46
|
+
for workbox_idx in range(self.count()):
|
|
47
|
+
workbox = self.widget(workbox_idx)
|
|
48
|
+
if workbox.__changed_by_instance__():
|
|
49
|
+
changed = True
|
|
50
|
+
break
|
|
51
|
+
return changed
|
|
52
|
+
|
|
53
|
+
def __orphaned_by_instance__(self):
|
|
54
|
+
"""Returns if any of this groups editors have been orphaned by another
|
|
55
|
+
PrEditor instance's prefs save.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
orphaned (bool)
|
|
59
|
+
"""
|
|
60
|
+
orphaned = False
|
|
61
|
+
for workbox_idx in range(self.count()):
|
|
62
|
+
workbox = self.widget(workbox_idx)
|
|
63
|
+
if workbox.__orphaned_by_instance__():
|
|
64
|
+
orphaned = True
|
|
65
|
+
break
|
|
66
|
+
return orphaned
|
|
67
|
+
|
|
68
|
+
def __is_dirty__(self):
|
|
69
|
+
"""Returns if any of this groups editors are dirty.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
is_dirty (bool)
|
|
73
|
+
"""
|
|
74
|
+
is_dirty = False
|
|
75
|
+
for workbox_idx in range(self.count()):
|
|
76
|
+
workbox = self.widget(workbox_idx)
|
|
77
|
+
if workbox.__is_dirty__():
|
|
78
|
+
is_dirty = True
|
|
79
|
+
break
|
|
80
|
+
return is_dirty
|
|
81
|
+
|
|
82
|
+
def __is_missing_linked_file__(self):
|
|
83
|
+
"""Determine if any of the workboxes are linked to file which is missing
|
|
84
|
+
on disk.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
bool: Whether any of this group's workboxes define a linked file
|
|
88
|
+
which is missing on disk.
|
|
89
|
+
"""
|
|
90
|
+
is_missing_linked_file = False
|
|
91
|
+
for workbox_idx in range(self.count()):
|
|
92
|
+
workbox = self.widget(workbox_idx)
|
|
93
|
+
if workbox.__is_missing_linked_file__():
|
|
94
|
+
is_missing_linked_file = True
|
|
95
|
+
break
|
|
96
|
+
return is_missing_linked_file
|
|
97
|
+
|
|
98
|
+
def add_new_editor(self, title=None, prefs=None):
|
|
99
|
+
title = title or self.default_title
|
|
100
|
+
|
|
101
|
+
title = self.get_next_available_tab_name(title)
|
|
102
|
+
editor, title = self.default_tab(title, prefs=prefs)
|
|
103
|
+
index = self.addTab(editor, title)
|
|
104
|
+
self.setCurrentIndex(index)
|
|
105
|
+
return editor
|
|
106
|
+
|
|
107
|
+
def addTab(self, *args, **kwargs): # noqa: N802
|
|
108
|
+
ret = super(GroupedTabWidget, self).addTab(*args, **kwargs)
|
|
109
|
+
self.update_closable_tabs()
|
|
110
|
+
return ret
|
|
111
|
+
|
|
112
|
+
def close_tab(self, index, ask=True):
|
|
113
|
+
if self.count() == 1:
|
|
114
|
+
msg = "You have to leave at least one tab open."
|
|
115
|
+
QMessageBox.critical(
|
|
116
|
+
self, 'Tab can not be closed.', msg, QMessageBox.StandardButton.Ok
|
|
117
|
+
)
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
workbox = self.widget(index)
|
|
121
|
+
name = workbox.__workbox_name__()
|
|
122
|
+
msg = (
|
|
123
|
+
f"Would you like to donate the contents of tab\n{name}\nto the "
|
|
124
|
+
"/dev/null fund for wayward code?"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if ask:
|
|
128
|
+
ret = QMessageBox.question(
|
|
129
|
+
self,
|
|
130
|
+
'Donate to the cause?',
|
|
131
|
+
msg,
|
|
132
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel,
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
ret = QMessageBox.StandardButton.Yes
|
|
136
|
+
|
|
137
|
+
if ret == QMessageBox.StandardButton.Yes:
|
|
138
|
+
editor = self.widget(index)
|
|
139
|
+
editor.__set_file_monitoring_enabled__(False)
|
|
140
|
+
self.store_closed_workbox(index)
|
|
141
|
+
super(GroupedTabWidget, self).close_tab(index)
|
|
142
|
+
|
|
143
|
+
def store_closed_workbox(self, index):
|
|
144
|
+
"""Store the name of the workbox being closed.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
index (int): The index of the workbox being closed
|
|
148
|
+
"""
|
|
149
|
+
workbox = self.widget(index)
|
|
150
|
+
|
|
151
|
+
# Save the workbox first, so we can possibly restore it later.
|
|
152
|
+
workbox.__save_prefs__(saveLinkedFile=False)
|
|
153
|
+
|
|
154
|
+
self.parent().window().addRecentlyClosedWorkbox(workbox)
|
|
155
|
+
|
|
156
|
+
def default_tab(self, title=None, prefs=None):
|
|
157
|
+
title = title or self.default_title
|
|
158
|
+
kwargs = self.editor_kwargs if self.editor_kwargs else {}
|
|
159
|
+
editor = None
|
|
160
|
+
orphaned_by_instance = False
|
|
161
|
+
if prefs:
|
|
162
|
+
editor_info = prefs.pop("existing_editor_info", None)
|
|
163
|
+
if editor_info:
|
|
164
|
+
editor = editor_info[0]
|
|
165
|
+
orphaned_by_instance = prefs.pop("orphaned_by_instance", False)
|
|
166
|
+
else:
|
|
167
|
+
prefs = {}
|
|
168
|
+
|
|
169
|
+
if editor:
|
|
170
|
+
editor.__load_workbox_version_text__(VersionTypes.Last)
|
|
171
|
+
|
|
172
|
+
editor.__set_last_saved_text__(editor.__text__())
|
|
173
|
+
editor.__set_last_workbox_name__(editor.__workbox_name__())
|
|
174
|
+
|
|
175
|
+
filename = prefs.get("filename", None)
|
|
176
|
+
editor.__set_filename__(filename)
|
|
177
|
+
|
|
178
|
+
editor.__determine_been_changed_by_instance__()
|
|
179
|
+
self.window().setWorkboxFontBasedOnConsole(editor)
|
|
180
|
+
else:
|
|
181
|
+
editor = self.editor_cls(
|
|
182
|
+
parent=self, core_name=self.core_name, **prefs, **kwargs
|
|
183
|
+
)
|
|
184
|
+
editor.__set_orphaned_by_instance__(orphaned_by_instance)
|
|
185
|
+
return editor, title
|
|
186
|
+
|
|
187
|
+
def get_next_available_tab_name(self, name=None):
|
|
188
|
+
"""Get the next available tab name, providing a default if needed.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
name (str, optional): The name for which to get the next available
|
|
192
|
+
name.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
str: The determined next available tab name
|
|
196
|
+
"""
|
|
197
|
+
if name is None:
|
|
198
|
+
name = self.default_title
|
|
199
|
+
return super().get_next_available_tab_name(name)
|
|
200
|
+
|
|
201
|
+
def showEvent(self, event): # noqa: N802
|
|
202
|
+
super(GroupedTabWidget, self).showEvent(event)
|
|
203
|
+
self.tab_shown(self.currentIndex())
|
|
204
|
+
|
|
205
|
+
def tab_shown(self, index):
|
|
206
|
+
editor = self.widget(index)
|
|
207
|
+
if editor and editor.isVisible():
|
|
208
|
+
editor.__show__()
|
|
209
|
+
|
|
210
|
+
if hasattr(self.window(), "setWorkboxFontBasedOnConsole"):
|
|
211
|
+
self.window().setWorkboxFontBasedOnConsole()
|
|
212
|
+
|
|
213
|
+
def tab_widget(self):
|
|
214
|
+
"""Return the tab widget which contains this group tab
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
self._tab_widget (GroupTabWidget): The tab widget which contains
|
|
218
|
+
this workbox
|
|
219
|
+
"""
|
|
220
|
+
return self.parent().parent()
|
|
221
|
+
|
|
222
|
+
def update_closable_tabs(self):
|
|
223
|
+
self.setTabsClosable(self.count() != 1)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import absolute_import
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from Qt.QtWidgets import QTabWidget
|
|
6
|
+
|
|
7
|
+
TAB_ITERATION_PATTERN = re.compile(r"(\d+)(?!.*\d)")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OneTabWidget(QTabWidget):
|
|
11
|
+
"""A QTabWidget that shows the close button only if there is more than one
|
|
12
|
+
tab. If something removes the last tab, it will add a default tab if the
|
|
13
|
+
default_tab method is implemented on a subclass. This is also used to create
|
|
14
|
+
the first tab on showEvent.
|
|
15
|
+
|
|
16
|
+
Subclasses can implement a `default_tab()` method. This should return the
|
|
17
|
+
widget to add and the title of the tab to create if implemented.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, *args, **kwargs):
|
|
21
|
+
super(OneTabWidget, self).__init__(*args, **kwargs)
|
|
22
|
+
self.tabCloseRequested.connect(self.close_tab)
|
|
23
|
+
|
|
24
|
+
def get_next_available_tab_name(self, name):
|
|
25
|
+
"""Get the next available tab name, incrementing an iteration if needed.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
name (str): The desired name
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
name (str): The name, or updated name if needed
|
|
32
|
+
"""
|
|
33
|
+
existing_names = [self.tabText(i) for i in range(self.count())]
|
|
34
|
+
|
|
35
|
+
# Use regex to find the last set of digits. If found, the base name is
|
|
36
|
+
# a slice of name minus the digits string. Otherwise, the base name is
|
|
37
|
+
# the full name and iteration is zero.
|
|
38
|
+
match = TAB_ITERATION_PATTERN.search(name)
|
|
39
|
+
if match:
|
|
40
|
+
# We found trailing digits, so slice to get base name, and convert
|
|
41
|
+
# iteration to int
|
|
42
|
+
iter_str = match.group()
|
|
43
|
+
base = name[: -len(iter_str)]
|
|
44
|
+
iteration = int(iter_str)
|
|
45
|
+
else:
|
|
46
|
+
# No trailing digits found, so base name is full name and iteration
|
|
47
|
+
# is zero.
|
|
48
|
+
base = name
|
|
49
|
+
iteration = 0
|
|
50
|
+
|
|
51
|
+
if name in existing_names:
|
|
52
|
+
for _ in range(99):
|
|
53
|
+
iteration += 1
|
|
54
|
+
new_iter_str = str(iteration).zfill(2)
|
|
55
|
+
name = base + new_iter_str
|
|
56
|
+
if name not in existing_names:
|
|
57
|
+
break
|
|
58
|
+
return name
|
|
59
|
+
|
|
60
|
+
def addTab(self, *args, **kwargs): # noqa: N802
|
|
61
|
+
ret = super(OneTabWidget, self).addTab(*args, **kwargs)
|
|
62
|
+
self.update_closable_tabs()
|
|
63
|
+
self.tabBar().setFont(self.window().font())
|
|
64
|
+
return ret
|
|
65
|
+
|
|
66
|
+
def close_tab(self, index):
|
|
67
|
+
self.removeTab(index)
|
|
68
|
+
self.update_closable_tabs()
|
|
69
|
+
|
|
70
|
+
def index_for_text(self, text):
|
|
71
|
+
"""Return the index of the tab with this text. Returns -1 if not found"""
|
|
72
|
+
for i in range(self.count()):
|
|
73
|
+
if self.tabText(i) == text:
|
|
74
|
+
return i
|
|
75
|
+
return -1
|
|
76
|
+
|
|
77
|
+
def insertTab(self, *args, **kwargs): # noqa: N802
|
|
78
|
+
ret = super(OneTabWidget, self).insertTab(*args, **kwargs)
|
|
79
|
+
self.update_closable_tabs()
|
|
80
|
+
return ret
|
|
81
|
+
|
|
82
|
+
def removeTab(self, index): # noqa: N802
|
|
83
|
+
super(OneTabWidget, self).removeTab(index)
|
|
84
|
+
if hasattr(self, 'default_tab') and not self.count():
|
|
85
|
+
self.addTab(*self.default_tab())
|
|
86
|
+
self.update_closable_tabs()
|
|
87
|
+
self.window().updateTabColorsAndToolTips()
|
|
88
|
+
|
|
89
|
+
def showEvent(self, event): # noqa: N802
|
|
90
|
+
super(OneTabWidget, self).showEvent(event)
|
|
91
|
+
# Force the creation of a default tab if defined
|
|
92
|
+
if hasattr(self, 'default_tab') and not self.count():
|
|
93
|
+
self.addTab(*self.default_tab())
|
|
94
|
+
|
|
95
|
+
def update_closable_tabs(self):
|
|
96
|
+
self.setTabsClosable(self.count() != 1)
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
from __future__ import absolute_import
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import types
|
|
5
|
+
from functools import partial
|
|
6
|
+
|
|
7
|
+
from Qt.QtGui import QIcon
|
|
8
|
+
from Qt.QtWidgets import QAction, QMenu, QToolButton
|
|
9
|
+
|
|
10
|
+
from .. import plugins, resourcePath
|
|
11
|
+
from ..enum import Enum, EnumGroup
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Level(Enum):
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
Custom `Enum` representing an information level.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
cached_icon(None): Used to cache the created icon from `get_icon` for
|
|
21
|
+
future use.
|
|
22
|
+
icon_name(str): Name of source icon file to use when creating icon via
|
|
23
|
+
`get_icon`.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
cached_icon = None
|
|
27
|
+
icon_name = "dot"
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def name(self):
|
|
31
|
+
"""
|
|
32
|
+
Override of `name` property allowing for the return of a "friendly
|
|
33
|
+
name" to be used in place of the inferred name from the `Enum` instance
|
|
34
|
+
name.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
str: Name of `Enum` instance.
|
|
38
|
+
"""
|
|
39
|
+
return getattr(self, "friendly_name", super(Level, self).name)
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def icon(self):
|
|
43
|
+
"""
|
|
44
|
+
Icon representing the level. On first access, the icon is created via
|
|
45
|
+
the `get_icon`-method and cached for later use.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
QIcon:
|
|
49
|
+
"""
|
|
50
|
+
if not self.cached_icon:
|
|
51
|
+
self.cached_icon = self.get_icon(self.icon_name, self.level)
|
|
52
|
+
return self.cached_icon
|
|
53
|
+
|
|
54
|
+
def get_icon(self, name, level):
|
|
55
|
+
"""
|
|
56
|
+
Retrieves the icon of `name` and level.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
name (str): Icon to retrieve QIcon for.
|
|
60
|
+
level (str): Level name to apply.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
QIcon: Correct instantiated QIcon.
|
|
64
|
+
"""
|
|
65
|
+
return QIcon(
|
|
66
|
+
resourcePath('img/{name}_{level}.png'.format(name=name, level=level))
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class LoggerLevel(Level):
|
|
71
|
+
"""A Logger level `Enum` using the 'format_align_left' icon."""
|
|
72
|
+
|
|
73
|
+
icon_name = "logging"
|
|
74
|
+
|
|
75
|
+
def is_current(self, logger):
|
|
76
|
+
"""Returns if the current logging level matches this label."""
|
|
77
|
+
return logging.getLevelName(logger.level) == self.label
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class LoggerLevels(EnumGroup):
|
|
81
|
+
"""
|
|
82
|
+
Logger levels with their implementation level name and number & custom
|
|
83
|
+
icon level.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
Disabled = LoggerLevel(
|
|
87
|
+
friendly_name="Not Set / Inherited", label="NOTSET", number=0, level="not_set"
|
|
88
|
+
)
|
|
89
|
+
Critical = LoggerLevel(label="CRITICAL", number=50, level="critical")
|
|
90
|
+
Error = LoggerLevel(label="ERROR", number=40, level="error")
|
|
91
|
+
Warning = LoggerLevel(label="WARNING", number=30, level="warning")
|
|
92
|
+
Info = LoggerLevel(label="INFO", number=20, level="info")
|
|
93
|
+
Debug = LoggerLevel(label="DEBUG", number=10, level="debug")
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def fromLabel(cls, label, default=None, logger=None):
|
|
97
|
+
try:
|
|
98
|
+
return super(LoggerLevels, cls).fromLabel(label, default=default)
|
|
99
|
+
except ValueError:
|
|
100
|
+
# This is not be a standard level, generate a custom level to use
|
|
101
|
+
if logger is None:
|
|
102
|
+
logger = logging.getLogger()
|
|
103
|
+
effective_level = logger.getEffectiveLevel()
|
|
104
|
+
effective_level_name = logging.getLevelName(effective_level)
|
|
105
|
+
|
|
106
|
+
enum = LoggerLevel(
|
|
107
|
+
label=effective_level_name,
|
|
108
|
+
number=effective_level,
|
|
109
|
+
level=effective_level_name,
|
|
110
|
+
)
|
|
111
|
+
# Force the custom icon as this enum's name won't match
|
|
112
|
+
enum.cached_icon = enum.get_icon(enum.icon_name, "custom")
|
|
113
|
+
# Add it to the enum
|
|
114
|
+
LoggerLevels.append(enum)
|
|
115
|
+
return enum
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class LoggingLevelButton(QToolButton):
|
|
119
|
+
|
|
120
|
+
"""
|
|
121
|
+
A drop down button to set logger levels for all loggers known to Python's
|
|
122
|
+
native logging implementation.
|
|
123
|
+
|
|
124
|
+
The logger menus present in the tool bar button have level-changing actions
|
|
125
|
+
as well a sub-menus for any descendant loggers.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def __init__(self, parent=None):
|
|
129
|
+
"""
|
|
130
|
+
Creates the root logger menu this button displays when clicked.
|
|
131
|
+
Additionally, any pre-existing loggers and their menus are added.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
parent (QWidget, optional): The parent widget for this button.
|
|
135
|
+
"""
|
|
136
|
+
super(LoggingLevelButton, self).__init__(parent=parent)
|
|
137
|
+
self.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
|
|
138
|
+
|
|
139
|
+
# create root logger menu
|
|
140
|
+
root = logging.getLogger("")
|
|
141
|
+
root_menu = LoggingLevelMenu(name="root", logger=root, parent=self)
|
|
142
|
+
self.setMenu(root_menu)
|
|
143
|
+
|
|
144
|
+
# TODO: Hook refresh up to a root logger signal
|
|
145
|
+
# Monkey patch root.setLogger to emit signal we connect to
|
|
146
|
+
root = self.patched_root_logger().level_changed.connect(self.refresh)
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
def patched_root_logger():
|
|
150
|
+
"""Returns `logging.getLogger("")`. This will have the level_changed
|
|
151
|
+
signal added if it wasn't already.
|
|
152
|
+
|
|
153
|
+
The level_changed signal is emitted any time something changes the
|
|
154
|
+
root logger level. PrEditor uses this to update the logging level button
|
|
155
|
+
icon any time the root logger's level is changed. The rest of the loggers
|
|
156
|
+
don't need this as the menu is built on demand with the correct icons indicated.
|
|
157
|
+
"""
|
|
158
|
+
root = logging.getLogger("")
|
|
159
|
+
if hasattr(root, "level_changed"):
|
|
160
|
+
# Already patched, nothing to do
|
|
161
|
+
return root
|
|
162
|
+
|
|
163
|
+
# Need to patch the root logger
|
|
164
|
+
from signalslot import Signal
|
|
165
|
+
|
|
166
|
+
root.level_changed = Signal(args=["level"], name="level_changed")
|
|
167
|
+
|
|
168
|
+
# Store the current setLevel, so we can call it in our method
|
|
169
|
+
root._setLevel = root.setLevel
|
|
170
|
+
|
|
171
|
+
def setLevel(self, level):
|
|
172
|
+
"""
|
|
173
|
+
Sets the threshold for this logger to `level`. Also emits the
|
|
174
|
+
instance's `level_changed`-signal with the level number as its payload.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
level (int): Numeric level value.
|
|
178
|
+
"""
|
|
179
|
+
# Call the original setLevel method
|
|
180
|
+
self._setLevel(level)
|
|
181
|
+
# Emit our signal
|
|
182
|
+
self.level_changed.emit(level=level)
|
|
183
|
+
|
|
184
|
+
root.setLevel = types.MethodType(setLevel, root)
|
|
185
|
+
|
|
186
|
+
return root
|
|
187
|
+
|
|
188
|
+
def refresh(self, **kwargs):
|
|
189
|
+
effective_level = logging.getLogger("").getEffectiveLevel()
|
|
190
|
+
effective_level_name = logging.getLevelName(effective_level)
|
|
191
|
+
level_enum = LoggerLevels.fromLabel(effective_level_name)
|
|
192
|
+
|
|
193
|
+
self.setIcon(level_enum.icon)
|
|
194
|
+
self.setToolTip("Logger 'root' current level: {}".format(level_enum.name))
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class LazyMenu(QMenu):
|
|
198
|
+
"""A menu class that only calls self.refresh when it is about to be shown."""
|
|
199
|
+
|
|
200
|
+
def __init__(self, *args, **kwargs):
|
|
201
|
+
super(LazyMenu, self).__init__(*args, **kwargs)
|
|
202
|
+
self.aboutToShow.connect(self.refresh)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class HandlerMenu(LazyMenu):
|
|
206
|
+
def __init__(self, logger, parent=None):
|
|
207
|
+
super(HandlerMenu, self).__init__(title="Handlers", parent=parent)
|
|
208
|
+
self.logger = logger
|
|
209
|
+
|
|
210
|
+
def install_handler(self, name):
|
|
211
|
+
plugins.add_logging_handler(self.logger, name)
|
|
212
|
+
|
|
213
|
+
def refresh(self):
|
|
214
|
+
self.clear()
|
|
215
|
+
# Add the Install sub menu showing all logging_handler plugins
|
|
216
|
+
handler_install = self.addMenu('Install')
|
|
217
|
+
for name, cls in plugins.logging_handlers():
|
|
218
|
+
act = handler_install.addAction(name)
|
|
219
|
+
act.triggered.connect(partial(self.install_handler, name))
|
|
220
|
+
for h in self.logger.handlers:
|
|
221
|
+
if type(h) is cls:
|
|
222
|
+
act.setEnabled(False)
|
|
223
|
+
act.setToolTip('Already installed for this logger.')
|
|
224
|
+
break
|
|
225
|
+
|
|
226
|
+
# Add a visual indication of all of the existing handlers
|
|
227
|
+
# TODO: Add ability to modify the formatters and auto-creation on startup
|
|
228
|
+
self.addSeparator()
|
|
229
|
+
for handler in self.logger.handlers:
|
|
230
|
+
act = self.addAction(repr(handler))
|
|
231
|
+
act.setEnabled(False)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class LoggingLevelMenu(LazyMenu):
|
|
235
|
+
|
|
236
|
+
"""
|
|
237
|
+
Custom menu for Python Loggers.
|
|
238
|
+
|
|
239
|
+
Provides an interface for changing logger levels via menu actions. Also
|
|
240
|
+
displays the presently set level by highlighting the relevant menu action
|
|
241
|
+
and via the menu's icon (which displays the logger's effective level,
|
|
242
|
+
potentially inherited from its ancestor).
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
def __init__(self, name, logger, parent=None):
|
|
246
|
+
"""
|
|
247
|
+
Creates the default level menu actions for updating the logger's level.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
name (str): Name of Logger this menu will represent.
|
|
251
|
+
logger (logging.Logger): Logger this menu will represent and control
|
|
252
|
+
via actions that modify the logger's set level.
|
|
253
|
+
parent (QToolButton/QMenu): `QMenu` or `QToolButton` this menu will
|
|
254
|
+
be parented to.
|
|
255
|
+
|
|
256
|
+
Note: If the logger is merely a placeholder it will be initialized so
|
|
257
|
+
it can be added to the menu hierarchy. This ensures all ancestors
|
|
258
|
+
exist for appropriate parenting when descendants are added.
|
|
259
|
+
"""
|
|
260
|
+
super(LoggingLevelMenu, self).__init__(title=name.split(".")[-1], parent=parent)
|
|
261
|
+
|
|
262
|
+
if isinstance(logger, logging.PlaceHolder):
|
|
263
|
+
logger = logging.getLogger(name)
|
|
264
|
+
|
|
265
|
+
self.logger = logger
|
|
266
|
+
self.name = name
|
|
267
|
+
self.update_ui()
|
|
268
|
+
|
|
269
|
+
def children(self):
|
|
270
|
+
"""The direct sub-loggers of this logging object."""
|
|
271
|
+
parent = self.name
|
|
272
|
+
if parent == "root":
|
|
273
|
+
parent = ""
|
|
274
|
+
for name, logger in sorted(
|
|
275
|
+
logging.root.manager.loggerDict.items(), key=lambda x: x[0].lower()
|
|
276
|
+
):
|
|
277
|
+
if name.startswith(parent):
|
|
278
|
+
remaining = name.lstrip(parent).lstrip(".")
|
|
279
|
+
if remaining and "." not in remaining:
|
|
280
|
+
yield name, logger
|
|
281
|
+
|
|
282
|
+
def level(self):
|
|
283
|
+
"""Returns the current effective LoggerLevel for self.logger."""
|
|
284
|
+
effective_level = self.logger.getEffectiveLevel()
|
|
285
|
+
effective_level_name = logging.getLevelName(effective_level)
|
|
286
|
+
return LoggerLevels.fromLabel(effective_level_name, logger=self.logger)
|
|
287
|
+
|
|
288
|
+
def refresh(self):
|
|
289
|
+
self.clear()
|
|
290
|
+
|
|
291
|
+
self.addMenu(HandlerMenu(self.logger, self))
|
|
292
|
+
self.addSeparator()
|
|
293
|
+
|
|
294
|
+
if self.logger.disabled:
|
|
295
|
+
# Warn the user that this logger has been disabled. The documentation
|
|
296
|
+
# says to consider this property as read only so this is just info
|
|
297
|
+
# and does not implement a way to re-enable this logger. It's not
|
|
298
|
+
# disabled because that would prevent the tooltip from showing.
|
|
299
|
+
action = self.addAction("Logger is disabled")
|
|
300
|
+
action.setToolTip(
|
|
301
|
+
'This <b>logger has been disabled</b> and does not handle events '
|
|
302
|
+
"so you likely won't see events from it or its children. "
|
|
303
|
+
'This normally happens when using logging.config with '
|
|
304
|
+
'"disable_existing_loggers" defaulted to True.'
|
|
305
|
+
)
|
|
306
|
+
action.setIcon(QIcon(resourcePath('img/warning-big.png')))
|
|
307
|
+
self.addSeparator()
|
|
308
|
+
|
|
309
|
+
for logger_level in LoggerLevels:
|
|
310
|
+
is_current = logger_level.is_current(self.logger)
|
|
311
|
+
|
|
312
|
+
action = QAction(logger_level.icon, logger_level.name, self)
|
|
313
|
+
action.setCheckable(True)
|
|
314
|
+
action.setChecked(is_current)
|
|
315
|
+
|
|
316
|
+
# tooltip example: "Set 'preditor.debug' to level Warning")
|
|
317
|
+
action.setToolTip(
|
|
318
|
+
"Set '{}' to level {}".format(self.name, logger_level.name)
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# when clicked/activated set associated loggers level
|
|
322
|
+
action.triggered.connect(partial(self.setLevel, logger_level.number))
|
|
323
|
+
self.addAction(action)
|
|
324
|
+
|
|
325
|
+
self.addSeparator()
|
|
326
|
+
|
|
327
|
+
for name, child in self.children():
|
|
328
|
+
self.addMenu(LoggingLevelMenu(name, child, self))
|
|
329
|
+
|
|
330
|
+
def setLevel(self, level):
|
|
331
|
+
"""
|
|
332
|
+
Sets the logger this menu object represents to the level supplied.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
level (str): Logging level to set logger to.
|
|
336
|
+
"""
|
|
337
|
+
self.logger.setLevel(level)
|
|
338
|
+
self.update_ui()
|
|
339
|
+
|
|
340
|
+
def update_ui(self):
|
|
341
|
+
"""Set the menu icon to this LoggerLevel's icon.
|
|
342
|
+
|
|
343
|
+
If the updated logger is the root logger, the logging level toolbar
|
|
344
|
+
button's icon is updated instead.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
level (LoggerLevel): Logging level to change icon to represent.
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
level_enum = self.level()
|
|
351
|
+
act = self.menuAction()
|
|
352
|
+
act.setIcon(level_enum.icon)
|
|
353
|
+
act.setToolTip(
|
|
354
|
+
"Logger '{}' current level: {}".format(self.logger.name, level_enum.name)
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
if self.name == "root":
|
|
358
|
+
self.parent().refresh()
|