pyqt-code-editor 0.0.1__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.
- pyqt_code_editor/__init__.py +5 -0
- pyqt_code_editor/_settings.py +232 -0
- pyqt_code_editor/app.py +438 -0
- pyqt_code_editor/code_editors/__init__.py +1 -0
- pyqt_code_editor/code_editors/code_editors.py +33 -0
- pyqt_code_editor/code_editors/languages/__init__.py +0 -0
- pyqt_code_editor/code_editors/languages/generic.py +11 -0
- pyqt_code_editor/code_editors/languages/markdown.py +11 -0
- pyqt_code_editor/code_editors/languages/python.py +13 -0
- pyqt_code_editor/code_editors/languages/text.py +9 -0
- pyqt_code_editor/components/__init__.py +0 -0
- pyqt_code_editor/components/editor_panel.py +399 -0
- pyqt_code_editor/components/find_in_files.py +199 -0
- pyqt_code_editor/components/jupyter_console.py +487 -0
- pyqt_code_editor/components/project_explorer.py +515 -0
- pyqt_code_editor/components/settings_panel.py +162 -0
- pyqt_code_editor/components/sigmund.py +75 -0
- pyqt_code_editor/components/workspace_explorer.py +162 -0
- pyqt_code_editor/mixins/__init__.py +15 -0
- pyqt_code_editor/mixins/auto_pair.py +201 -0
- pyqt_code_editor/mixins/base.py +142 -0
- pyqt_code_editor/mixins/check.py +94 -0
- pyqt_code_editor/mixins/comment.py +91 -0
- pyqt_code_editor/mixins/complete.py +482 -0
- pyqt_code_editor/mixins/execute.py +182 -0
- pyqt_code_editor/mixins/file_link.py +166 -0
- pyqt_code_editor/mixins/line_number.py +206 -0
- pyqt_code_editor/mixins/python_auto_indent.py +239 -0
- pyqt_code_editor/mixins/search_replace.py +396 -0
- pyqt_code_editor/mixins/shortcuts.py +228 -0
- pyqt_code_editor/mixins/smart_backspace_delete.py +120 -0
- pyqt_code_editor/mixins/symbols.py +45 -0
- pyqt_code_editor/mixins/theme.py +99 -0
- pyqt_code_editor/mixins/widgets/__init__.py +0 -0
- pyqt_code_editor/mixins/zoom.py +51 -0
- pyqt_code_editor/python_utils.py +936 -0
- pyqt_code_editor/signal_router.py +116 -0
- pyqt_code_editor/syntax_highlighters/__init__.py +0 -0
- pyqt_code_editor/syntax_highlighters/languages/__init__.py +0 -0
- pyqt_code_editor/syntax_highlighters/languages/generic.py +86 -0
- pyqt_code_editor/syntax_highlighters/languages/python.py +62 -0
- pyqt_code_editor/syntax_highlighters/syntax_highlighter.py +26 -0
- pyqt_code_editor/themes.py +36 -0
- pyqt_code_editor/utils.py +96 -0
- pyqt_code_editor/watchdog.py +141 -0
- pyqt_code_editor/widgets/__init__.py +6 -0
- pyqt_code_editor/widgets/calltip_widget.py +44 -0
- pyqt_code_editor/widgets/completion_popup.py +89 -0
- pyqt_code_editor/widgets/dock.py +15 -0
- pyqt_code_editor/widgets/quick_open_dialog.py +196 -0
- pyqt_code_editor/widgets/quick_open_file_dialog.py +32 -0
- pyqt_code_editor/widgets/quick_symbol_dialog.py +13 -0
- pyqt_code_editor/widgets/search_replace_frame.py +128 -0
- pyqt_code_editor/widgets/tab_splitter.py +22 -0
- pyqt_code_editor/widgets/tabbed_editor.py +351 -0
- pyqt_code_editor/worker/__init__.py +0 -0
- pyqt_code_editor/worker/languages/__init__.py +0 -0
- pyqt_code_editor/worker/languages/generic.py +15 -0
- pyqt_code_editor/worker/languages/python.py +17 -0
- pyqt_code_editor/worker/manager.py +113 -0
- pyqt_code_editor/worker/process.py +128 -0
- pyqt_code_editor/worker/providers/__init__.py +1 -0
- pyqt_code_editor/worker/providers/codestral.py +56 -0
- pyqt_code_editor/worker/providers/jedi.py +115 -0
- pyqt_code_editor/worker/providers/ruff.py +45 -0
- pyqt_code_editor/worker/providers/sigmund.py +29 -0
- pyqt_code_editor/worker/providers/symbol.py +78 -0
- pyqt_code_editor/worker/settings.py +1 -0
- pyqt_code_editor-0.0.1.dist-info/METADATA +69 -0
- pyqt_code_editor-0.0.1.dist-info/RECORD +73 -0
- pyqt_code_editor-0.0.1.dist-info/WHEEL +4 -0
- pyqt_code_editor-0.0.1.dist-info/entry_points.txt +4 -0
- pyqt_code_editor-0.0.1.dist-info/licenses/COPYING +674 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from qtpy.QtCore import Qt
|
|
2
|
+
from qtpy.QtGui import QTextCursor
|
|
3
|
+
from qtpy.QtWidgets import QListWidget, QListWidgetItem
|
|
4
|
+
from .. import settings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CompletionPopup(QListWidget):
|
|
8
|
+
"""
|
|
9
|
+
A popup widget to display multiple completion suggestions,
|
|
10
|
+
but it does NOT automatically grab focus or hide on focusOut
|
|
11
|
+
so that it won't flicker when the user keeps typing in the editor.
|
|
12
|
+
"""
|
|
13
|
+
def __init__(self, editor):
|
|
14
|
+
super().__init__(editor)
|
|
15
|
+
self.editor = editor
|
|
16
|
+
self._completion_map = {}
|
|
17
|
+
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
18
|
+
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
19
|
+
if editor.code_editor_colors is not None:
|
|
20
|
+
self.setStyleSheet(f'''QListWidget {{
|
|
21
|
+
color: {editor.code_editor_colors['text']};
|
|
22
|
+
background-color: {editor.code_editor_colors['background']};
|
|
23
|
+
font: {settings.font_size}pt '{settings.font_family}';
|
|
24
|
+
border: 1px solid {editor.code_editor_colors['border']};
|
|
25
|
+
}}''')
|
|
26
|
+
# Use a frameless tool window so it can float above the editor
|
|
27
|
+
# without stealing focus. Note we do NOT use Qt.Popup here.
|
|
28
|
+
self.setWindowFlags(Qt.FramelessWindowHint | Qt.Tool)
|
|
29
|
+
|
|
30
|
+
# Let the editor keep focus
|
|
31
|
+
self.setFocusPolicy(Qt.NoFocus)
|
|
32
|
+
|
|
33
|
+
# Single selection mode
|
|
34
|
+
self.setSelectionMode(self.SingleSelection)
|
|
35
|
+
|
|
36
|
+
# Insert the chosen completion when activated
|
|
37
|
+
self.itemActivated.connect(self.insert_completion)
|
|
38
|
+
|
|
39
|
+
def show_completions(self, completions):
|
|
40
|
+
"""
|
|
41
|
+
Show/update the list of completions near the editor's cursor,
|
|
42
|
+
without grabbing focus or hiding automatically.
|
|
43
|
+
"""
|
|
44
|
+
self.clear()
|
|
45
|
+
for c in completions:
|
|
46
|
+
self._completion_map[c['name']] = c['name']
|
|
47
|
+
QListWidgetItem(c['name'], self)
|
|
48
|
+
|
|
49
|
+
if not completions:
|
|
50
|
+
self.hide()
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
# Place near the text cursor, taking into account that the viewport
|
|
54
|
+
# might change due to line numbers or other things to the left of the
|
|
55
|
+
# editor
|
|
56
|
+
cursor_rect = self.editor.cursorRect()
|
|
57
|
+
cursor_rect.moveRight(cursor_rect.left() + self.editor.viewportMargins().left())
|
|
58
|
+
global_pos = self.editor.mapToGlobal(cursor_rect.bottomLeft())
|
|
59
|
+
self.move(global_pos)
|
|
60
|
+
|
|
61
|
+
# Resize to fit the number of completions (within reason)
|
|
62
|
+
self.setCurrentRow(0)
|
|
63
|
+
self.resize(min(500, self.sizeHintForColumn(0) + 8),
|
|
64
|
+
min(500, self.sizeHintForRow(0) * len(completions) + 8))
|
|
65
|
+
|
|
66
|
+
# If we are already visible, no need to hide/show
|
|
67
|
+
# Just continue to show it in the new position.
|
|
68
|
+
if not self.isVisible():
|
|
69
|
+
self.show()
|
|
70
|
+
|
|
71
|
+
def focusOutEvent(self, event):
|
|
72
|
+
"""
|
|
73
|
+
We override focusOutEvent but don't hide the popup
|
|
74
|
+
so we can continue showing suggestions while the editor has focus.
|
|
75
|
+
"""
|
|
76
|
+
# If you want it to hide whenever it loses focus, uncomment:
|
|
77
|
+
super().focusOutEvent(event)
|
|
78
|
+
|
|
79
|
+
def insert_completion(self, item):
|
|
80
|
+
"""
|
|
81
|
+
Inserts the selected completion text into the editor at the current cursor position.
|
|
82
|
+
"""
|
|
83
|
+
if not item:
|
|
84
|
+
return
|
|
85
|
+
name = item.text()
|
|
86
|
+
completion = self._completion_map.get(name)
|
|
87
|
+
if not completion:
|
|
88
|
+
return
|
|
89
|
+
self.editor._cm_insert_completion(completion)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from qtpy.QtCore import Signal
|
|
2
|
+
from qtpy.QtWidgets import QDockWidget
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Dock(QDockWidget):
|
|
6
|
+
|
|
7
|
+
close_requested = Signal()
|
|
8
|
+
|
|
9
|
+
def __init__(self, *args, **kwargs):
|
|
10
|
+
super().__init__(*args, **kwargs)
|
|
11
|
+
|
|
12
|
+
def closeEvent(self, event):
|
|
13
|
+
event.ignore()
|
|
14
|
+
self.hide()
|
|
15
|
+
self.close_requested.emit()
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
from qtpy.QtWidgets import (
|
|
4
|
+
QDialog,
|
|
5
|
+
QVBoxLayout,
|
|
6
|
+
QLineEdit,
|
|
7
|
+
QListView,
|
|
8
|
+
QAbstractItemView,
|
|
9
|
+
QApplication
|
|
10
|
+
)
|
|
11
|
+
from qtpy.QtCore import Qt, QSortFilterProxyModel, QModelIndex
|
|
12
|
+
from qtpy.QtGui import QStandardItemModel, QStandardItem
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
class MultiNeedleFilterProxyModel(QSortFilterProxyModel):
|
|
17
|
+
"""
|
|
18
|
+
Custom proxy model that splits the user input string into
|
|
19
|
+
multiple tokens (space-delimited). Each token must appear
|
|
20
|
+
somewhere in the item text (case-insensitive) for it to match.
|
|
21
|
+
"""
|
|
22
|
+
def __init__(self, parent=None):
|
|
23
|
+
super().__init__(parent)
|
|
24
|
+
self.setFilterCaseSensitivity(Qt.CaseInsensitive)
|
|
25
|
+
self._needles = []
|
|
26
|
+
|
|
27
|
+
def setFilterString(self, text):
|
|
28
|
+
# Split on whitespace to get multiple tokens
|
|
29
|
+
self._needles = text.strip().split()
|
|
30
|
+
self.invalidateFilter()
|
|
31
|
+
|
|
32
|
+
def filterAcceptsRow(self, source_row, source_parent):
|
|
33
|
+
if not self._needles:
|
|
34
|
+
return True # If no input, show all
|
|
35
|
+
index = self.sourceModel().index(source_row, 0, source_parent)
|
|
36
|
+
item_text = index.data(Qt.DisplayRole) or ""
|
|
37
|
+
item_text_lower = item_text.lower()
|
|
38
|
+
|
|
39
|
+
# Each needle must be present in the text
|
|
40
|
+
for needle in self._needles:
|
|
41
|
+
needle = needle.lower()
|
|
42
|
+
if needle not in item_text_lower:
|
|
43
|
+
return False
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
class QuickOpenDialog(QDialog):
|
|
47
|
+
"""
|
|
48
|
+
A generic quick-open dialog for filtering and selecting from a list of items, each a dict
|
|
49
|
+
with at least a 'name' key.
|
|
50
|
+
|
|
51
|
+
Subclasses should override on_item_selected(item_dict) to handle the chosen item.
|
|
52
|
+
"""
|
|
53
|
+
def __init__(self, parent, items, title="Quick Open"):
|
|
54
|
+
super().__init__(parent)
|
|
55
|
+
self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint)
|
|
56
|
+
self.setWindowModality(Qt.ApplicationModal)
|
|
57
|
+
|
|
58
|
+
self.setWindowTitle(title)
|
|
59
|
+
self.resize(400, 400)
|
|
60
|
+
|
|
61
|
+
layout = QVBoxLayout(self)
|
|
62
|
+
self._filter_edit = QLineEdit(self)
|
|
63
|
+
self._filter_edit.setPlaceholderText("Type to filter...")
|
|
64
|
+
layout.addWidget(self._filter_edit)
|
|
65
|
+
|
|
66
|
+
self._list_view = QListView(self)
|
|
67
|
+
self._list_view.setSelectionMode(QAbstractItemView.SingleSelection)
|
|
68
|
+
layout.addWidget(self._list_view)
|
|
69
|
+
|
|
70
|
+
self._items = items
|
|
71
|
+
self._item_model = QStandardItemModel(self)
|
|
72
|
+
self._proxy_model = MultiNeedleFilterProxyModel(self)
|
|
73
|
+
self._proxy_model.setSourceModel(self._item_model)
|
|
74
|
+
self._list_view.setModel(self._proxy_model)
|
|
75
|
+
|
|
76
|
+
# Populate the model with items
|
|
77
|
+
self._populate_model()
|
|
78
|
+
|
|
79
|
+
# Ensure we select the top item whenever the filter results change
|
|
80
|
+
self._proxy_model.layoutChanged.connect(self._select_top_item_if_available)
|
|
81
|
+
|
|
82
|
+
# Connect signals
|
|
83
|
+
self._filter_edit.textChanged.connect(self._proxy_model.setFilterString)
|
|
84
|
+
self._list_view.doubleClicked.connect(self._on_item_double_clicked)
|
|
85
|
+
|
|
86
|
+
# Attempt to select top item immediately
|
|
87
|
+
self._select_top_item_if_available()
|
|
88
|
+
|
|
89
|
+
def _populate_model(self):
|
|
90
|
+
for item_dict in self._items:
|
|
91
|
+
name = item_dict.get("name", "")
|
|
92
|
+
item = QStandardItem(name)
|
|
93
|
+
# Store the entire dict in UserRole
|
|
94
|
+
item.setData(item_dict, Qt.UserRole)
|
|
95
|
+
self._item_model.appendRow(item)
|
|
96
|
+
|
|
97
|
+
def _on_item_double_clicked(self, proxy_index: QModelIndex):
|
|
98
|
+
source_index = self._proxy_model.mapToSource(proxy_index)
|
|
99
|
+
item = self._item_model.itemFromIndex(source_index)
|
|
100
|
+
item_dict = item.data(Qt.UserRole)
|
|
101
|
+
self.on_item_selected(item_dict)
|
|
102
|
+
self.accept()
|
|
103
|
+
|
|
104
|
+
def _select_top_item_if_available(self):
|
|
105
|
+
"""Select the top item if it exists, otherwise clear selection."""
|
|
106
|
+
count = self._proxy_model.rowCount()
|
|
107
|
+
if count > 0:
|
|
108
|
+
top_index = self._proxy_model.index(0, 0)
|
|
109
|
+
self._list_view.setCurrentIndex(top_index)
|
|
110
|
+
else:
|
|
111
|
+
self._list_view.clearSelection()
|
|
112
|
+
|
|
113
|
+
def on_item_selected(self, item_dict: dict):
|
|
114
|
+
"""
|
|
115
|
+
Override this in subclasses to define what happens when an item is chosen.
|
|
116
|
+
"""
|
|
117
|
+
raise NotImplementedError("Subclasses must implement on_item_selected()")
|
|
118
|
+
|
|
119
|
+
def keyPressEvent(self, event):
|
|
120
|
+
"""Handle Enter for selection and Up/Down wrap-around navigation."""
|
|
121
|
+
key = event.key()
|
|
122
|
+
count = self._proxy_model.rowCount()
|
|
123
|
+
|
|
124
|
+
# If there are no matches, ignore arrow and page navigation
|
|
125
|
+
if count == 0:
|
|
126
|
+
if key in (
|
|
127
|
+
Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown,
|
|
128
|
+
Qt.Key_Home, Qt.Key_End
|
|
129
|
+
):
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
if key in (Qt.Key_Return, Qt.Key_Enter):
|
|
133
|
+
current_index = self._list_view.currentIndex()
|
|
134
|
+
if current_index.isValid():
|
|
135
|
+
proxy_index = current_index
|
|
136
|
+
else:
|
|
137
|
+
# If nothing is selected, fall back to top item if available
|
|
138
|
+
proxy_index = self._proxy_model.index(0, 0) if count > 0 else None
|
|
139
|
+
|
|
140
|
+
if proxy_index is not None and proxy_index.isValid():
|
|
141
|
+
source_index = self._proxy_model.mapToSource(proxy_index)
|
|
142
|
+
item = self._item_model.itemFromIndex(source_index)
|
|
143
|
+
item_dict = item.data(Qt.UserRole)
|
|
144
|
+
self.on_item_selected(item_dict)
|
|
145
|
+
self.accept()
|
|
146
|
+
|
|
147
|
+
elif key == Qt.Key_Up:
|
|
148
|
+
if count > 0:
|
|
149
|
+
current_index = self._list_view.currentIndex()
|
|
150
|
+
if current_index.isValid():
|
|
151
|
+
row = current_index.row()
|
|
152
|
+
if row <= 0:
|
|
153
|
+
# wrap to last
|
|
154
|
+
self._list_view.setCurrentIndex(
|
|
155
|
+
self._proxy_model.index(count - 1, 0)
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
self._list_view.setCurrentIndex(
|
|
159
|
+
self._proxy_model.index(row - 1, 0)
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
# If nothing is selected, go to last
|
|
163
|
+
self._list_view.setCurrentIndex(
|
|
164
|
+
self._proxy_model.index(count - 1, 0)
|
|
165
|
+
)
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
elif key == Qt.Key_Down:
|
|
169
|
+
if count > 0:
|
|
170
|
+
current_index = self._list_view.currentIndex()
|
|
171
|
+
if current_index.isValid():
|
|
172
|
+
row = current_index.row()
|
|
173
|
+
if row >= count - 1:
|
|
174
|
+
# wrap to first
|
|
175
|
+
self._list_view.setCurrentIndex(
|
|
176
|
+
self._proxy_model.index(0, 0)
|
|
177
|
+
)
|
|
178
|
+
else:
|
|
179
|
+
self._list_view.setCurrentIndex(
|
|
180
|
+
self._proxy_model.index(row + 1, 0)
|
|
181
|
+
)
|
|
182
|
+
else:
|
|
183
|
+
# If nothing is selected, go to first
|
|
184
|
+
self._list_view.setCurrentIndex(
|
|
185
|
+
self._proxy_model.index(0, 0)
|
|
186
|
+
)
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
elif key in (Qt.Key_PageUp, Qt.Key_PageDown, Qt.Key_Home, Qt.Key_End):
|
|
190
|
+
# Let default navigation logic handle these keys
|
|
191
|
+
self._list_view.setFocus()
|
|
192
|
+
QApplication.sendEvent(self._list_view, event)
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
else:
|
|
196
|
+
super().keyPressEvent(event)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
from . import QuickOpenDialog
|
|
4
|
+
logger = logging.getLogger(__name__)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class QuickOpenFileDialog(QuickOpenDialog):
|
|
8
|
+
"""
|
|
9
|
+
Specialized dialog that handles quickly opening files.
|
|
10
|
+
Collects files in a given root_path, strips common prefix,
|
|
11
|
+
and calls open_file_callback on selection.
|
|
12
|
+
"""
|
|
13
|
+
def __init__(self, parent, file_list, open_file_callback):
|
|
14
|
+
self.open_file_callback = open_file_callback
|
|
15
|
+
items = []
|
|
16
|
+
if file_list:
|
|
17
|
+
common_prefix = os.path.commonpath(file_list)
|
|
18
|
+
for full_path in file_list:
|
|
19
|
+
relative_path = os.path.relpath(full_path, common_prefix)
|
|
20
|
+
items.append({
|
|
21
|
+
"name": relative_path,
|
|
22
|
+
"full_path": full_path,
|
|
23
|
+
})
|
|
24
|
+
else:
|
|
25
|
+
logger.warning('no files to show')
|
|
26
|
+
super().__init__(parent, items, title="Quick Open File")
|
|
27
|
+
|
|
28
|
+
def on_item_selected(self, item_dict: dict):
|
|
29
|
+
"""Opens the file at item_dict['full_path'] and closes the dialog."""
|
|
30
|
+
full_path = item_dict.get("full_path", None)
|
|
31
|
+
if full_path:
|
|
32
|
+
self.open_file_callback(full_path)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
from . import QuickOpenDialog
|
|
4
|
+
logger = logging.getLogger(__name__)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class QuickSymbolDialog(QuickOpenDialog):
|
|
8
|
+
def __init__(self, parent, items, jump_to_symbol_callback):
|
|
9
|
+
self.jump_to_symbol_callback = jump_to_symbol_callback
|
|
10
|
+
super().__init__(parent, items, title="Symbols")
|
|
11
|
+
|
|
12
|
+
def on_item_selected(self, item_dict: dict):
|
|
13
|
+
self.jump_to_symbol_callback(item_dict)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from qtpy.QtWidgets import (
|
|
2
|
+
QFrame, QVBoxLayout, QHBoxLayout,
|
|
3
|
+
QLineEdit, QPushButton, QCheckBox, QLabel
|
|
4
|
+
)
|
|
5
|
+
from qtpy.QtCore import Qt, Signal
|
|
6
|
+
from .. import settings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SearchReplaceFrame(QFrame):
|
|
10
|
+
"""
|
|
11
|
+
A small widget containing 'Find' and optionally 'Replace' fields,
|
|
12
|
+
plus checkboxes and buttons for next/prev/replace/replace all, etc.
|
|
13
|
+
Hides or shows replace-related UI depending on mode.
|
|
14
|
+
"""
|
|
15
|
+
findNextRequested = Signal()
|
|
16
|
+
findPrevRequested = Signal()
|
|
17
|
+
replaceOneRequested = Signal()
|
|
18
|
+
replaceAllRequested = Signal()
|
|
19
|
+
|
|
20
|
+
def __init__(self, parent=None):
|
|
21
|
+
super().__init__(parent)
|
|
22
|
+
|
|
23
|
+
self.setFrameShape(QFrame.StyledPanel)
|
|
24
|
+
self.setFrameShadow(QFrame.Raised)
|
|
25
|
+
editor = parent
|
|
26
|
+
if editor.code_editor_colors is not None:
|
|
27
|
+
self.setStyleSheet(f'''
|
|
28
|
+
QCheckBox,
|
|
29
|
+
QPushButton,
|
|
30
|
+
QLineEdit,
|
|
31
|
+
QFrame {{
|
|
32
|
+
color: {editor.code_editor_colors['text']};
|
|
33
|
+
background-color: {editor.code_editor_colors['background']};
|
|
34
|
+
font: {settings.font_size}pt '{settings.font_family}';
|
|
35
|
+
padding: 8px;
|
|
36
|
+
}}
|
|
37
|
+
QCheckBox::indicator,
|
|
38
|
+
QPushButton,
|
|
39
|
+
QLineEdit,
|
|
40
|
+
QFrame {{
|
|
41
|
+
border-color: {editor.code_editor_colors['border']};
|
|
42
|
+
border-width: 1px;
|
|
43
|
+
border-style: solid;
|
|
44
|
+
border-radius: 4px;
|
|
45
|
+
}}
|
|
46
|
+
QCheckBox::indicator:checked {{
|
|
47
|
+
background-color: {editor.code_editor_colors['text']};
|
|
48
|
+
}}
|
|
49
|
+
QLabel {{
|
|
50
|
+
border: none;
|
|
51
|
+
}}
|
|
52
|
+
''')
|
|
53
|
+
self.setWindowFlags(Qt.SubWindow | Qt.FramelessWindowHint)
|
|
54
|
+
|
|
55
|
+
# Layout
|
|
56
|
+
layout = QVBoxLayout(self)
|
|
57
|
+
layout.setContentsMargins(5, 5, 5, 5)
|
|
58
|
+
|
|
59
|
+
# Find row
|
|
60
|
+
findRow = QHBoxLayout()
|
|
61
|
+
|
|
62
|
+
self.findLabel = QLabel("Find:", self)
|
|
63
|
+
self.matchCountLabel = QLabel("0 of 0", self)
|
|
64
|
+
self.findEdit = QLineEdit(self)
|
|
65
|
+
self.caseBox = QCheckBox("Aa", self)
|
|
66
|
+
self.caseBox.setToolTip("Case Sensitive")
|
|
67
|
+
self.regexBox = QCheckBox(".*", self)
|
|
68
|
+
self.regexBox.setToolTip("Use Regular Expressions")
|
|
69
|
+
self.wholeWordBox = QCheckBox("\\b", self)
|
|
70
|
+
self.wholeWordBox.setToolTip("Match Whole Word")
|
|
71
|
+
|
|
72
|
+
self.findNextBtn = QPushButton("Next", self)
|
|
73
|
+
self.findPrevBtn = QPushButton("Prev", self)
|
|
74
|
+
|
|
75
|
+
findRow.addWidget(self.findLabel)
|
|
76
|
+
findRow.addWidget(self.findEdit)
|
|
77
|
+
findRow.addWidget(self.matchCountLabel)
|
|
78
|
+
findRow.addWidget(self.caseBox)
|
|
79
|
+
findRow.addWidget(self.regexBox)
|
|
80
|
+
findRow.addWidget(self.wholeWordBox)
|
|
81
|
+
findRow.addWidget(self.findNextBtn)
|
|
82
|
+
findRow.addWidget(self.findPrevBtn)
|
|
83
|
+
|
|
84
|
+
layout.addLayout(findRow)
|
|
85
|
+
|
|
86
|
+
# Replace row
|
|
87
|
+
replaceRow = QHBoxLayout()
|
|
88
|
+
self.replaceLabel = QLabel("Replace:", self)
|
|
89
|
+
self.replaceEdit = QLineEdit(self)
|
|
90
|
+
self.replaceBtn = QPushButton("Replace", self)
|
|
91
|
+
self.replaceAllBtn = QPushButton("Replace All", self)
|
|
92
|
+
|
|
93
|
+
replaceRow.addWidget(self.replaceLabel)
|
|
94
|
+
replaceRow.addWidget(self.replaceEdit)
|
|
95
|
+
replaceRow.addWidget(self.replaceBtn)
|
|
96
|
+
replaceRow.addWidget(self.replaceAllBtn)
|
|
97
|
+
|
|
98
|
+
layout.addLayout(replaceRow)
|
|
99
|
+
|
|
100
|
+
self.replaceRowWidget = replaceRow # Keep reference to manage visibility
|
|
101
|
+
|
|
102
|
+
# Connections
|
|
103
|
+
self.findNextBtn.clicked.connect(self.findNextRequested)
|
|
104
|
+
self.findPrevBtn.clicked.connect(self.findPrevRequested)
|
|
105
|
+
self.replaceBtn.clicked.connect(self.replaceOneRequested)
|
|
106
|
+
self.replaceAllBtn.clicked.connect(self.replaceAllRequested)
|
|
107
|
+
|
|
108
|
+
# Default: searching only, so hide the "replace" row
|
|
109
|
+
self.showSearchOnly()
|
|
110
|
+
|
|
111
|
+
def showSearchOnly(self):
|
|
112
|
+
# Hide replace UI
|
|
113
|
+
self.replaceLabel.setVisible(False)
|
|
114
|
+
self.replaceEdit.setVisible(False)
|
|
115
|
+
self.replaceBtn.setVisible(False)
|
|
116
|
+
self.replaceAllBtn.setVisible(False)
|
|
117
|
+
self.adjustSize()
|
|
118
|
+
self.resize(self.sizeHint())
|
|
119
|
+
|
|
120
|
+
def showSearchReplace(self):
|
|
121
|
+
# Show replace UI
|
|
122
|
+
self.replaceLabel.setVisible(True)
|
|
123
|
+
self.replaceEdit.setVisible(True)
|
|
124
|
+
self.replaceBtn.setVisible(True)
|
|
125
|
+
self.replaceAllBtn.setVisible(True)
|
|
126
|
+
self.adjustSize()
|
|
127
|
+
self.resize(self.sizeHint())
|
|
128
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from qtpy.QtWidgets import QSplitter
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TabSplitter(QSplitter):
|
|
5
|
+
|
|
6
|
+
def editors(self):
|
|
7
|
+
"""Returns a list of all in this splitter and all sub splitters"""
|
|
8
|
+
editors = []
|
|
9
|
+
for i in range(self.count()):
|
|
10
|
+
editors += self.widget(i).editors()
|
|
11
|
+
return editors
|
|
12
|
+
|
|
13
|
+
def tab_widgets(self):
|
|
14
|
+
"""Returns a list of all tab widgets in this splitter and all sub splitters"""
|
|
15
|
+
tab_widgets = []
|
|
16
|
+
for i in range(self.count()):
|
|
17
|
+
widget = self.widget(i)
|
|
18
|
+
if isinstance(widget, TabSplitter):
|
|
19
|
+
tab_widgets += widget.tab_widgets()
|
|
20
|
+
else:
|
|
21
|
+
tab_widgets.append(widget)
|
|
22
|
+
return tab_widgets
|