pygpt-net 2.6.67__py3-none-any.whl → 2.7.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.
- pygpt_net/CHANGELOG.txt +20 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/controller/assistant/assistant.py +13 -8
- pygpt_net/controller/assistant/batch.py +29 -15
- pygpt_net/controller/assistant/files.py +19 -14
- pygpt_net/controller/assistant/store.py +63 -41
- pygpt_net/controller/attachment/attachment.py +45 -35
- pygpt_net/controller/chat/attachment.py +50 -39
- pygpt_net/controller/config/field/dictionary.py +26 -14
- pygpt_net/controller/ctx/common.py +27 -17
- pygpt_net/controller/ctx/ctx.py +185 -101
- pygpt_net/controller/files/files.py +101 -41
- pygpt_net/controller/idx/indexer.py +87 -31
- pygpt_net/controller/kernel/kernel.py +13 -2
- pygpt_net/controller/mode/mode.py +3 -3
- pygpt_net/controller/model/editor.py +70 -15
- pygpt_net/controller/model/importer.py +153 -54
- pygpt_net/controller/painter/common.py +43 -11
- pygpt_net/controller/painter/painter.py +2 -2
- pygpt_net/controller/presets/experts.py +68 -15
- pygpt_net/controller/presets/presets.py +72 -36
- pygpt_net/controller/settings/profile.py +76 -35
- pygpt_net/controller/settings/workdir.py +70 -39
- pygpt_net/core/assistants/files.py +20 -18
- pygpt_net/core/filesystem/actions.py +111 -10
- pygpt_net/core/filesystem/filesystem.py +72 -1
- pygpt_net/core/filesystem/packer.py +161 -1
- pygpt_net/core/idx/idx.py +12 -11
- pygpt_net/core/idx/worker.py +13 -1
- pygpt_net/core/image/image.py +2 -2
- pygpt_net/core/models/models.py +4 -4
- pygpt_net/core/profile/profile.py +13 -3
- pygpt_net/core/video/video.py +2 -3
- pygpt_net/data/config/config.json +3 -3
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/css/style.dark.css +45 -0
- pygpt_net/data/css/style.light.css +46 -0
- pygpt_net/data/locale/locale.de.ini +5 -1
- pygpt_net/data/locale/locale.en.ini +5 -1
- pygpt_net/data/locale/locale.es.ini +5 -1
- pygpt_net/data/locale/locale.fr.ini +5 -1
- pygpt_net/data/locale/locale.it.ini +5 -1
- pygpt_net/data/locale/locale.pl.ini +6 -2
- pygpt_net/data/locale/locale.uk.ini +5 -1
- pygpt_net/data/locale/locale.zh.ini +5 -1
- pygpt_net/provider/api/openai/__init__.py +4 -2
- pygpt_net/provider/core/config/patch.py +17 -1
- pygpt_net/tools/image_viewer/tool.py +17 -0
- pygpt_net/tools/text_editor/tool.py +9 -0
- pygpt_net/ui/__init__.py +2 -2
- pygpt_net/ui/dialog/preset.py +1 -0
- pygpt_net/ui/layout/ctx/ctx_list.py +16 -6
- pygpt_net/ui/layout/toolbox/image.py +2 -1
- pygpt_net/ui/layout/toolbox/indexes.py +2 -0
- pygpt_net/ui/layout/toolbox/video.py +5 -1
- pygpt_net/ui/main.py +3 -1
- pygpt_net/ui/widget/calendar/select.py +3 -3
- pygpt_net/ui/widget/draw/painter.py +238 -51
- pygpt_net/ui/widget/filesystem/explorer.py +1164 -142
- pygpt_net/ui/widget/lists/assistant.py +185 -24
- pygpt_net/ui/widget/lists/assistant_store.py +245 -42
- pygpt_net/ui/widget/lists/attachment.py +230 -47
- pygpt_net/ui/widget/lists/attachment_ctx.py +189 -33
- pygpt_net/ui/widget/lists/base_list_combo.py +2 -2
- pygpt_net/ui/widget/lists/context.py +1253 -70
- pygpt_net/ui/widget/lists/experts.py +110 -8
- pygpt_net/ui/widget/lists/model_editor.py +217 -14
- pygpt_net/ui/widget/lists/model_importer.py +125 -6
- pygpt_net/ui/widget/lists/preset.py +460 -71
- pygpt_net/ui/widget/lists/profile.py +149 -27
- pygpt_net/ui/widget/lists/uploaded.py +230 -38
- pygpt_net/ui/widget/option/combo.py +1211 -33
- pygpt_net/ui/widget/option/dictionary.py +35 -7
- {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/METADATA +22 -57
- {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/RECORD +78 -78
- {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/entry_points.txt +0 -0
|
@@ -6,7 +6,7 @@
|
|
|
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.12.27 21:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtCore import QPoint, QItemSelectionModel, Qt, QEventLoop, QTimer, QMimeData
|
|
@@ -17,6 +17,7 @@ from pygpt_net.core.types import (
|
|
|
17
17
|
MODE_EXPERT,
|
|
18
18
|
)
|
|
19
19
|
from pygpt_net.ui.widget.lists.base import BaseList
|
|
20
|
+
# Keep imports minimal and unchanged
|
|
20
21
|
from pygpt_net.utils import trans
|
|
21
22
|
|
|
22
23
|
|
|
@@ -56,7 +57,8 @@ class PresetList(BaseList):
|
|
|
56
57
|
self.setItemsExpandable(False)
|
|
57
58
|
self.setUniformRowHeights(True)
|
|
58
59
|
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
|
59
|
-
|
|
60
|
+
# ExtendedSelection enables Ctrl/Shift multi-select gestures
|
|
61
|
+
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
|
60
62
|
|
|
61
63
|
# Drag & drop state
|
|
62
64
|
self._dnd_enabled = False
|
|
@@ -75,6 +77,14 @@ class PresetList(BaseList):
|
|
|
75
77
|
self._dragging = False
|
|
76
78
|
self._dragged_was_selected = False
|
|
77
79
|
|
|
80
|
+
# Virtual multi-select: suppress single-item business click once (Ctrl/Shift)
|
|
81
|
+
self._suppress_item_click = False
|
|
82
|
+
# Flag for "virtual" Ctrl multi-select gesture
|
|
83
|
+
self._ctrl_multi_active = False
|
|
84
|
+
self._ctrl_multi_index = None
|
|
85
|
+
# Guard to detect Shift-click range selection and bypass single-select follow-ups
|
|
86
|
+
self._was_shift_click = False
|
|
87
|
+
|
|
78
88
|
# Mark that we already applied selection at drag start (one-shot per DnD)
|
|
79
89
|
self._drag_selection_applied = False
|
|
80
90
|
|
|
@@ -105,6 +115,14 @@ class PresetList(BaseList):
|
|
|
105
115
|
self._pending_scroll_value = None
|
|
106
116
|
self._pending_refocus_role_id = None
|
|
107
117
|
|
|
118
|
+
# RMB-anchored scroll guard (preserve viewport after destructive updates from context menu)
|
|
119
|
+
self._rmb_anchor_scroll_value = None
|
|
120
|
+
self._scroll_guard_active = False
|
|
121
|
+
self._pre_update_scroll_value = 0
|
|
122
|
+
self._connected_model = None
|
|
123
|
+
self._model_signals_connected = False
|
|
124
|
+
self._connect_model_signals_safely()
|
|
125
|
+
|
|
108
126
|
# -------- Public helpers to protect updates --------
|
|
109
127
|
|
|
110
128
|
def begin_model_update(self):
|
|
@@ -157,7 +175,7 @@ class PresetList(BaseList):
|
|
|
157
175
|
self._scroll_freeze_timer.start(max(50, int(ms)))
|
|
158
176
|
|
|
159
177
|
def _apply_pending_scroll(self):
|
|
160
|
-
"""Re-apply saved scroll position when frozen."""
|
|
178
|
+
"""Re-apply saved scroll position when frozen or guard is active."""
|
|
161
179
|
if self._pending_scroll_value is None:
|
|
162
180
|
return
|
|
163
181
|
try:
|
|
@@ -177,10 +195,10 @@ class PresetList(BaseList):
|
|
|
177
195
|
|
|
178
196
|
def scrollTo(self, index, hint=QAbstractItemView.EnsureVisible):
|
|
179
197
|
"""
|
|
180
|
-
Temporarily suppress automatic scrolling while frozen.
|
|
198
|
+
Temporarily suppress automatic scrolling while frozen or guard is active.
|
|
181
199
|
This prevents list jumping when selection triggers scrollTo during refresh.
|
|
182
200
|
"""
|
|
183
|
-
if self._scroll_freeze_depth > 0:
|
|
201
|
+
if self._scroll_freeze_depth > 0 or self._scroll_guard_active:
|
|
184
202
|
self._apply_pending_scroll()
|
|
185
203
|
return
|
|
186
204
|
return super().scrollTo(index, hint)
|
|
@@ -224,6 +242,149 @@ class PresetList(BaseList):
|
|
|
224
242
|
# Keep pending id for next attempt if apply failed
|
|
225
243
|
pass
|
|
226
244
|
|
|
245
|
+
# --------------------------------------------------------------------------
|
|
246
|
+
# RMB-anchored scroll guard: capture scroll at context open and restore after delete
|
|
247
|
+
# --------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
def _connect_model_signals_safely(self):
|
|
250
|
+
"""Connect model change signals once to preserve scroll after updates."""
|
|
251
|
+
model = self.model()
|
|
252
|
+
if model is None:
|
|
253
|
+
return
|
|
254
|
+
if self._connected_model is model and self._model_signals_connected:
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
if self._connected_model is not None and self._connected_model is not model:
|
|
258
|
+
try:
|
|
259
|
+
self._connected_model.rowsAboutToBeRemoved.disconnect(self._on_rows_about_to_be_removed)
|
|
260
|
+
except Exception:
|
|
261
|
+
pass
|
|
262
|
+
try:
|
|
263
|
+
self._connected_model.rowsRemoved.disconnect(self._on_rows_removed)
|
|
264
|
+
except Exception:
|
|
265
|
+
pass
|
|
266
|
+
try:
|
|
267
|
+
self._connected_model.modelAboutToBeReset.disconnect(self._on_model_about_to_be_reset)
|
|
268
|
+
except Exception:
|
|
269
|
+
pass
|
|
270
|
+
try:
|
|
271
|
+
self._connected_model.modelReset.disconnect(self._on_model_reset)
|
|
272
|
+
except Exception:
|
|
273
|
+
pass
|
|
274
|
+
try:
|
|
275
|
+
self._connected_model.layoutAboutToBeChanged.disconnect(self._on_layout_about_to_change)
|
|
276
|
+
except Exception:
|
|
277
|
+
pass
|
|
278
|
+
try:
|
|
279
|
+
self._connected_model.layoutChanged.disconnect(self._on_layout_changed)
|
|
280
|
+
except Exception:
|
|
281
|
+
pass
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
model.rowsAboutToBeRemoved.connect(self._on_rows_about_to_be_removed)
|
|
285
|
+
except Exception:
|
|
286
|
+
pass
|
|
287
|
+
try:
|
|
288
|
+
model.rowsRemoved.connect(self._on_rows_removed)
|
|
289
|
+
except Exception:
|
|
290
|
+
pass
|
|
291
|
+
try:
|
|
292
|
+
model.modelAboutToBeReset.connect(self._on_model_about_to_be_reset)
|
|
293
|
+
except Exception:
|
|
294
|
+
pass
|
|
295
|
+
try:
|
|
296
|
+
model.modelReset.connect(self._on_model_reset)
|
|
297
|
+
except Exception:
|
|
298
|
+
pass
|
|
299
|
+
try:
|
|
300
|
+
model.layoutAboutToBeChanged.connect(self._on_layout_about_to_change)
|
|
301
|
+
except Exception:
|
|
302
|
+
pass
|
|
303
|
+
try:
|
|
304
|
+
model.layoutChanged.connect(self._on_layout_changed)
|
|
305
|
+
except Exception:
|
|
306
|
+
pass
|
|
307
|
+
|
|
308
|
+
self._connected_model = model
|
|
309
|
+
self._model_signals_connected = True
|
|
310
|
+
|
|
311
|
+
def setModel(self, model):
|
|
312
|
+
"""Reconnect model signals when the view's model is replaced."""
|
|
313
|
+
super().setModel(model)
|
|
314
|
+
self._connect_model_signals_safely()
|
|
315
|
+
|
|
316
|
+
def _activate_scroll_guard(self, override_value: int | None = None):
|
|
317
|
+
"""Arm guard with provided anchor value (or current) and keep it until updates settle."""
|
|
318
|
+
try:
|
|
319
|
+
sb = self.verticalScrollBar()
|
|
320
|
+
except Exception:
|
|
321
|
+
sb = None
|
|
322
|
+
val = None
|
|
323
|
+
if override_value is not None:
|
|
324
|
+
val = int(override_value)
|
|
325
|
+
elif sb is not None:
|
|
326
|
+
val = sb.value()
|
|
327
|
+
if val is None:
|
|
328
|
+
val = 0
|
|
329
|
+
self._pre_update_scroll_value = val
|
|
330
|
+
self._pending_scroll_value = val
|
|
331
|
+
self._scroll_guard_active = True
|
|
332
|
+
|
|
333
|
+
def _schedule_scroll_restore(self):
|
|
334
|
+
"""Restore anchored scroll repeatedly to outlast selection-driven scrolls."""
|
|
335
|
+
if not self._scroll_guard_active:
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
def apply():
|
|
339
|
+
try:
|
|
340
|
+
sb = self.verticalScrollBar()
|
|
341
|
+
if sb is None:
|
|
342
|
+
return
|
|
343
|
+
target = min(self._pre_update_scroll_value, sb.maximum())
|
|
344
|
+
sb.setValue(target)
|
|
345
|
+
self._pending_scroll_value = target
|
|
346
|
+
except Exception:
|
|
347
|
+
pass
|
|
348
|
+
|
|
349
|
+
QTimer.singleShot(0, apply)
|
|
350
|
+
QTimer.singleShot(25, apply)
|
|
351
|
+
QTimer.singleShot(75, apply)
|
|
352
|
+
QTimer.singleShot(150, apply)
|
|
353
|
+
QTimer.singleShot(300, apply)
|
|
354
|
+
QTimer.singleShot(600, self._clear_scroll_guard)
|
|
355
|
+
|
|
356
|
+
def _clear_scroll_guard(self):
|
|
357
|
+
"""Clear RMB anchor guard."""
|
|
358
|
+
self._scroll_guard_active = False
|
|
359
|
+
self._rmb_anchor_scroll_value = None
|
|
360
|
+
|
|
361
|
+
# Model signal handlers
|
|
362
|
+
|
|
363
|
+
def _on_rows_about_to_be_removed(self, parent, start, end):
|
|
364
|
+
if self._scroll_guard_active:
|
|
365
|
+
# guard already armed by action; nothing else to do
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
def _on_rows_removed(self, parent, start, end):
|
|
369
|
+
if self._scroll_guard_active:
|
|
370
|
+
self._schedule_scroll_restore()
|
|
371
|
+
|
|
372
|
+
def _on_model_about_to_be_reset(self):
|
|
373
|
+
if self._scroll_guard_active:
|
|
374
|
+
return
|
|
375
|
+
|
|
376
|
+
def _on_model_reset(self):
|
|
377
|
+
if self._scroll_guard_active:
|
|
378
|
+
self._schedule_scroll_restore()
|
|
379
|
+
|
|
380
|
+
def _on_layout_about_to_change(self):
|
|
381
|
+
if self._scroll_guard_active:
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
def _on_layout_changed(self):
|
|
385
|
+
if self._scroll_guard_active:
|
|
386
|
+
self._schedule_scroll_restore()
|
|
387
|
+
|
|
227
388
|
# --------------------------------------------------------------------------
|
|
228
389
|
|
|
229
390
|
def set_dnd_enabled(self, enabled: bool):
|
|
@@ -303,6 +464,16 @@ class PresetList(BaseList):
|
|
|
303
464
|
"""Row click handler; select by ID (stable under reordering)."""
|
|
304
465
|
if self._model_updating:
|
|
305
466
|
return
|
|
467
|
+
|
|
468
|
+
# Suppress business click after virtual Ctrl/Shift selection
|
|
469
|
+
if self._suppress_item_click:
|
|
470
|
+
self._suppress_item_click = False
|
|
471
|
+
return
|
|
472
|
+
|
|
473
|
+
# Ignore business click if multiple rows are selected
|
|
474
|
+
if self._has_multi_selection():
|
|
475
|
+
return
|
|
476
|
+
|
|
306
477
|
index = val
|
|
307
478
|
if not index.isValid():
|
|
308
479
|
return
|
|
@@ -333,70 +504,184 @@ class PresetList(BaseList):
|
|
|
333
504
|
if row >= 0:
|
|
334
505
|
self.window.controller.presets.editor.edit(row)
|
|
335
506
|
|
|
507
|
+
# ----------------------------
|
|
508
|
+
# Selection helpers (multi / single)
|
|
509
|
+
# ----------------------------
|
|
510
|
+
|
|
511
|
+
def _selected_indexes(self):
|
|
512
|
+
"""Return list of selected row indexes (column 0)."""
|
|
513
|
+
try:
|
|
514
|
+
return list(self.selectionModel().selectedRows())
|
|
515
|
+
except Exception:
|
|
516
|
+
return []
|
|
517
|
+
|
|
518
|
+
def _selected_rows(self) -> list[int]:
|
|
519
|
+
"""Return list of selected row numbers."""
|
|
520
|
+
try:
|
|
521
|
+
return [ix.row() for ix in self.selectionModel().selectedRows()]
|
|
522
|
+
except Exception:
|
|
523
|
+
return []
|
|
524
|
+
|
|
525
|
+
def _selected_role_ids(self) -> list[str]:
|
|
526
|
+
"""Return list of selected ROLE_ID (stable IDs)."""
|
|
527
|
+
try:
|
|
528
|
+
out = []
|
|
529
|
+
for ix in self.selectionModel().selectedRows():
|
|
530
|
+
pid = ix.data(self.ROLE_ID)
|
|
531
|
+
if pid:
|
|
532
|
+
out.append(pid)
|
|
533
|
+
return out
|
|
534
|
+
except Exception:
|
|
535
|
+
return []
|
|
536
|
+
|
|
537
|
+
def _has_multi_selection(self) -> bool:
|
|
538
|
+
"""Check whether more than one row is selected."""
|
|
539
|
+
try:
|
|
540
|
+
return len(self.selectionModel().selectedRows()) > 1
|
|
541
|
+
except Exception:
|
|
542
|
+
return False
|
|
543
|
+
|
|
544
|
+
# ----------------------------
|
|
545
|
+
# Context menu
|
|
546
|
+
# ----------------------------
|
|
547
|
+
|
|
336
548
|
def show_context_menu(self, pos: QPoint):
|
|
337
549
|
"""Context menu event"""
|
|
338
550
|
if self._model_updating:
|
|
339
551
|
return
|
|
552
|
+
|
|
553
|
+
# Capture RMB anchor scroll value at the moment of opening the context menu
|
|
554
|
+
try:
|
|
555
|
+
self._rmb_anchor_scroll_value = self.verticalScrollBar().value()
|
|
556
|
+
except Exception:
|
|
557
|
+
self._rmb_anchor_scroll_value = None
|
|
558
|
+
|
|
340
559
|
global_pos = self.viewport().mapToGlobal(pos)
|
|
341
560
|
mode = self.window.core.config.get('mode')
|
|
342
561
|
index = self.indexAt(pos)
|
|
343
|
-
idx = index.row()
|
|
562
|
+
idx = index.row() if index.isValid() else -1
|
|
563
|
+
|
|
564
|
+
# Gather selection state
|
|
565
|
+
selected_idx_list = self._selected_indexes()
|
|
566
|
+
selected_ids = [ix.data(self.ROLE_ID) for ix in selected_idx_list if ix.data(self.ROLE_ID)]
|
|
567
|
+
selected_rows = [ix.row() for ix in selected_idx_list]
|
|
568
|
+
multi = len(selected_rows) > 1
|
|
344
569
|
|
|
570
|
+
# Allow menu on empty area only when multi-selection is active
|
|
571
|
+
if not index.isValid() and not multi:
|
|
572
|
+
return
|
|
573
|
+
|
|
574
|
+
# Resolve clicked item (for single)
|
|
345
575
|
preset = None
|
|
346
|
-
preset_id = None
|
|
347
576
|
if idx >= 0:
|
|
348
577
|
preset_id = self.window.core.presets.get_by_idx(idx, mode)
|
|
349
578
|
if preset_id:
|
|
350
579
|
preset = self.window.core.presets.items.get(preset_id)
|
|
351
580
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
581
|
+
# Determine special/current flags for single target
|
|
582
|
+
is_current_single = idx >= 0 and self.window.controller.presets.is_current(idx)
|
|
583
|
+
is_special_single = bool(index.data(self.ROLE_IS_SPECIAL)) if index.isValid() else False
|
|
584
|
+
|
|
585
|
+
# Build menu
|
|
586
|
+
menu = QMenu(self)
|
|
587
|
+
|
|
588
|
+
# Edit (only for single)
|
|
589
|
+
edit_act = QAction(self._ICO_EDIT, trans('preset.action.edit'), menu)
|
|
590
|
+
edit_act.triggered.connect(lambda checked=False, it=index: self.action_edit(it))
|
|
591
|
+
edit_act.setEnabled(idx >= 0 and not multi)
|
|
592
|
+
menu.addAction(edit_act)
|
|
593
|
+
|
|
594
|
+
# Enable / Disable (single or multi, expert mode; ignore current.*)
|
|
595
|
+
if mode == MODE_EXPERT:
|
|
596
|
+
if multi:
|
|
597
|
+
items = self.window.core.presets.items
|
|
598
|
+
any_enable = False
|
|
599
|
+
any_disable = False
|
|
600
|
+
for ix in selected_idx_list:
|
|
601
|
+
pid = ix.data(self.ROLE_ID)
|
|
602
|
+
if not pid:
|
|
603
|
+
continue
|
|
604
|
+
it = items.get(pid)
|
|
605
|
+
if not it:
|
|
606
|
+
continue
|
|
607
|
+
if getattr(it, "filename", "").startswith("current."):
|
|
608
|
+
continue
|
|
609
|
+
if getattr(it, "enabled", False):
|
|
610
|
+
any_disable = True
|
|
611
|
+
else:
|
|
612
|
+
any_enable = True
|
|
613
|
+
if any_enable:
|
|
364
614
|
enable_act = QAction(self._ICO_CHECK, trans('preset.action.enable'), menu)
|
|
365
|
-
enable_act.triggered.connect(lambda checked=False,
|
|
615
|
+
enable_act.triggered.connect(lambda checked=False, ids=list(selected_ids): self.action_enable(ids))
|
|
366
616
|
menu.addAction(enable_act)
|
|
367
|
-
|
|
617
|
+
if any_disable:
|
|
368
618
|
disable_act = QAction(self._ICO_CLOSE, trans('preset.action.disable'), menu)
|
|
369
|
-
disable_act.triggered.connect(lambda checked=False,
|
|
619
|
+
disable_act.triggered.connect(lambda checked=False, ids=list(selected_ids): self.action_disable(ids))
|
|
370
620
|
menu.addAction(disable_act)
|
|
371
|
-
|
|
372
|
-
|
|
621
|
+
else:
|
|
622
|
+
if preset and not getattr(preset, "filename", "").startswith("current."):
|
|
623
|
+
if not getattr(preset, "enabled", False):
|
|
624
|
+
enable_act = QAction(self._ICO_CHECK, trans('preset.action.enable'), menu)
|
|
625
|
+
enable_act.triggered.connect(lambda checked=False, it=index: self.action_enable(it))
|
|
626
|
+
menu.addAction(enable_act)
|
|
627
|
+
else:
|
|
628
|
+
disable_act = QAction(self._ICO_CLOSE, trans('preset.action.disable'), menu)
|
|
629
|
+
disable_act.triggered.connect(lambda checked=False, it=index: self.action_disable(it))
|
|
630
|
+
menu.addAction(disable_act)
|
|
631
|
+
|
|
632
|
+
# Duplicate (single or multi)
|
|
633
|
+
duplicate_act = QAction(self._ICO_COPY, trans('preset.action.duplicate'), menu)
|
|
634
|
+
if multi:
|
|
635
|
+
duplicate_act.triggered.connect(lambda checked=False, ids=list(selected_ids): self.action_duplicate(ids))
|
|
636
|
+
else:
|
|
373
637
|
duplicate_act.triggered.connect(lambda checked=False, it=index: self.action_duplicate(it))
|
|
374
638
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
639
|
+
# Up/Down only for single, non-current, non-special, and when DnD is on
|
|
640
|
+
if not multi and self._dnd_enabled and not is_current_single and not is_special_single and idx >= 0:
|
|
641
|
+
up_act = QAction(self._ICO_UP, trans('common.up'), menu)
|
|
642
|
+
down_act = QAction(self._ICO_DOWN, trans('common.down'), menu)
|
|
643
|
+
up_act.setEnabled(idx > 1)
|
|
644
|
+
down_act.setEnabled(idx < (self.model().rowCount() - 1))
|
|
645
|
+
up_act.triggered.connect(lambda checked=False, it=index: self.action_move_up(it))
|
|
646
|
+
down_act.triggered.connect(lambda checked=False, it=index: self.action_move_down(it))
|
|
647
|
+
menu.addAction(up_act)
|
|
648
|
+
menu.addAction(down_act)
|
|
649
|
+
|
|
650
|
+
# Restore / Delete depending on current / multi
|
|
651
|
+
if not multi and is_current_single:
|
|
652
|
+
edit_act.setEnabled(False)
|
|
653
|
+
restore_act = QAction(self._ICO_UNDO, trans('dialog.editor.btn.defaults'), menu)
|
|
654
|
+
restore_act.triggered.connect(lambda checked=False, it=index: self.action_restore(it))
|
|
655
|
+
menu.addAction(restore_act)
|
|
656
|
+
menu.addAction(duplicate_act)
|
|
657
|
+
else:
|
|
658
|
+
# In multi-selection: disable delete if any current.* or special in selection
|
|
659
|
+
can_delete_all = True
|
|
660
|
+
if multi:
|
|
661
|
+
for ix in selected_idx_list:
|
|
662
|
+
pid = ix.data(self.ROLE_ID)
|
|
663
|
+
if not pid:
|
|
664
|
+
continue
|
|
665
|
+
if pid.startswith("current.") or bool(ix.data(self.ROLE_IS_SPECIAL)):
|
|
666
|
+
can_delete_all = False
|
|
667
|
+
break
|
|
668
|
+
else:
|
|
669
|
+
can_delete_all = idx >= 0 and not is_current_single
|
|
670
|
+
|
|
671
|
+
delete_act = QAction(self._ICO_DELETE, trans('preset.action.delete'), menu)
|
|
672
|
+
if multi:
|
|
673
|
+
delete_act.triggered.connect(lambda checked=False, ids=list(selected_ids): self.action_delete(ids))
|
|
391
674
|
else:
|
|
392
|
-
delete_act = QAction(self._ICO_DELETE, trans('preset.action.delete'), menu)
|
|
393
675
|
delete_act.triggered.connect(lambda checked=False, it=index: self.action_delete(it))
|
|
394
|
-
|
|
395
|
-
menu.addAction(delete_act)
|
|
676
|
+
delete_act.setEnabled(can_delete_all)
|
|
396
677
|
|
|
397
|
-
|
|
398
|
-
menu.
|
|
678
|
+
menu.addAction(duplicate_act)
|
|
679
|
+
menu.addAction(delete_act)
|
|
399
680
|
|
|
681
|
+
self.selection = self.selectionModel().selection()
|
|
682
|
+
menu.exec_(global_pos)
|
|
683
|
+
|
|
684
|
+
# Restore selection after context menu if needed
|
|
400
685
|
self.store_scroll_position()
|
|
401
686
|
if self.restore_after_ctx_menu:
|
|
402
687
|
if self._backup_selection is not None:
|
|
@@ -410,6 +695,12 @@ class PresetList(BaseList):
|
|
|
410
695
|
self.restore_after_ctx_menu = True
|
|
411
696
|
self.restore_scroll_position()
|
|
412
697
|
|
|
698
|
+
# ----------------------------
|
|
699
|
+
# Context actions (single or multi)
|
|
700
|
+
# If 'item' is a QModelIndex -> single row (int will be passed to external code).
|
|
701
|
+
# If 'item' is a list/tuple -> multi; pass list of ROLE_ID strings to external code.
|
|
702
|
+
# ----------------------------
|
|
703
|
+
|
|
413
704
|
def action_edit(self, item):
|
|
414
705
|
idx = item.row()
|
|
415
706
|
if idx >= 0:
|
|
@@ -417,12 +708,23 @@ class PresetList(BaseList):
|
|
|
417
708
|
self.window.controller.presets.editor.edit(idx)
|
|
418
709
|
|
|
419
710
|
def action_duplicate(self, item):
|
|
711
|
+
if isinstance(item, (list, tuple)):
|
|
712
|
+
self.restore_after_ctx_menu = False
|
|
713
|
+
self.window.controller.presets.duplicate(list(item))
|
|
714
|
+
return
|
|
420
715
|
idx = item.row()
|
|
421
716
|
if idx >= 0:
|
|
422
717
|
self.restore_after_ctx_menu = False
|
|
423
718
|
self.window.controller.presets.duplicate(idx)
|
|
424
719
|
|
|
425
720
|
def action_delete(self, item):
|
|
721
|
+
# Preserve scroll at the RMB anchor position before delete
|
|
722
|
+
self._activate_scroll_guard(self._rmb_anchor_scroll_value)
|
|
723
|
+
|
|
724
|
+
if isinstance(item, (list, tuple)):
|
|
725
|
+
self.restore_after_ctx_menu = False
|
|
726
|
+
self.window.controller.presets.delete(list(item))
|
|
727
|
+
return
|
|
426
728
|
idx = item.row()
|
|
427
729
|
if idx >= 0:
|
|
428
730
|
self.restore_after_ctx_menu = False
|
|
@@ -432,11 +734,17 @@ class PresetList(BaseList):
|
|
|
432
734
|
self.window.controller.presets.restore()
|
|
433
735
|
|
|
434
736
|
def action_enable(self, item):
|
|
737
|
+
if isinstance(item, (list, tuple)):
|
|
738
|
+
self.window.controller.presets.enable(list(item))
|
|
739
|
+
return
|
|
435
740
|
idx = item.row()
|
|
436
741
|
if idx >= 0:
|
|
437
742
|
self.window.controller.presets.enable(idx)
|
|
438
743
|
|
|
439
744
|
def action_disable(self, item):
|
|
745
|
+
if isinstance(item, (list, tuple)):
|
|
746
|
+
self.window.controller.presets.disable(list(item))
|
|
747
|
+
return
|
|
440
748
|
idx = item.row()
|
|
441
749
|
if idx >= 0:
|
|
442
750
|
self.window.controller.presets.disable(idx)
|
|
@@ -834,12 +1142,48 @@ class PresetList(BaseList):
|
|
|
834
1142
|
if self._model_updating:
|
|
835
1143
|
event.ignore()
|
|
836
1144
|
return
|
|
1145
|
+
|
|
1146
|
+
# Ctrl+Left: virtual toggle without business click
|
|
1147
|
+
if event.button() == Qt.LeftButton and (event.modifiers() & Qt.ControlModifier):
|
|
1148
|
+
idx = self.indexAt(self._mouse_event_point(event))
|
|
1149
|
+
if idx.isValid():
|
|
1150
|
+
self._ctrl_multi_active = True
|
|
1151
|
+
self._ctrl_multi_index = idx
|
|
1152
|
+
self._suppress_item_click = True
|
|
1153
|
+
event.accept()
|
|
1154
|
+
return
|
|
1155
|
+
self._suppress_item_click = True
|
|
1156
|
+
event.accept()
|
|
1157
|
+
return
|
|
1158
|
+
|
|
1159
|
+
# Shift+Left: let Qt perform range selection (anchor -> clicked), suppress business click
|
|
1160
|
+
if event.button() == Qt.LeftButton and (event.modifiers() & Qt.ShiftModifier):
|
|
1161
|
+
idx = self.indexAt(self._mouse_event_point(event))
|
|
1162
|
+
if idx.isValid():
|
|
1163
|
+
self._suppress_item_click = True
|
|
1164
|
+
self._was_shift_click = True
|
|
1165
|
+
super().mousePressEvent(event) # default range selection
|
|
1166
|
+
return
|
|
1167
|
+
# Shift on empty area -> ignore silently
|
|
1168
|
+
self._suppress_item_click = True
|
|
1169
|
+
self._was_shift_click = True
|
|
1170
|
+
event.accept()
|
|
1171
|
+
return
|
|
1172
|
+
|
|
837
1173
|
if event.button() == Qt.LeftButton:
|
|
838
1174
|
index = self.indexAt(self._mouse_event_point(event))
|
|
839
|
-
|
|
840
|
-
|
|
1175
|
+
# When multiple are selected, a single plain click clears the multi-selection
|
|
1176
|
+
if self._has_multi_selection():
|
|
1177
|
+
sel_model = self.selectionModel()
|
|
1178
|
+
sel_model.clearSelection()
|
|
1179
|
+
if not index.isValid():
|
|
1180
|
+
event.accept()
|
|
1181
|
+
return
|
|
1182
|
+
# continue with default single selection for clicked row
|
|
1183
|
+
|
|
841
1184
|
# Freeze scroll for a moment to prevent jumps caused by selection-triggered refresh
|
|
842
|
-
|
|
1185
|
+
if index.isValid():
|
|
1186
|
+
self._freeze_scroll(250)
|
|
843
1187
|
if self._dnd_enabled:
|
|
844
1188
|
sel_model = self.selectionModel()
|
|
845
1189
|
self._press_backup_selection = list(sel_model.selectedIndexes())
|
|
@@ -859,22 +1203,29 @@ class PresetList(BaseList):
|
|
|
859
1203
|
return
|
|
860
1204
|
else:
|
|
861
1205
|
super().mousePressEvent(event)
|
|
1206
|
+
|
|
862
1207
|
elif event.button() == Qt.RightButton:
|
|
863
1208
|
index = self.indexAt(self._mouse_event_point(event))
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
self._ctx_menu_original_ids = []
|
|
868
|
-
for ix in sel_model.selectedRows():
|
|
869
|
-
pid = ix.data(self.ROLE_ID)
|
|
870
|
-
if pid:
|
|
871
|
-
self._ctx_menu_original_ids.append(pid)
|
|
1209
|
+
sel_model = self.selectionModel()
|
|
1210
|
+
selected_rows = [ix.row() for ix in sel_model.selectedRows()]
|
|
1211
|
+
multi = len(selected_rows) > 1
|
|
872
1212
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1213
|
+
# Save original IDs only if we are going to change selection
|
|
1214
|
+
if index.isValid():
|
|
1215
|
+
if multi and index.row() in selected_rows:
|
|
1216
|
+
# Keep existing multi-selection; do not alter selection on right click
|
|
1217
|
+
self._backup_selection = None
|
|
1218
|
+
self._ctx_menu_original_ids = [ix.data(self.ROLE_ID) for ix in sel_model.selectedRows() if ix.data(self.ROLE_ID)]
|
|
1219
|
+
else:
|
|
1220
|
+
# Right-click outside current selection (or not multi) -> select the clicked row temporarily
|
|
1221
|
+
self._ctx_menu_original_ids = []
|
|
1222
|
+
for ix in sel_model.selectedRows():
|
|
1223
|
+
pid = ix.data(self.ROLE_ID)
|
|
1224
|
+
if pid:
|
|
1225
|
+
self._ctx_menu_original_ids.append(pid)
|
|
1226
|
+
self._backup_selection = list(sel_model.selectedIndexes())
|
|
1227
|
+
sel_model.clearSelection()
|
|
1228
|
+
sel_model.select(index, QItemSelectionModel.Select | QItemSelectionModel.Rows)
|
|
878
1229
|
event.accept()
|
|
879
1230
|
else:
|
|
880
1231
|
super().mousePressEvent(event)
|
|
@@ -924,6 +1275,35 @@ class PresetList(BaseList):
|
|
|
924
1275
|
if self._model_updating:
|
|
925
1276
|
event.ignore()
|
|
926
1277
|
return
|
|
1278
|
+
|
|
1279
|
+
# If the click was a Shift-based range selection, bypass single-select synchronization
|
|
1280
|
+
if event.button() == Qt.LeftButton and self._was_shift_click:
|
|
1281
|
+
self._was_shift_click = False
|
|
1282
|
+
# Let Qt finish its own release processing; do not run our single-select logic
|
|
1283
|
+
super().mouseReleaseEvent(event)
|
|
1284
|
+
return
|
|
1285
|
+
|
|
1286
|
+
# Finish "virtual" Ctrl toggle on same row
|
|
1287
|
+
if event.button() == Qt.LeftButton and self._ctrl_multi_active:
|
|
1288
|
+
try:
|
|
1289
|
+
idx = self.indexAt(self._mouse_event_point(event))
|
|
1290
|
+
if idx.isValid() and self._ctrl_multi_index and idx == self._ctrl_multi_index:
|
|
1291
|
+
sel_model = self.selectionModel()
|
|
1292
|
+
sel_model.select(idx, QItemSelectionModel.Toggle | QItemSelectionModel.Rows)
|
|
1293
|
+
finally:
|
|
1294
|
+
self._ctrl_multi_active = False
|
|
1295
|
+
self._ctrl_multi_index = None
|
|
1296
|
+
# do not emit business click after Ctrl path
|
|
1297
|
+
self._press_pos = None
|
|
1298
|
+
self._press_index = None
|
|
1299
|
+
self._press_backup_selection = None
|
|
1300
|
+
self._press_backup_current = None
|
|
1301
|
+
self._dragging = False
|
|
1302
|
+
self._dragged_was_selected = False
|
|
1303
|
+
self._drag_selection_applied = False
|
|
1304
|
+
event.accept()
|
|
1305
|
+
return
|
|
1306
|
+
|
|
927
1307
|
try:
|
|
928
1308
|
if self._dnd_enabled and event.button() == Qt.LeftButton:
|
|
929
1309
|
self.unsetCursor()
|
|
@@ -931,17 +1311,21 @@ class PresetList(BaseList):
|
|
|
931
1311
|
if not self._dragging:
|
|
932
1312
|
idx = self.indexAt(self._mouse_event_point(event))
|
|
933
1313
|
if idx.isValid():
|
|
934
|
-
|
|
935
|
-
if
|
|
936
|
-
|
|
937
|
-
self._freeze_scroll(300)
|
|
938
|
-
self._pending_refocus_role_id = pid
|
|
939
|
-
self.window.controller.presets.select_by_id(pid)
|
|
940
|
-
QTimer.singleShot(0, self._apply_pending_refocus)
|
|
941
|
-
QTimer.singleShot(50, self._apply_pending_refocus)
|
|
1314
|
+
# Skip business selection if multi-selection is active (e.g., after Shift)
|
|
1315
|
+
if self._has_multi_selection():
|
|
1316
|
+
pass
|
|
942
1317
|
else:
|
|
943
|
-
self.
|
|
944
|
-
|
|
1318
|
+
pid = idx.data(self.ROLE_ID)
|
|
1319
|
+
if pid:
|
|
1320
|
+
# Keep scroll stable also for this late selection path
|
|
1321
|
+
self._freeze_scroll(300)
|
|
1322
|
+
self._pending_refocus_role_id = pid
|
|
1323
|
+
self.window.controller.presets.select_by_id(pid)
|
|
1324
|
+
QTimer.singleShot(0, self._apply_pending_refocus)
|
|
1325
|
+
QTimer.singleShot(50, self._apply_pending_refocus)
|
|
1326
|
+
else:
|
|
1327
|
+
self.setCurrentIndex(idx)
|
|
1328
|
+
self.window.controller.presets.select(idx.row())
|
|
945
1329
|
finally:
|
|
946
1330
|
self._press_pos = None
|
|
947
1331
|
self._press_index = None
|
|
@@ -1064,6 +1448,11 @@ class PresetList(BaseList):
|
|
|
1064
1448
|
self.window.controller.presets.persist_order_for_mode(mode, uuids)
|
|
1065
1449
|
|
|
1066
1450
|
def selectionCommand(self, index, event=None):
|
|
1451
|
+
"""
|
|
1452
|
+
Selection command
|
|
1453
|
+
:param index: Index
|
|
1454
|
+
:param event: Event
|
|
1455
|
+
"""
|
|
1067
1456
|
# Prevent selection changes while model is updating (guards against stale indexes)
|
|
1068
1457
|
if self._model_updating:
|
|
1069
1458
|
return QItemSelectionModel.NoUpdate
|