pygpt-net 2.6.59__py3-none-any.whl → 2.6.61__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.
- pygpt_net/CHANGELOG.txt +11 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +9 -5
- pygpt_net/controller/__init__.py +1 -0
- pygpt_net/controller/chat/common.py +115 -6
- pygpt_net/controller/chat/input.py +4 -1
- pygpt_net/controller/presets/editor.py +442 -39
- pygpt_net/controller/presets/presets.py +121 -6
- pygpt_net/controller/settings/editor.py +0 -15
- pygpt_net/controller/theme/markdown.py +2 -5
- pygpt_net/controller/ui/ui.py +4 -7
- pygpt_net/core/agents/custom/__init__.py +281 -0
- pygpt_net/core/agents/custom/debug.py +64 -0
- pygpt_net/core/agents/custom/factory.py +109 -0
- pygpt_net/core/agents/custom/graph.py +71 -0
- pygpt_net/core/agents/custom/llama_index/__init__.py +10 -0
- pygpt_net/core/agents/custom/llama_index/factory.py +100 -0
- pygpt_net/core/agents/custom/llama_index/router_streamer.py +106 -0
- pygpt_net/core/agents/custom/llama_index/runner.py +562 -0
- pygpt_net/core/agents/custom/llama_index/stream.py +56 -0
- pygpt_net/core/agents/custom/llama_index/utils.py +253 -0
- pygpt_net/core/agents/custom/logging.py +50 -0
- pygpt_net/core/agents/custom/memory.py +51 -0
- pygpt_net/core/agents/custom/router.py +155 -0
- pygpt_net/core/agents/custom/router_streamer.py +187 -0
- pygpt_net/core/agents/custom/runner.py +455 -0
- pygpt_net/core/agents/custom/schema.py +127 -0
- pygpt_net/core/agents/custom/utils.py +193 -0
- pygpt_net/core/agents/provider.py +72 -7
- pygpt_net/core/agents/runner.py +7 -4
- pygpt_net/core/agents/runners/helpers.py +1 -1
- pygpt_net/core/agents/runners/llama_workflow.py +3 -0
- pygpt_net/core/agents/runners/openai_workflow.py +8 -1
- pygpt_net/core/db/viewer.py +11 -5
- pygpt_net/{ui/widget/builder → core/node_editor}/__init__.py +2 -2
- pygpt_net/core/{builder → node_editor}/graph.py +28 -226
- pygpt_net/core/node_editor/models.py +118 -0
- pygpt_net/core/node_editor/types.py +78 -0
- pygpt_net/core/node_editor/utils.py +17 -0
- pygpt_net/core/presets/presets.py +216 -29
- pygpt_net/core/render/markdown/parser.py +0 -2
- pygpt_net/core/render/web/renderer.py +10 -8
- pygpt_net/data/config/config.json +5 -6
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/config/settings.json +2 -38
- pygpt_net/data/locale/locale.de.ini +64 -1
- pygpt_net/data/locale/locale.en.ini +63 -4
- pygpt_net/data/locale/locale.es.ini +64 -1
- pygpt_net/data/locale/locale.fr.ini +64 -1
- pygpt_net/data/locale/locale.it.ini +64 -1
- pygpt_net/data/locale/locale.pl.ini +65 -2
- pygpt_net/data/locale/locale.uk.ini +64 -1
- pygpt_net/data/locale/locale.zh.ini +64 -1
- pygpt_net/data/locale/plugin.cmd_system.en.ini +62 -66
- pygpt_net/item/agent.py +5 -1
- pygpt_net/item/preset.py +19 -1
- pygpt_net/provider/agents/base.py +33 -2
- pygpt_net/provider/agents/llama_index/flow_from_schema.py +92 -0
- pygpt_net/provider/agents/openai/flow_from_schema.py +96 -0
- pygpt_net/provider/core/agent/json_file.py +11 -5
- pygpt_net/provider/core/config/patch.py +10 -1
- pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +0 -6
- pygpt_net/tools/agent_builder/tool.py +233 -52
- pygpt_net/tools/agent_builder/ui/dialogs.py +172 -28
- pygpt_net/tools/agent_builder/ui/list.py +37 -10
- pygpt_net/ui/__init__.py +2 -4
- pygpt_net/ui/dialog/about.py +58 -38
- pygpt_net/ui/dialog/db.py +142 -3
- pygpt_net/ui/dialog/preset.py +62 -8
- pygpt_net/ui/layout/toolbox/presets.py +52 -16
- pygpt_net/ui/main.py +1 -1
- pygpt_net/ui/widget/dialog/db.py +0 -0
- pygpt_net/ui/widget/lists/preset.py +644 -60
- pygpt_net/{core/builder → ui/widget/node_editor}/__init__.py +2 -2
- pygpt_net/ui/widget/node_editor/command.py +373 -0
- pygpt_net/ui/widget/node_editor/config.py +157 -0
- pygpt_net/ui/widget/node_editor/editor.py +2070 -0
- pygpt_net/ui/widget/node_editor/item.py +493 -0
- pygpt_net/ui/widget/node_editor/node.py +1460 -0
- pygpt_net/ui/widget/node_editor/utils.py +17 -0
- pygpt_net/ui/widget/node_editor/view.py +364 -0
- pygpt_net/ui/widget/tabs/output.py +1 -1
- pygpt_net/ui/widget/textarea/input.py +2 -2
- pygpt_net/utils.py +114 -2
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/METADATA +80 -93
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/RECORD +88 -61
- pygpt_net/core/agents/custom.py +0 -150
- pygpt_net/ui/widget/builder/editor.py +0 -2001
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/entry_points.txt +0 -0
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2025.
|
|
9
|
+
# Updated Date: 2025.09.26 03:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
|
-
from PySide6.QtCore import QPoint, QItemSelectionModel
|
|
13
|
-
from PySide6.QtGui import QAction, QIcon,
|
|
14
|
-
from PySide6.QtWidgets import QMenu
|
|
12
|
+
from PySide6.QtCore import QPoint, QItemSelectionModel, Qt, QEventLoop, QTimer, QMimeData
|
|
13
|
+
from PySide6.QtGui import QAction, QIcon, QCursor, QDrag, QPainter, QPixmap, QPen, QColor
|
|
14
|
+
from PySide6.QtWidgets import QMenu, QAbstractItemView, QApplication
|
|
15
15
|
|
|
16
16
|
from pygpt_net.core.types import (
|
|
17
17
|
MODE_EXPERT,
|
|
@@ -27,6 +27,12 @@ class PresetList(BaseList):
|
|
|
27
27
|
_ICO_DELETE = QIcon(":/icons/delete.svg")
|
|
28
28
|
_ICO_CHECK = QIcon(":/icons/check.svg")
|
|
29
29
|
_ICO_CLOSE = QIcon(":/icons/close.svg")
|
|
30
|
+
_ICO_UP = QIcon(":/icons/collapse.svg")
|
|
31
|
+
_ICO_DOWN = QIcon(":/icons/expand.svg")
|
|
32
|
+
|
|
33
|
+
ROLE_UUID = Qt.UserRole + 1
|
|
34
|
+
ROLE_ID = Qt.UserRole + 2
|
|
35
|
+
ROLE_IS_SPECIAL = Qt.UserRole + 3
|
|
30
36
|
|
|
31
37
|
def __init__(self, window=None, id=None):
|
|
32
38
|
"""
|
|
@@ -45,40 +51,175 @@ class PresetList(BaseList):
|
|
|
45
51
|
self._backup_selection = None
|
|
46
52
|
self.restore_after_ctx_menu = True
|
|
47
53
|
|
|
54
|
+
# Flat list behavior
|
|
55
|
+
self.setRootIsDecorated(False)
|
|
56
|
+
self.setItemsExpandable(False)
|
|
57
|
+
self.setUniformRowHeights(True)
|
|
58
|
+
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
|
59
|
+
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
|
60
|
+
|
|
61
|
+
# Drag & drop state
|
|
62
|
+
self._dnd_enabled = False
|
|
63
|
+
self.setDragEnabled(False)
|
|
64
|
+
self.setAcceptDrops(False)
|
|
65
|
+
self.setDragDropMode(QAbstractItemView.NoDragDrop) # switched dynamically
|
|
66
|
+
self.setDefaultDropAction(Qt.MoveAction)
|
|
67
|
+
self.setDragDropOverwriteMode(False)
|
|
68
|
+
self.setDropIndicatorShown(False)
|
|
69
|
+
|
|
70
|
+
self._press_pos = None
|
|
71
|
+
self._press_index = None
|
|
72
|
+
self._press_backup_selection = None
|
|
73
|
+
self._press_backup_current = None
|
|
74
|
+
self._dragging = False
|
|
75
|
+
self._dragged_was_selected = False
|
|
76
|
+
|
|
77
|
+
# Mark that we already applied selection at drag start (one-shot per DnD)
|
|
78
|
+
self._drag_selection_applied = False
|
|
79
|
+
|
|
80
|
+
# ID-based selection persistence (single selection list)
|
|
81
|
+
self._saved_selection_ids = None
|
|
82
|
+
|
|
83
|
+
# Defer refresh payload after drop (DnD teardown must finish first)
|
|
84
|
+
self._pending_after_drop = None
|
|
85
|
+
|
|
86
|
+
# Guard against input during model rebuild (prevents crashes on quick clicks)
|
|
87
|
+
self._model_updating = False
|
|
88
|
+
|
|
89
|
+
# Keep original selection IDs before opening context menu (right-click)
|
|
90
|
+
self._ctx_menu_original_ids = None
|
|
91
|
+
|
|
92
|
+
# One-shot forced selection after refresh (list of ROLE_ID)
|
|
93
|
+
self._selection_override_ids = None
|
|
94
|
+
|
|
95
|
+
# -------- Public helpers to protect updates --------
|
|
96
|
+
|
|
97
|
+
def begin_model_update(self):
|
|
98
|
+
"""Temporarily block user interaction while the model/view is rebuilt."""
|
|
99
|
+
self._model_updating = True
|
|
100
|
+
self.setEnabled(False)
|
|
101
|
+
|
|
102
|
+
def end_model_update(self):
|
|
103
|
+
"""Re-enable interaction after model/view rebuild is complete."""
|
|
104
|
+
self.setEnabled(True)
|
|
105
|
+
self._model_updating = False
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------
|
|
108
|
+
|
|
109
|
+
def set_dnd_enabled(self, enabled: bool):
|
|
110
|
+
"""
|
|
111
|
+
Toggle DnD behaviour at runtime.
|
|
112
|
+
Using DragDrop (not InternalMove) to avoid implicit Qt reordering.
|
|
113
|
+
"""
|
|
114
|
+
self._dnd_enabled = bool(enabled)
|
|
115
|
+
if self._dnd_enabled:
|
|
116
|
+
self.setDragEnabled(True)
|
|
117
|
+
self.setAcceptDrops(True)
|
|
118
|
+
self.setDragDropMode(QAbstractItemView.DragDrop)
|
|
119
|
+
self.setDropIndicatorShown(True)
|
|
120
|
+
else:
|
|
121
|
+
self.setDragEnabled(False)
|
|
122
|
+
self.setAcceptDrops(False)
|
|
123
|
+
self.setDragDropMode(QAbstractItemView.NoDragDrop)
|
|
124
|
+
self.setDropIndicatorShown(False)
|
|
125
|
+
self.unsetCursor()
|
|
126
|
+
|
|
127
|
+
def backup_selection(self):
|
|
128
|
+
"""
|
|
129
|
+
Persist selected preset identity (by ROLE_ID) instead of raw indexes.
|
|
130
|
+
"""
|
|
131
|
+
try:
|
|
132
|
+
sel_rows = self.selectionModel().selectedRows()
|
|
133
|
+
ids = []
|
|
134
|
+
for ix in sel_rows:
|
|
135
|
+
pid = ix.data(self.ROLE_ID)
|
|
136
|
+
if pid:
|
|
137
|
+
ids.append(str(pid))
|
|
138
|
+
self._saved_selection_ids = ids if ids else None
|
|
139
|
+
except Exception:
|
|
140
|
+
self._saved_selection_ids = None
|
|
141
|
+
|
|
142
|
+
def restore_selection(self):
|
|
143
|
+
"""
|
|
144
|
+
Restore selection by ROLE_ID to keep it attached to the same item regardless of position.
|
|
145
|
+
"""
|
|
146
|
+
ids = self._saved_selection_ids or []
|
|
147
|
+
self._saved_selection_ids = None
|
|
148
|
+
if not ids:
|
|
149
|
+
return
|
|
150
|
+
model = self.model()
|
|
151
|
+
if model is None:
|
|
152
|
+
return
|
|
153
|
+
target_id = ids[0]
|
|
154
|
+
sel_model = self.selectionModel()
|
|
155
|
+
prev_unlocked = self.unlocked
|
|
156
|
+
self.unlocked = True
|
|
157
|
+
try:
|
|
158
|
+
sel_model.clearSelection()
|
|
159
|
+
first_idx = None
|
|
160
|
+
for r in range(model.rowCount()):
|
|
161
|
+
idx = model.index(r, 0)
|
|
162
|
+
pid = idx.data(self.ROLE_ID)
|
|
163
|
+
if pid == target_id:
|
|
164
|
+
first_idx = idx
|
|
165
|
+
break
|
|
166
|
+
if first_idx is not None and first_idx.isValid():
|
|
167
|
+
sel_model.select(first_idx, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
|
|
168
|
+
self.setCurrentIndex(first_idx)
|
|
169
|
+
self.scrollTo(first_idx)
|
|
170
|
+
finally:
|
|
171
|
+
self.unlocked = prev_unlocked
|
|
172
|
+
|
|
173
|
+
def _current_selected_ids(self) -> list[str]:
|
|
174
|
+
"""Read current selection IDs (ROLE_ID)."""
|
|
175
|
+
try:
|
|
176
|
+
return [ix.data(self.ROLE_ID) for ix in self.selectionModel().selectedRows() if ix.data(self.ROLE_ID)]
|
|
177
|
+
except Exception:
|
|
178
|
+
return []
|
|
179
|
+
|
|
48
180
|
def click(self, val):
|
|
49
|
-
|
|
181
|
+
"""Row click handler; select by ID (stable under reordering)."""
|
|
182
|
+
if self._model_updating:
|
|
183
|
+
return
|
|
184
|
+
index = val
|
|
185
|
+
if not index.isValid():
|
|
186
|
+
return
|
|
187
|
+
preset_id = index.data(self.ROLE_ID)
|
|
188
|
+
if preset_id:
|
|
189
|
+
self.window.controller.presets.select_by_id(preset_id)
|
|
190
|
+
self.selection = self.selectionModel().selection()
|
|
191
|
+
return
|
|
192
|
+
row = index.row()
|
|
50
193
|
if row >= 0:
|
|
51
194
|
self.window.controller.presets.select(row)
|
|
52
195
|
self.selection = self.selectionModel().selection()
|
|
53
196
|
|
|
54
197
|
def dblclick(self, val):
|
|
55
|
-
"""
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
:param val: double click event
|
|
59
|
-
"""
|
|
198
|
+
"""Double click event"""
|
|
199
|
+
if self._model_updating:
|
|
200
|
+
return
|
|
60
201
|
row = val.row()
|
|
61
202
|
if row >= 0:
|
|
62
203
|
self.window.controller.presets.editor.edit(row)
|
|
63
204
|
|
|
64
205
|
def show_context_menu(self, pos: QPoint):
|
|
65
|
-
"""
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
:param pos: QPoint
|
|
69
|
-
"""
|
|
206
|
+
"""Context menu event"""
|
|
207
|
+
if self._model_updating:
|
|
208
|
+
return
|
|
70
209
|
global_pos = self.viewport().mapToGlobal(pos)
|
|
71
210
|
mode = self.window.core.config.get('mode')
|
|
72
211
|
index = self.indexAt(pos)
|
|
73
212
|
idx = index.row()
|
|
74
213
|
|
|
75
214
|
preset = None
|
|
215
|
+
preset_id = None
|
|
76
216
|
if idx >= 0:
|
|
77
217
|
preset_id = self.window.core.presets.get_by_idx(idx, mode)
|
|
78
218
|
if preset_id:
|
|
79
219
|
preset = self.window.core.presets.items.get(preset_id)
|
|
80
220
|
|
|
81
221
|
is_current = idx >= 0 and self.window.controller.presets.is_current(idx)
|
|
222
|
+
is_special = bool(index.data(self.ROLE_IS_SPECIAL)) if index.isValid() else False
|
|
82
223
|
|
|
83
224
|
if idx >= 0:
|
|
84
225
|
menu = QMenu(self)
|
|
@@ -100,6 +241,16 @@ class PresetList(BaseList):
|
|
|
100
241
|
duplicate_act = QAction(self._ICO_COPY, trans('preset.action.duplicate'), menu)
|
|
101
242
|
duplicate_act.triggered.connect(lambda checked=False, it=index: self.action_duplicate(it))
|
|
102
243
|
|
|
244
|
+
if self._dnd_enabled and not is_current and not is_special:
|
|
245
|
+
up_act = QAction(self._ICO_UP, trans('common.up'), menu)
|
|
246
|
+
down_act = QAction(self._ICO_DOWN, trans('common.down'), menu)
|
|
247
|
+
up_act.setEnabled(idx > 1)
|
|
248
|
+
down_act.setEnabled(idx < (self.model().rowCount() - 1))
|
|
249
|
+
up_act.triggered.connect(lambda checked=False, it=index: self.action_move_up(it))
|
|
250
|
+
down_act.triggered.connect(lambda checked=False, it=index: self.action_move_down(it))
|
|
251
|
+
menu.addAction(up_act)
|
|
252
|
+
menu.addAction(down_act)
|
|
253
|
+
|
|
103
254
|
if is_current:
|
|
104
255
|
edit_act.setEnabled(False)
|
|
105
256
|
restore_act = QAction(self._ICO_UNDO, trans('dialog.editor.btn.defaults'), menu)
|
|
@@ -115,10 +266,7 @@ class PresetList(BaseList):
|
|
|
115
266
|
self.selection = self.selectionModel().selection()
|
|
116
267
|
menu.exec_(global_pos)
|
|
117
268
|
|
|
118
|
-
# store previous scroll position
|
|
119
269
|
self.store_scroll_position()
|
|
120
|
-
|
|
121
|
-
# restore selection if it was backed up
|
|
122
270
|
if self.restore_after_ctx_menu:
|
|
123
271
|
if self._backup_selection is not None:
|
|
124
272
|
sel_model = self.selectionModel()
|
|
@@ -128,82 +276,340 @@ class PresetList(BaseList):
|
|
|
128
276
|
i, QItemSelectionModel.Select | QItemSelectionModel.Rows
|
|
129
277
|
)
|
|
130
278
|
self._backup_selection = None
|
|
131
|
-
|
|
132
|
-
# restore scroll position
|
|
133
279
|
self.restore_after_ctx_menu = True
|
|
134
280
|
self.restore_scroll_position()
|
|
135
281
|
|
|
136
282
|
def action_edit(self, item):
|
|
137
|
-
"""
|
|
138
|
-
Edit action handler
|
|
139
|
-
|
|
140
|
-
:param item: list item
|
|
141
|
-
"""
|
|
142
283
|
idx = item.row()
|
|
143
284
|
if idx >= 0:
|
|
144
|
-
self.restore_after_ctx_menu = False
|
|
285
|
+
self.restore_after_ctx_menu = False
|
|
145
286
|
self.window.controller.presets.editor.edit(idx)
|
|
146
287
|
|
|
147
288
|
def action_duplicate(self, item):
|
|
148
|
-
"""
|
|
149
|
-
Duplicate action handler
|
|
150
|
-
|
|
151
|
-
:param item: list item
|
|
152
|
-
"""
|
|
153
289
|
idx = item.row()
|
|
154
290
|
if idx >= 0:
|
|
155
|
-
self.restore_after_ctx_menu = False
|
|
291
|
+
self.restore_after_ctx_menu = False
|
|
156
292
|
self.window.controller.presets.duplicate(idx)
|
|
157
293
|
|
|
158
294
|
def action_delete(self, item):
|
|
159
|
-
"""
|
|
160
|
-
Delete action handler
|
|
161
|
-
|
|
162
|
-
:param item: list item
|
|
163
|
-
"""
|
|
164
295
|
idx = item.row()
|
|
165
296
|
if idx >= 0:
|
|
166
|
-
self.restore_after_ctx_menu = False
|
|
297
|
+
self.restore_after_ctx_menu = False
|
|
167
298
|
self.window.controller.presets.delete(idx)
|
|
168
299
|
|
|
169
300
|
def action_restore(self, item):
|
|
170
|
-
"""
|
|
171
|
-
Restore action handler
|
|
172
|
-
|
|
173
|
-
:param item: list item
|
|
174
|
-
"""
|
|
175
301
|
self.window.controller.presets.restore()
|
|
176
302
|
|
|
177
303
|
def action_enable(self, item):
|
|
178
|
-
"""
|
|
179
|
-
Enable action handler
|
|
180
|
-
|
|
181
|
-
:param item: list item
|
|
182
|
-
"""
|
|
183
304
|
idx = item.row()
|
|
184
305
|
if idx >= 0:
|
|
185
306
|
self.window.controller.presets.enable(idx)
|
|
186
307
|
|
|
187
308
|
def action_disable(self, item):
|
|
188
|
-
"""
|
|
189
|
-
Disable action handler
|
|
190
|
-
|
|
191
|
-
:param item: list item
|
|
192
|
-
"""
|
|
193
309
|
idx = item.row()
|
|
194
310
|
if idx >= 0:
|
|
195
311
|
self.window.controller.presets.disable(idx)
|
|
196
312
|
|
|
313
|
+
def action_move_up(self, item):
|
|
314
|
+
row = item.row()
|
|
315
|
+
if row <= 1:
|
|
316
|
+
return
|
|
317
|
+
self.restore_after_ctx_menu = False
|
|
318
|
+
# Select the moved element (exception rule for context Up)
|
|
319
|
+
moved_role_id = item.data(self.ROLE_ID)
|
|
320
|
+
if moved_role_id:
|
|
321
|
+
self._selection_override_ids = [moved_role_id]
|
|
322
|
+
# Keep controller in sync with the view selection
|
|
323
|
+
self.window.controller.presets.select_by_id(moved_role_id)
|
|
324
|
+
self._move_row(row, row - 1)
|
|
325
|
+
|
|
326
|
+
def action_move_down(self, item):
|
|
327
|
+
row = item.row()
|
|
328
|
+
if row < 0 or row >= (self.model().rowCount() - 1):
|
|
329
|
+
return
|
|
330
|
+
if row == 0:
|
|
331
|
+
return
|
|
332
|
+
self.restore_after_ctx_menu = False
|
|
333
|
+
# Select the moved element (exception rule for context Down)
|
|
334
|
+
moved_role_id = item.data(self.ROLE_ID)
|
|
335
|
+
if moved_role_id:
|
|
336
|
+
self._selection_override_ids = [moved_role_id]
|
|
337
|
+
# Keep controller in sync with the view selection
|
|
338
|
+
self.window.controller.presets.select_by_id(moved_role_id)
|
|
339
|
+
self._move_row(row, row + 1)
|
|
340
|
+
|
|
341
|
+
# ----------------------------
|
|
342
|
+
# Ordering helpers (core-based)
|
|
343
|
+
# ----------------------------
|
|
344
|
+
|
|
345
|
+
def _core_regular_ids_for_mode(self) -> list[str]:
|
|
346
|
+
"""Return current ordered preset IDs for mode, excluding pinned current.<mode>."""
|
|
347
|
+
mode = self.window.core.config.get('mode')
|
|
348
|
+
data = self.window.core.presets.get_by_mode(mode) or {}
|
|
349
|
+
ids = list(data.keys())
|
|
350
|
+
if ids and ids[0].startswith("current."):
|
|
351
|
+
ids = ids[1:]
|
|
352
|
+
return ids
|
|
353
|
+
|
|
354
|
+
def _core_regular_uuids_for_mode(self) -> list[str]:
|
|
355
|
+
"""UUID list resolved from core ordered IDs (excluding pinned)."""
|
|
356
|
+
ids = self._core_regular_ids_for_mode()
|
|
357
|
+
items = self.window.core.presets.items
|
|
358
|
+
out = []
|
|
359
|
+
for pid in ids:
|
|
360
|
+
it = items.get(pid)
|
|
361
|
+
if it and it.uuid:
|
|
362
|
+
out.append(it.uuid)
|
|
363
|
+
return out
|
|
364
|
+
|
|
365
|
+
def _collect_regular_uuids(self) -> list[str]:
|
|
366
|
+
"""Backward-compatible wrapper used by older code: now returns core-based UUIDs."""
|
|
367
|
+
return self._core_regular_uuids_for_mode()
|
|
368
|
+
|
|
369
|
+
def _is_row_selected(self, row: int) -> bool:
|
|
370
|
+
"""Check if given row is currently selected."""
|
|
371
|
+
try:
|
|
372
|
+
sel = self.selectionModel().selectedRows()
|
|
373
|
+
return any(ix.row() == row for ix in sel)
|
|
374
|
+
except Exception:
|
|
375
|
+
return False
|
|
376
|
+
|
|
377
|
+
def _reorder_and_persist(self, from_row: int, to_row: int) -> str:
|
|
378
|
+
"""
|
|
379
|
+
Compute new UUID order using core order (not the view), then persist it.
|
|
380
|
+
Returns moved preset ID (filename) for later selection if needed.
|
|
381
|
+
"""
|
|
382
|
+
if from_row <= 0 or to_row <= 0:
|
|
383
|
+
return ""
|
|
384
|
+
|
|
385
|
+
ids_seq = self._core_regular_ids_for_mode()
|
|
386
|
+
if not ids_seq:
|
|
387
|
+
return ""
|
|
388
|
+
|
|
389
|
+
i_from = from_row - 1
|
|
390
|
+
i_to = to_row - 1
|
|
391
|
+
if i_from < 0 or i_from >= len(ids_seq):
|
|
392
|
+
return ""
|
|
393
|
+
if i_to < 0:
|
|
394
|
+
i_to = 0
|
|
395
|
+
if i_to > len(ids_seq):
|
|
396
|
+
i_to = len(ids_seq)
|
|
397
|
+
|
|
398
|
+
moved_id = ids_seq[i_from]
|
|
399
|
+
seq_ids = list(ids_seq)
|
|
400
|
+
item = seq_ids.pop(i_from)
|
|
401
|
+
seq_ids.insert(i_to if i_to <= len(seq_ids) else len(seq_ids), item)
|
|
402
|
+
|
|
403
|
+
items = self.window.core.presets.items
|
|
404
|
+
uuids = [items[pid].uuid for pid in seq_ids if pid in items and items[pid].uuid]
|
|
405
|
+
mode = self.window.core.config.get('mode')
|
|
406
|
+
self.window.controller.presets.persist_order_for_mode(mode, uuids)
|
|
407
|
+
|
|
408
|
+
return moved_id
|
|
409
|
+
|
|
410
|
+
# ----------------------------
|
|
411
|
+
# Drag visuals (safe, no delegate painting)
|
|
412
|
+
# ----------------------------
|
|
413
|
+
|
|
414
|
+
def _drag_pixmap_for_index(self, index) -> QPixmap | None:
|
|
415
|
+
"""
|
|
416
|
+
Build a safe pixmap for dragged row without using delegate.paint (prevents crash).
|
|
417
|
+
"""
|
|
418
|
+
try:
|
|
419
|
+
text = str(index.data(Qt.DisplayRole) or "")
|
|
420
|
+
fm = self.fontMetrics()
|
|
421
|
+
w = max(fm.horizontalAdvance(text) + 24, 80)
|
|
422
|
+
h = max(fm.height() + 10, 24)
|
|
423
|
+
pm = QPixmap(w, h)
|
|
424
|
+
pm.fill(Qt.transparent)
|
|
425
|
+
|
|
426
|
+
painter = QPainter()
|
|
427
|
+
painter.begin(pm)
|
|
428
|
+
try:
|
|
429
|
+
# background bubble
|
|
430
|
+
bg = self.palette().base().color()
|
|
431
|
+
bg.setAlpha(220)
|
|
432
|
+
painter.fillRect(pm.rect(), bg)
|
|
433
|
+
# border
|
|
434
|
+
pen = QPen(QColor(0, 0, 0, 40))
|
|
435
|
+
painter.setPen(pen)
|
|
436
|
+
painter.drawRect(pm.rect().adjusted(0, 0, -1, -1))
|
|
437
|
+
# text
|
|
438
|
+
painter.setPen(self.palette().text().color())
|
|
439
|
+
painter.drawText(pm.rect().adjusted(8, 0, -8, 0), Qt.AlignVCenter | Qt.AlignLeft, text)
|
|
440
|
+
finally:
|
|
441
|
+
painter.end()
|
|
442
|
+
return pm
|
|
443
|
+
except Exception:
|
|
444
|
+
return None
|
|
445
|
+
|
|
446
|
+
def startDrag(self, supportedActions):
|
|
447
|
+
"""
|
|
448
|
+
Start drag with pixmap built from the actually dragged row (self._press_index).
|
|
449
|
+
Avoids using selection for drag visuals (no 'ghost' of another item).
|
|
450
|
+
"""
|
|
451
|
+
if not self._dnd_enabled or self._press_index is None or not self._press_index.isValid():
|
|
452
|
+
return super().startDrag(supportedActions)
|
|
453
|
+
|
|
454
|
+
model = self.model()
|
|
455
|
+
drag = QDrag(self)
|
|
456
|
+
# mime data from the pressed index (not from selection)
|
|
457
|
+
try:
|
|
458
|
+
mime = model.mimeData([self._press_index])
|
|
459
|
+
except Exception:
|
|
460
|
+
mime = QMimeData()
|
|
461
|
+
drag.setMimeData(mime)
|
|
462
|
+
|
|
463
|
+
pm = self._drag_pixmap_for_index(self._press_index)
|
|
464
|
+
if pm is not None:
|
|
465
|
+
drag.setPixmap(pm)
|
|
466
|
+
drag.setHotSpot(pm.rect().center())
|
|
467
|
+
|
|
468
|
+
drag.exec(Qt.MoveAction)
|
|
469
|
+
|
|
470
|
+
# ----------------------------
|
|
471
|
+
# Refresh & painting
|
|
472
|
+
# ----------------------------
|
|
473
|
+
|
|
474
|
+
def _force_full_repaint(self):
|
|
475
|
+
"""
|
|
476
|
+
Force a synchronous full repaint of the viewport and notify the view that data/layout could change.
|
|
477
|
+
This clears any stale drag visuals on some platforms/styles.
|
|
478
|
+
"""
|
|
479
|
+
model = self.model()
|
|
480
|
+
if model is not None and model.rowCount() > 0:
|
|
481
|
+
top = model.index(0, 0)
|
|
482
|
+
bottom = model.index(model.rowCount() - 1, 0)
|
|
483
|
+
try:
|
|
484
|
+
model.dataChanged.emit(top, bottom, [Qt.DisplayRole])
|
|
485
|
+
except Exception:
|
|
486
|
+
pass
|
|
487
|
+
try:
|
|
488
|
+
model.layoutChanged.emit()
|
|
489
|
+
except Exception:
|
|
490
|
+
pass
|
|
491
|
+
self.viewport().repaint()
|
|
492
|
+
|
|
493
|
+
def _refresh_after_order_change(self, moved_id: str, follow_selection: bool):
|
|
494
|
+
"""
|
|
495
|
+
Refresh the list from core order and keep selection/scroll stable.
|
|
496
|
+
|
|
497
|
+
For both DnD and context moves:
|
|
498
|
+
- if _selection_override_ids is set, layout will restore those IDs;
|
|
499
|
+
- otherwise, take current selected IDs and use them as override to ensure
|
|
500
|
+
selection 'follows element, not position'.
|
|
501
|
+
"""
|
|
502
|
+
if not self._selection_override_ids:
|
|
503
|
+
self._selection_override_ids = self._current_selected_ids()
|
|
504
|
+
|
|
505
|
+
self.store_scroll_position()
|
|
506
|
+
|
|
507
|
+
di_prev = self._dnd_enabled
|
|
508
|
+
self.setDropIndicatorShown(False)
|
|
509
|
+
self.setUpdatesEnabled(False)
|
|
510
|
+
try:
|
|
511
|
+
self.window.controller.presets.update_list()
|
|
512
|
+
self.restore_scroll_position()
|
|
513
|
+
finally:
|
|
514
|
+
self.setUpdatesEnabled(True)
|
|
515
|
+
if di_prev and self._dnd_enabled:
|
|
516
|
+
self.setDropIndicatorShown(True)
|
|
517
|
+
|
|
518
|
+
# Clear helpers for context menu (layout will consume _selection_override_ids)
|
|
519
|
+
self._ctx_menu_original_ids = None
|
|
520
|
+
self._backup_selection = None
|
|
521
|
+
|
|
522
|
+
QApplication.processEvents(QEventLoop.ExcludeUserInputEvents | QEventLoop.ExcludeSocketNotifiers)
|
|
523
|
+
self._force_full_repaint()
|
|
524
|
+
QTimer.singleShot(0, self.viewport().update)
|
|
525
|
+
|
|
526
|
+
def _apply_after_drop(self):
|
|
527
|
+
"""Execute deferred refresh after the drop event has fully finished in Qt."""
|
|
528
|
+
payload = self._pending_after_drop
|
|
529
|
+
self._pending_after_drop = None
|
|
530
|
+
if not payload:
|
|
531
|
+
return
|
|
532
|
+
moved_id, follow_selection = payload
|
|
533
|
+
self._refresh_after_order_change(moved_id, follow_selection)
|
|
534
|
+
# Activate moved preset in controller at the very end (deferred to avoid re-entrancy)
|
|
535
|
+
QTimer.singleShot(0, lambda mid=moved_id: self._finalize_select_after_drop(mid))
|
|
536
|
+
|
|
537
|
+
def _finalize_select_after_drop(self, moved_role_id: str):
|
|
538
|
+
"""
|
|
539
|
+
Final activation of the moved preset in controller after DnD completed and view got refreshed.
|
|
540
|
+
This is intentionally deferred to the next event loop tick.
|
|
541
|
+
"""
|
|
542
|
+
try:
|
|
543
|
+
pid = moved_role_id
|
|
544
|
+
if not pid:
|
|
545
|
+
ids = self._current_selected_ids()
|
|
546
|
+
pid = ids[0] if ids else ""
|
|
547
|
+
if pid:
|
|
548
|
+
self.window.controller.presets.select_by_id(pid)
|
|
549
|
+
except Exception:
|
|
550
|
+
pass
|
|
551
|
+
|
|
552
|
+
def _move_row(self, from_row: int, to_row: int):
|
|
553
|
+
"""Move row programmatically; persist order and keep selection attached to the same item."""
|
|
554
|
+
if from_row == to_row:
|
|
555
|
+
return
|
|
556
|
+
moved_id = self._reorder_and_persist(from_row, to_row)
|
|
557
|
+
self._refresh_after_order_change(moved_id, follow_selection=False)
|
|
558
|
+
|
|
559
|
+
# ----------------------------
|
|
560
|
+
# Mouse / DnD events
|
|
561
|
+
# ----------------------------
|
|
562
|
+
|
|
563
|
+
def _mouse_event_point(self, event):
|
|
564
|
+
if hasattr(event, "position"):
|
|
565
|
+
try:
|
|
566
|
+
p = event.position()
|
|
567
|
+
if hasattr(p, "toPoint"):
|
|
568
|
+
return p.toPoint()
|
|
569
|
+
except Exception:
|
|
570
|
+
pass
|
|
571
|
+
if hasattr(event, "pos"):
|
|
572
|
+
return event.pos()
|
|
573
|
+
return self.viewport().mapFromGlobal(QCursor.pos())
|
|
574
|
+
|
|
197
575
|
def mousePressEvent(self, event):
|
|
576
|
+
if self._model_updating:
|
|
577
|
+
event.ignore()
|
|
578
|
+
return
|
|
198
579
|
if event.button() == Qt.LeftButton:
|
|
199
|
-
index = self.indexAt(
|
|
580
|
+
index = self.indexAt(self._mouse_event_point(event))
|
|
200
581
|
if not index.isValid():
|
|
201
582
|
return
|
|
202
|
-
|
|
583
|
+
if self._dnd_enabled:
|
|
584
|
+
sel_model = self.selectionModel()
|
|
585
|
+
self._press_backup_selection = list(sel_model.selectedIndexes())
|
|
586
|
+
self._press_backup_current = self.currentIndex()
|
|
587
|
+
self._dragged_was_selected = any(ix.row() == index.row() for ix in self._press_backup_selection or [])
|
|
588
|
+
super().mousePressEvent(event)
|
|
589
|
+
# Keep old selection (do not auto-select dragged item yet)
|
|
590
|
+
sel_model.clearSelection()
|
|
591
|
+
for i in self._press_backup_selection or []:
|
|
592
|
+
sel_model.select(i, QItemSelectionModel.Select | QItemSelectionModel.Rows)
|
|
593
|
+
if self._press_backup_current and self._press_backup_current.isValid():
|
|
594
|
+
self.setCurrentIndex(self._press_backup_current)
|
|
595
|
+
self._press_pos = self._mouse_event_point(event)
|
|
596
|
+
self._press_index = index
|
|
597
|
+
self._drag_selection_applied = False
|
|
598
|
+
event.accept()
|
|
599
|
+
return
|
|
600
|
+
else:
|
|
601
|
+
super().mousePressEvent(event)
|
|
203
602
|
elif event.button() == Qt.RightButton:
|
|
204
|
-
index = self.indexAt(
|
|
603
|
+
index = self.indexAt(self._mouse_event_point(event))
|
|
205
604
|
if index.isValid():
|
|
206
605
|
sel_model = self.selectionModel()
|
|
606
|
+
# Save original IDs (before we temporarily select right-click row)
|
|
607
|
+
self._ctx_menu_original_ids = []
|
|
608
|
+
for ix in sel_model.selectedRows():
|
|
609
|
+
pid = ix.data(self.ROLE_ID)
|
|
610
|
+
if pid:
|
|
611
|
+
self._ctx_menu_original_ids.append(pid)
|
|
612
|
+
|
|
207
613
|
self._backup_selection = list(sel_model.selectedIndexes())
|
|
208
614
|
sel_model.clearSelection()
|
|
209
615
|
sel_model.select(
|
|
@@ -213,10 +619,188 @@ class PresetList(BaseList):
|
|
|
213
619
|
else:
|
|
214
620
|
super().mousePressEvent(event)
|
|
215
621
|
|
|
216
|
-
def
|
|
622
|
+
def mouseMoveEvent(self, event):
|
|
623
|
+
if self._model_updating:
|
|
624
|
+
return
|
|
625
|
+
if not self._dnd_enabled:
|
|
626
|
+
return super().mouseMoveEvent(event)
|
|
627
|
+
if self._press_index is None or self._press_pos is None:
|
|
628
|
+
return super().mouseMoveEvent(event)
|
|
629
|
+
if not (event.buttons() & Qt.LeftButton):
|
|
630
|
+
return super().mouseMoveEvent(event)
|
|
631
|
+
|
|
632
|
+
cur = self._mouse_event_point(event)
|
|
633
|
+
dist = (cur - self._press_pos).manhattanLength()
|
|
634
|
+
threshold = QApplication.startDragDistance()
|
|
635
|
+
if dist < threshold:
|
|
636
|
+
return
|
|
637
|
+
|
|
638
|
+
# Pin current.* at the top; prevent dragging it
|
|
639
|
+
if self._press_index.row() == 0 or bool(self._press_index.data(self.ROLE_IS_SPECIAL)):
|
|
640
|
+
return super().mouseMoveEvent(event)
|
|
641
|
+
|
|
642
|
+
# Exception rule: at the start of drag, select the dragged item (view-only to avoid re-entrancy)
|
|
643
|
+
if not self._drag_selection_applied:
|
|
644
|
+
try:
|
|
645
|
+
sel_model = self.selectionModel()
|
|
646
|
+
prev_unlocked = self.unlocked
|
|
647
|
+
self.unlocked = True
|
|
648
|
+
try:
|
|
649
|
+
sel_model.clearSelection()
|
|
650
|
+
sel_model.select(self._press_index, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
|
|
651
|
+
self.setCurrentIndex(self._press_index)
|
|
652
|
+
finally:
|
|
653
|
+
self.unlocked = prev_unlocked
|
|
654
|
+
except Exception:
|
|
655
|
+
pass
|
|
656
|
+
self._drag_selection_applied = True
|
|
657
|
+
|
|
658
|
+
self._dragging = True
|
|
659
|
+
self.setCursor(QCursor(Qt.ClosedHandCursor))
|
|
660
|
+
super().mouseMoveEvent(event)
|
|
661
|
+
|
|
662
|
+
def mouseReleaseEvent(self, event):
|
|
663
|
+
if self._model_updating:
|
|
664
|
+
event.ignore()
|
|
665
|
+
return
|
|
666
|
+
try:
|
|
667
|
+
if self._dnd_enabled and event.button() == Qt.LeftButton:
|
|
668
|
+
self.unsetCursor()
|
|
669
|
+
if not self._dragging:
|
|
670
|
+
idx = self.indexAt(self._mouse_event_point(event))
|
|
671
|
+
if idx.isValid():
|
|
672
|
+
pid = idx.data(self.ROLE_ID)
|
|
673
|
+
if pid:
|
|
674
|
+
self.window.controller.presets.select_by_id(pid)
|
|
675
|
+
else:
|
|
676
|
+
self.setCurrentIndex(idx)
|
|
677
|
+
self.window.controller.presets.select(idx.row())
|
|
678
|
+
finally:
|
|
679
|
+
self._press_pos = None
|
|
680
|
+
self._press_index = None
|
|
681
|
+
self._press_backup_selection = None
|
|
682
|
+
self._press_backup_current = None
|
|
683
|
+
self._dragging = False
|
|
684
|
+
self._dragged_was_selected = False
|
|
685
|
+
self._drag_selection_applied = False
|
|
686
|
+
super().mouseReleaseEvent(event)
|
|
687
|
+
|
|
688
|
+
def dragEnterEvent(self, event):
|
|
689
|
+
if self._model_updating:
|
|
690
|
+
event.ignore()
|
|
691
|
+
return
|
|
692
|
+
if not self._dnd_enabled:
|
|
693
|
+
return
|
|
694
|
+
event.setDropAction(Qt.MoveAction)
|
|
695
|
+
event.acceptProposedAction()
|
|
696
|
+
|
|
697
|
+
def dragLeaveEvent(self, event):
|
|
698
|
+
if self._model_updating:
|
|
699
|
+
event.ignore()
|
|
700
|
+
return
|
|
701
|
+
self.unsetCursor()
|
|
702
|
+
super().dragLeaveEvent(event)
|
|
703
|
+
|
|
704
|
+
def dragMoveEvent(self, event):
|
|
705
|
+
if self._model_updating:
|
|
706
|
+
event.ignore()
|
|
707
|
+
return
|
|
708
|
+
if not self._dnd_enabled:
|
|
709
|
+
return
|
|
710
|
+
event.setDropAction(Qt.MoveAction)
|
|
711
|
+
|
|
712
|
+
pos = self._mouse_event_point(event)
|
|
713
|
+
idx = self.indexAt(pos)
|
|
714
|
+
# Do not allow dropping into the pinned first row zone
|
|
715
|
+
if idx.isValid() and idx.row() == 0:
|
|
716
|
+
rect = self.visualRect(idx)
|
|
717
|
+
if pos.y() <= rect.center().y():
|
|
718
|
+
event.ignore()
|
|
719
|
+
return
|
|
720
|
+
event.acceptProposedAction()
|
|
721
|
+
|
|
722
|
+
def dropEvent(self, event):
|
|
217
723
|
"""
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
:param event: Event
|
|
724
|
+
Fully handle flat row-to-row move. Persist order and defer view rebuild to next event loop,
|
|
725
|
+
so Qt can finish DnD teardown (prevents temporary disappearance).
|
|
221
726
|
"""
|
|
727
|
+
if self._model_updating:
|
|
728
|
+
event.ignore()
|
|
729
|
+
return
|
|
730
|
+
if not self._dnd_enabled:
|
|
731
|
+
return super().dropEvent(event)
|
|
732
|
+
|
|
733
|
+
model = self.model()
|
|
734
|
+
if model is None:
|
|
735
|
+
event.ignore()
|
|
736
|
+
return
|
|
737
|
+
|
|
738
|
+
# Source row (from press index if available)
|
|
739
|
+
if self._press_index is not None and self._press_index.isValid():
|
|
740
|
+
from_row = self._press_index.row()
|
|
741
|
+
else:
|
|
742
|
+
cur = self.currentIndex()
|
|
743
|
+
from_row = cur.row() if cur.isValid() else -1
|
|
744
|
+
|
|
745
|
+
if from_row < 0:
|
|
746
|
+
event.ignore()
|
|
747
|
+
self.unsetCursor()
|
|
748
|
+
self._drag_selection_applied = False
|
|
749
|
+
return
|
|
750
|
+
|
|
751
|
+
# Target row
|
|
752
|
+
pos = self._mouse_event_point(event)
|
|
753
|
+
idx = self.indexAt(pos)
|
|
754
|
+
if not idx.isValid():
|
|
755
|
+
to_row = model.rowCount() # append
|
|
756
|
+
else:
|
|
757
|
+
rect = self.visualRect(idx)
|
|
758
|
+
to_row = idx.row()
|
|
759
|
+
if pos.y() > rect.center().y():
|
|
760
|
+
to_row += 1
|
|
761
|
+
|
|
762
|
+
# Keep first row pinned
|
|
763
|
+
if to_row <= 1:
|
|
764
|
+
to_row = 1
|
|
765
|
+
|
|
766
|
+
# Adjust when moving down (Qt inserts before position)
|
|
767
|
+
if to_row > from_row:
|
|
768
|
+
to_row -= 1
|
|
769
|
+
|
|
770
|
+
moved_id = self._reorder_and_persist(from_row, to_row)
|
|
771
|
+
|
|
772
|
+
# Defer the heavy refresh to the next event loop tick
|
|
773
|
+
self._pending_after_drop = (moved_id, False)
|
|
774
|
+
QTimer.singleShot(0, self._apply_after_drop)
|
|
775
|
+
|
|
776
|
+
# Properly finalize DnD in Qt and exit without mutating the model here
|
|
777
|
+
event.setDropAction(Qt.MoveAction)
|
|
778
|
+
event.acceptProposedAction()
|
|
779
|
+
self.unsetCursor()
|
|
780
|
+
self._drag_selection_applied = False
|
|
781
|
+
|
|
782
|
+
# ----------------------------
|
|
783
|
+
# Legacy helper (not used in new path)
|
|
784
|
+
# ----------------------------
|
|
785
|
+
|
|
786
|
+
def _persist_current_model_order(self):
|
|
787
|
+
"""Deprecated in favor of _reorder_and_persist; retained for backward compatibility if needed."""
|
|
788
|
+
model = self.model()
|
|
789
|
+
if model is None:
|
|
790
|
+
return
|
|
791
|
+
uuids = []
|
|
792
|
+
for i in range(model.rowCount()):
|
|
793
|
+
if i == 0:
|
|
794
|
+
continue
|
|
795
|
+
idx = model.index(i, 0)
|
|
796
|
+
u = idx.data(self.ROLE_UUID)
|
|
797
|
+
if u and isinstance(u, str):
|
|
798
|
+
uuids.append(u)
|
|
799
|
+
mode = self.window.core.config.get('mode')
|
|
800
|
+
self.window.controller.presets.persist_order_for_mode(mode, uuids)
|
|
801
|
+
|
|
802
|
+
def selectionCommand(self, index, event=None):
|
|
803
|
+
# Prevent selection changes while model is updating (guards against stale indexes)
|
|
804
|
+
if self._model_updating:
|
|
805
|
+
return QItemSelectionModel.NoUpdate
|
|
222
806
|
return super().selectionCommand(index, event)
|