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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# list.py
|
|
2
|
+
|
|
1
3
|
#!/usr/bin/env python3
|
|
2
4
|
# -*- coding: utf-8 -*-
|
|
3
5
|
# ================================================== #
|
|
@@ -6,17 +8,117 @@
|
|
|
6
8
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
9
|
# MIT License #
|
|
8
10
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date:
|
|
11
|
+
# Updated Date: 2025.12.27 21:00:00 #
|
|
10
12
|
# ================================================== #
|
|
11
13
|
|
|
12
14
|
from PySide6 import QtCore
|
|
13
|
-
from PySide6.QtGui import QStandardItemModel
|
|
14
|
-
from PySide6.QtWidgets import QWidget, QHBoxLayout, QPushButton, QVBoxLayout, QLabel
|
|
15
|
+
from PySide6.QtGui import QStandardItemModel, QAction, QIcon
|
|
16
|
+
from PySide6.QtWidgets import QWidget, QHBoxLayout, QPushButton, QVBoxLayout, QLabel, QAbstractItemView, QMenu
|
|
17
|
+
from PySide6.QtCore import QItemSelectionModel
|
|
15
18
|
|
|
16
19
|
from pygpt_net.ui.widget.lists.base import BaseList
|
|
17
20
|
from pygpt_net.utils import trans
|
|
18
21
|
import pygpt_net.icons_rc
|
|
19
22
|
|
|
23
|
+
|
|
24
|
+
class ExpertsList(BaseList):
|
|
25
|
+
def __init__(self, window=None, id=None):
|
|
26
|
+
"""
|
|
27
|
+
Experts list view with virtual multi-select and context menu.
|
|
28
|
+
|
|
29
|
+
- ExtendedSelection enables Ctrl/Shift multi-select gestures.
|
|
30
|
+
- Single left click with no modifiers clears multi-selection when active.
|
|
31
|
+
- Right-click context menu:
|
|
32
|
+
* available list: Add
|
|
33
|
+
* selected list: Remove
|
|
34
|
+
"""
|
|
35
|
+
super(ExpertsList, self).__init__(window, id=id)
|
|
36
|
+
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
|
37
|
+
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
|
38
|
+
|
|
39
|
+
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
|
40
|
+
self.customContextMenuRequested.connect(self.show_context_menu)
|
|
41
|
+
self._backup_selection = None
|
|
42
|
+
self.restore_after_ctx_menu = True
|
|
43
|
+
|
|
44
|
+
def mousePressEvent(self, event):
|
|
45
|
+
"""
|
|
46
|
+
Clear multi-selection on a single left click (any position or empty area),
|
|
47
|
+
then proceed with default selection behavior for the clicked row.
|
|
48
|
+
"""
|
|
49
|
+
if event.button() == QtCore.Qt.LeftButton and event.modifiers() == QtCore.Qt.NoModifier:
|
|
50
|
+
sel_model = self.selectionModel()
|
|
51
|
+
if sel_model is not None and len(sel_model.selectedRows()) > 1:
|
|
52
|
+
index = self.indexAt(event.pos())
|
|
53
|
+
sel_model.clearSelection()
|
|
54
|
+
if not index.isValid():
|
|
55
|
+
event.accept()
|
|
56
|
+
return
|
|
57
|
+
super(ExpertsList, self).mousePressEvent(event)
|
|
58
|
+
|
|
59
|
+
def _selected_rows(self):
|
|
60
|
+
"""Return list of selected row indexes."""
|
|
61
|
+
try:
|
|
62
|
+
return list(self.selectionModel().selectedRows())
|
|
63
|
+
except Exception:
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
def show_context_menu(self, pos: QtCore.QPoint):
|
|
67
|
+
"""Context menu for available/selected experts lists."""
|
|
68
|
+
index = self.indexAt(pos)
|
|
69
|
+
selected = self._selected_rows()
|
|
70
|
+
|
|
71
|
+
if not index.isValid() and not selected:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
sel_model = self.selectionModel()
|
|
75
|
+
# Preserve multi-selection if RMB inside it; otherwise temporarily select clicked row
|
|
76
|
+
if index.isValid():
|
|
77
|
+
selected_rows = [ix.row() for ix in selected]
|
|
78
|
+
if len(selected_rows) > 1 and index.row() in selected_rows:
|
|
79
|
+
self._backup_selection = None
|
|
80
|
+
else:
|
|
81
|
+
self._backup_selection = list(sel_model.selectedIndexes())
|
|
82
|
+
sel_model.clearSelection()
|
|
83
|
+
sel_model.select(index, QItemSelectionModel.Select | QItemSelectionModel.Rows)
|
|
84
|
+
else:
|
|
85
|
+
self._backup_selection = None
|
|
86
|
+
|
|
87
|
+
menu = QMenu(self)
|
|
88
|
+
if self.id == "preset.experts.available":
|
|
89
|
+
act_add = QAction(QIcon(":/icons/add.svg"), trans('action.add'), menu)
|
|
90
|
+
act_add.triggered.connect(self._action_add)
|
|
91
|
+
act_add.setEnabled(len(self._selected_rows()) > 0)
|
|
92
|
+
menu.addAction(act_add)
|
|
93
|
+
elif self.id == "preset.experts.selected":
|
|
94
|
+
act_remove = QAction(QIcon(":/icons/close.svg"), trans('action.delete'), menu)
|
|
95
|
+
act_remove.triggered.connect(self._action_remove)
|
|
96
|
+
act_remove.setEnabled(len(self._selected_rows()) > 0)
|
|
97
|
+
menu.addAction(act_remove)
|
|
98
|
+
else:
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
global_pos = self.viewport().mapToGlobal(pos)
|
|
102
|
+
menu.exec_(global_pos)
|
|
103
|
+
|
|
104
|
+
if self.restore_after_ctx_menu and self._backup_selection is not None:
|
|
105
|
+
sel_model.clearSelection()
|
|
106
|
+
for i in self._backup_selection:
|
|
107
|
+
sel_model.select(i, QItemSelectionModel.Select | QItemSelectionModel.Rows)
|
|
108
|
+
self._backup_selection = None
|
|
109
|
+
self.restore_after_ctx_menu = True
|
|
110
|
+
|
|
111
|
+
def _action_add(self):
|
|
112
|
+
"""Add selected experts from available list (same as '>')."""
|
|
113
|
+
self.restore_after_ctx_menu = False
|
|
114
|
+
self.window.controller.presets.editor.experts.add_expert()
|
|
115
|
+
|
|
116
|
+
def _action_remove(self):
|
|
117
|
+
"""Remove selected experts from selected list (same as '<')."""
|
|
118
|
+
self.restore_after_ctx_menu = False
|
|
119
|
+
self.window.controller.presets.editor.experts.remove_expert()
|
|
120
|
+
|
|
121
|
+
|
|
20
122
|
class ExpertsEditor(QWidget):
|
|
21
123
|
def __init__(self, window=None):
|
|
22
124
|
"""
|
|
@@ -65,11 +167,11 @@ class ExpertsEditor(QWidget):
|
|
|
65
167
|
arrows_layout.addWidget(self.window.ui.nodes["preset.experts.remove"])
|
|
66
168
|
|
|
67
169
|
self.window.ui.nodes["preset.experts.available.label"] = QLabel(trans("preset.experts.available.label"))
|
|
68
|
-
self.window.ui.nodes["preset.experts.available"] =
|
|
170
|
+
self.window.ui.nodes["preset.experts.available"] = ExpertsList(self.window, id="preset.experts.available")
|
|
69
171
|
self.window.ui.nodes["preset.experts.available"].clicked.disconnect()
|
|
70
172
|
|
|
71
173
|
self.window.ui.nodes["preset.experts.selected.label"] = QLabel(trans("preset.experts.selected.label"))
|
|
72
|
-
self.window.ui.nodes["preset.experts.selected"] =
|
|
174
|
+
self.window.ui.nodes["preset.experts.selected"] = ExpertsList(self.window, id="preset.experts.selected")
|
|
73
175
|
self.window.ui.nodes["preset.experts.selected"].clicked.disconnect()
|
|
74
176
|
|
|
75
177
|
available_layout = QVBoxLayout()
|
|
@@ -100,7 +202,7 @@ class ExpertsEditor(QWidget):
|
|
|
100
202
|
self.window.ui.models[id].insertRow(i)
|
|
101
203
|
name = data[n].name + " [" + data[n].filename + "]"
|
|
102
204
|
index = self.window.ui.models[id].index(i, 0)
|
|
103
|
-
tooltip = data[n].
|
|
205
|
+
tooltip = data[n].uuid
|
|
104
206
|
self.window.ui.models[id].setData(index, tooltip, QtCore.Qt.ToolTipRole)
|
|
105
207
|
self.window.ui.models[id].setData(self.window.ui.models[id].index(i, 0), name)
|
|
106
208
|
i += 1
|
|
@@ -120,7 +222,7 @@ class ExpertsEditor(QWidget):
|
|
|
120
222
|
self.window.ui.models[id].insertRow(i)
|
|
121
223
|
name = data[n].name + " [" + data[n].filename + "]"
|
|
122
224
|
index = self.window.ui.models[id].index(i, 0)
|
|
123
|
-
tooltip = data[n].
|
|
225
|
+
tooltip = data[n].uuid
|
|
124
226
|
self.window.ui.models[id].setData(index, tooltip, QtCore.Qt.ToolTipRole)
|
|
125
227
|
self.window.ui.models[id].setData(self.window.ui.models[id].index(i, 0), name)
|
|
126
228
|
i += 1
|
|
@@ -134,4 +236,4 @@ class ExpertsEditor(QWidget):
|
|
|
134
236
|
:param selected: data with selected experts
|
|
135
237
|
"""
|
|
136
238
|
self.update_available(available)
|
|
137
|
-
self.update_selected(selected)
|
|
239
|
+
self.update_selected(selected)
|
|
@@ -6,14 +6,16 @@
|
|
|
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 20:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtGui import QAction, QIcon
|
|
13
|
-
from PySide6.
|
|
13
|
+
from PySide6.QtCore import Qt, QItemSelectionModel
|
|
14
|
+
from PySide6.QtWidgets import QMenu, QAbstractItemView
|
|
14
15
|
|
|
15
16
|
from pygpt_net.ui.widget.lists.base import BaseList
|
|
16
17
|
from pygpt_net.utils import trans
|
|
18
|
+
import pygpt_net.icons_rc
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
class ModelEditorList(BaseList):
|
|
@@ -28,9 +30,153 @@ class ModelEditorList(BaseList):
|
|
|
28
30
|
self.window = window
|
|
29
31
|
self.id = id
|
|
30
32
|
|
|
33
|
+
# Virtual multi-select helpers
|
|
34
|
+
self._suppress_item_click = False # suppress business click after Ctrl/Shift selection
|
|
35
|
+
self._ctrl_multi_active = False # Ctrl gesture in progress
|
|
36
|
+
self._ctrl_multi_index = None
|
|
37
|
+
self._was_shift_click = False # Shift range gesture
|
|
38
|
+
|
|
39
|
+
# Context menu selection backup (temporary right-click selection)
|
|
40
|
+
self._backup_selection = None
|
|
41
|
+
self.restore_after_ctx_menu = True
|
|
42
|
+
|
|
43
|
+
# Row-based multi-selection behavior
|
|
44
|
+
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
|
45
|
+
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
|
46
|
+
|
|
47
|
+
# Disable default BaseList click handler; business action is handled manually
|
|
48
|
+
self.clicked.disconnect(self.click)
|
|
49
|
+
|
|
50
|
+
# ----------------------------
|
|
51
|
+
# Selection helpers
|
|
52
|
+
# ----------------------------
|
|
53
|
+
|
|
54
|
+
def _selected_rows(self) -> list[int]:
|
|
55
|
+
"""Return list of selected row numbers."""
|
|
56
|
+
try:
|
|
57
|
+
return sorted([ix.row() for ix in self.selectionModel().selectedRows()])
|
|
58
|
+
except Exception:
|
|
59
|
+
return []
|
|
60
|
+
|
|
61
|
+
def _has_multi_selection(self) -> bool:
|
|
62
|
+
"""Check whether more than one row is selected."""
|
|
63
|
+
try:
|
|
64
|
+
return len(self.selectionModel().selectedRows()) > 1
|
|
65
|
+
except Exception:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
# ----------------------------
|
|
69
|
+
# Mouse events (virtual multi-select)
|
|
70
|
+
# ----------------------------
|
|
71
|
+
|
|
72
|
+
def mousePressEvent(self, event):
|
|
73
|
+
"""
|
|
74
|
+
Mouse press event
|
|
75
|
+
|
|
76
|
+
:param event: mouse event
|
|
77
|
+
"""
|
|
78
|
+
# Ctrl+Left: virtual toggle without business click
|
|
79
|
+
if event.button() == Qt.LeftButton and (event.modifiers() & Qt.ControlModifier):
|
|
80
|
+
idx = self.indexAt(event.pos())
|
|
81
|
+
if idx.isValid():
|
|
82
|
+
self._ctrl_multi_active = True
|
|
83
|
+
self._ctrl_multi_index = idx
|
|
84
|
+
self._suppress_item_click = True
|
|
85
|
+
event.accept()
|
|
86
|
+
return
|
|
87
|
+
self._suppress_item_click = True
|
|
88
|
+
event.accept()
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
# Shift+Left: let Qt perform range selection (anchor->clicked), suppress business click
|
|
92
|
+
if event.button() == Qt.LeftButton and (event.modifiers() & Qt.ShiftModifier):
|
|
93
|
+
idx = self.indexAt(event.pos())
|
|
94
|
+
self._suppress_item_click = True
|
|
95
|
+
self._was_shift_click = True
|
|
96
|
+
if idx.isValid():
|
|
97
|
+
super(ModelEditorList, self).mousePressEvent(event)
|
|
98
|
+
else:
|
|
99
|
+
event.accept()
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
# Plain left click
|
|
103
|
+
if event.button() == Qt.LeftButton:
|
|
104
|
+
idx = self.indexAt(event.pos())
|
|
105
|
+
|
|
106
|
+
# When multiple are selected, a single plain click clears the multi-selection.
|
|
107
|
+
if self._has_multi_selection():
|
|
108
|
+
sel_model = self.selectionModel()
|
|
109
|
+
sel_model.clearSelection()
|
|
110
|
+
if not idx.isValid():
|
|
111
|
+
event.accept()
|
|
112
|
+
return
|
|
113
|
+
# continue with default single selection for clicked row
|
|
114
|
+
|
|
115
|
+
super(ModelEditorList, self).mousePressEvent(event)
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
# Right click: prepare selection for context menu
|
|
119
|
+
if event.button() == Qt.RightButton:
|
|
120
|
+
idx = self.indexAt(event.pos())
|
|
121
|
+
sel_model = self.selectionModel()
|
|
122
|
+
selected_rows = [ix.row() for ix in sel_model.selectedRows()]
|
|
123
|
+
multi = len(selected_rows) > 1
|
|
124
|
+
|
|
125
|
+
if idx.isValid():
|
|
126
|
+
if multi and idx.row() in selected_rows:
|
|
127
|
+
# Keep existing multi-selection; do not alter selection on right click
|
|
128
|
+
self._backup_selection = None
|
|
129
|
+
else:
|
|
130
|
+
# Temporarily select the clicked row; backup previous selection to restore later
|
|
131
|
+
self._backup_selection = list(sel_model.selectedIndexes())
|
|
132
|
+
sel_model.clearSelection()
|
|
133
|
+
sel_model.select(idx, QItemSelectionModel.Select | QItemSelectionModel.Rows)
|
|
134
|
+
event.accept()
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
super(ModelEditorList, self).mousePressEvent(event)
|
|
138
|
+
|
|
139
|
+
def mouseReleaseEvent(self, event):
|
|
140
|
+
# If the click was a Shift-based range selection, bypass business click
|
|
141
|
+
if event.button() == Qt.LeftButton and self._was_shift_click:
|
|
142
|
+
self._was_shift_click = False
|
|
143
|
+
self._suppress_item_click = False
|
|
144
|
+
super(ModelEditorList, self).mouseReleaseEvent(event)
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
# Finish "virtual" Ctrl toggle on same row (no business click)
|
|
148
|
+
if event.button() == Qt.LeftButton and self._ctrl_multi_active:
|
|
149
|
+
try:
|
|
150
|
+
idx = self.indexAt(event.pos())
|
|
151
|
+
if idx.isValid() and self._ctrl_multi_index and idx == self._ctrl_multi_index:
|
|
152
|
+
sel_model = self.selectionModel()
|
|
153
|
+
sel_model.select(idx, QItemSelectionModel.Toggle | QItemSelectionModel.Rows)
|
|
154
|
+
finally:
|
|
155
|
+
self._ctrl_multi_active = False
|
|
156
|
+
self._ctrl_multi_index = None
|
|
157
|
+
self._suppress_item_click = False
|
|
158
|
+
event.accept()
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
# Plain left: perform business selection only for single selection
|
|
162
|
+
if event.button() == Qt.LeftButton:
|
|
163
|
+
idx = self.indexAt(event.pos())
|
|
164
|
+
if not self._has_multi_selection():
|
|
165
|
+
if idx.isValid() and not self._suppress_item_click:
|
|
166
|
+
self.window.controller.model.editor.select(idx.row())
|
|
167
|
+
self._suppress_item_click = False
|
|
168
|
+
super(ModelEditorList, self).mouseReleaseEvent(event)
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
super(ModelEditorList, self).mouseReleaseEvent(event)
|
|
172
|
+
|
|
31
173
|
def click(self, val):
|
|
32
|
-
|
|
33
|
-
|
|
174
|
+
# Not used; single-selection business click is handled in mouseReleaseEvent
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
# ----------------------------
|
|
178
|
+
# Context menu
|
|
179
|
+
# ----------------------------
|
|
34
180
|
|
|
35
181
|
def contextMenuEvent(self, event):
|
|
36
182
|
"""
|
|
@@ -40,25 +186,82 @@ class ModelEditorList(BaseList):
|
|
|
40
186
|
"""
|
|
41
187
|
actions = {}
|
|
42
188
|
actions['delete'] = QAction(QIcon(":/icons/delete.svg"), trans('action.delete'), self)
|
|
43
|
-
actions['
|
|
44
|
-
lambda: self.action_delete(event))
|
|
189
|
+
actions['duplicate'] = QAction(QIcon(":/icons/copy.svg"), trans('action.duplicate'), self)
|
|
45
190
|
|
|
46
191
|
menu = QMenu(self)
|
|
192
|
+
menu.addAction(actions['duplicate'])
|
|
47
193
|
menu.addAction(actions['delete'])
|
|
48
194
|
|
|
49
|
-
|
|
50
|
-
idx =
|
|
51
|
-
|
|
52
|
-
|
|
195
|
+
index = self.indexAt(event.pos())
|
|
196
|
+
idx = index.row() if index.isValid() else -1
|
|
197
|
+
|
|
198
|
+
# Selection state for multi / single
|
|
199
|
+
selected_rows = self._selected_rows()
|
|
200
|
+
multi = len(selected_rows) > 1
|
|
201
|
+
|
|
202
|
+
# Allow menu on empty area only when multi-selection is active
|
|
203
|
+
if not index.isValid() and not multi:
|
|
204
|
+
if self._backup_selection is not None and self.restore_after_ctx_menu:
|
|
205
|
+
sel_model = self.selectionModel()
|
|
206
|
+
sel_model.clearSelection()
|
|
207
|
+
for i in self._backup_selection:
|
|
208
|
+
sel_model.select(i, QItemSelectionModel.Select | QItemSelectionModel.Rows)
|
|
209
|
+
self._backup_selection = None
|
|
210
|
+
return
|
|
53
211
|
|
|
54
|
-
|
|
212
|
+
# Route actions: pass list on multi, int on single
|
|
213
|
+
if multi:
|
|
214
|
+
actions['duplicate'].triggered.connect(lambda: self.action_duplicate(list(selected_rows)))
|
|
215
|
+
actions['delete'].triggered.connect(lambda: self.action_delete(list(selected_rows)))
|
|
216
|
+
else:
|
|
217
|
+
actions['duplicate'].triggered.connect(lambda: self.action_duplicate(idx))
|
|
218
|
+
actions['delete'].triggered.connect(lambda: self.action_delete(idx))
|
|
219
|
+
|
|
220
|
+
menu.exec_(event.globalPos())
|
|
221
|
+
|
|
222
|
+
# Restore selection after context menu if it was temporarily changed
|
|
223
|
+
if self.restore_after_ctx_menu and self._backup_selection is not None:
|
|
224
|
+
sel_model = self.selectionModel()
|
|
225
|
+
sel_model.clearSelection()
|
|
226
|
+
for i in self._backup_selection:
|
|
227
|
+
sel_model.select(i, QItemSelectionModel.Select | QItemSelectionModel.Rows)
|
|
228
|
+
self._backup_selection = None
|
|
229
|
+
self.restore_after_ctx_menu = True
|
|
230
|
+
|
|
231
|
+
# ----------------------------
|
|
232
|
+
# Context actions (single or multi)
|
|
233
|
+
# If 'item' is a list/tuple -> pass list of row ints to external code.
|
|
234
|
+
# If 'item' is an int -> pass single row int to external code.
|
|
235
|
+
# ----------------------------
|
|
236
|
+
|
|
237
|
+
def action_delete(self, item):
|
|
55
238
|
"""
|
|
56
239
|
Delete action handler
|
|
57
240
|
|
|
58
|
-
:param
|
|
241
|
+
:param item: int row or list of rows
|
|
59
242
|
"""
|
|
60
|
-
item
|
|
61
|
-
|
|
243
|
+
if isinstance(item, (list, tuple)):
|
|
244
|
+
if item:
|
|
245
|
+
self.restore_after_ctx_menu = False
|
|
246
|
+
self.window.controller.model.editor.delete_by_idx(list(item))
|
|
247
|
+
return
|
|
248
|
+
idx = int(item)
|
|
62
249
|
if idx >= 0:
|
|
250
|
+
self.restore_after_ctx_menu = False
|
|
63
251
|
self.window.controller.model.editor.delete_by_idx(idx)
|
|
64
252
|
|
|
253
|
+
def action_duplicate(self, item):
|
|
254
|
+
"""
|
|
255
|
+
Duplicate action handler
|
|
256
|
+
|
|
257
|
+
:param item: int row or list of rows
|
|
258
|
+
"""
|
|
259
|
+
if isinstance(item, (list, tuple)):
|
|
260
|
+
if item:
|
|
261
|
+
self.restore_after_ctx_menu = False
|
|
262
|
+
self.window.controller.model.editor.duplicate_by_idx(list(item))
|
|
263
|
+
return
|
|
264
|
+
idx = int(item)
|
|
265
|
+
if idx >= 0:
|
|
266
|
+
self.restore_after_ctx_menu = False
|
|
267
|
+
self.window.controller.model.editor.duplicate_by_idx(idx)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# models_list.py
|
|
2
|
+
|
|
1
3
|
#!/usr/bin/env python3
|
|
2
4
|
# -*- coding: utf-8 -*-
|
|
3
5
|
# ================================================== #
|
|
@@ -6,16 +8,129 @@
|
|
|
6
8
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
9
|
# MIT License #
|
|
8
10
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2025.
|
|
11
|
+
# Updated Date: 2025.12.27 21:00:00 #
|
|
10
12
|
# ================================================== #
|
|
11
13
|
|
|
12
14
|
from PySide6 import QtCore
|
|
13
|
-
from PySide6.QtGui import QStandardItemModel
|
|
14
|
-
from PySide6.QtWidgets import QWidget, QHBoxLayout, QPushButton, QVBoxLayout, QLabel, QCheckBox
|
|
15
|
+
from PySide6.QtGui import QStandardItemModel, QAction, QIcon
|
|
16
|
+
from PySide6.QtWidgets import QWidget, QHBoxLayout, QPushButton, QVBoxLayout, QLabel, QCheckBox, QAbstractItemView, QMenu
|
|
15
17
|
|
|
16
18
|
from pygpt_net.ui.widget.lists.base import BaseList
|
|
17
19
|
from pygpt_net.utils import trans
|
|
18
20
|
|
|
21
|
+
|
|
22
|
+
class ImporterList(BaseList):
|
|
23
|
+
def __init__(self, window=None, id=None):
|
|
24
|
+
"""
|
|
25
|
+
Importer list view with virtual multi-select and context menu.
|
|
26
|
+
|
|
27
|
+
- ExtendedSelection enables Ctrl/Shift multi-select gestures.
|
|
28
|
+
- Single left click with no modifiers clears multi-selection when active.
|
|
29
|
+
- Right-click context menu:
|
|
30
|
+
* available list: Import
|
|
31
|
+
* current list: Remove
|
|
32
|
+
"""
|
|
33
|
+
super(ImporterList, self).__init__(window, id=id)
|
|
34
|
+
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
|
35
|
+
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
|
36
|
+
|
|
37
|
+
# RMB context menu
|
|
38
|
+
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
|
39
|
+
self.customContextMenuRequested.connect(self.show_context_menu)
|
|
40
|
+
self._backup_selection = None
|
|
41
|
+
self.restore_after_ctx_menu = True
|
|
42
|
+
|
|
43
|
+
def mousePressEvent(self, event):
|
|
44
|
+
"""
|
|
45
|
+
Clear multi-selection on a single left click (any position or empty area),
|
|
46
|
+
then proceed with default selection behavior for the clicked row.
|
|
47
|
+
"""
|
|
48
|
+
if event.button() == QtCore.Qt.LeftButton and event.modifiers() == QtCore.Qt.NoModifier:
|
|
49
|
+
sel_model = self.selectionModel()
|
|
50
|
+
if sel_model is not None and len(sel_model.selectedRows()) > 1:
|
|
51
|
+
index = self.indexAt(event.pos())
|
|
52
|
+
sel_model.clearSelection()
|
|
53
|
+
if not index.isValid():
|
|
54
|
+
event.accept()
|
|
55
|
+
return
|
|
56
|
+
super(ImporterList, self).mousePressEvent(event)
|
|
57
|
+
|
|
58
|
+
def _selected_rows(self):
|
|
59
|
+
"""Return list of selected row indexes."""
|
|
60
|
+
try:
|
|
61
|
+
return list(self.selectionModel().selectedRows())
|
|
62
|
+
except Exception:
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
def show_context_menu(self, pos: QtCore.QPoint):
|
|
66
|
+
"""Context menu for available/current lists."""
|
|
67
|
+
index = self.indexAt(pos)
|
|
68
|
+
selected = self._selected_rows()
|
|
69
|
+
multi = len(selected) > 1
|
|
70
|
+
|
|
71
|
+
# Allow menu on empty area only when selection exists
|
|
72
|
+
if not index.isValid() and not selected:
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
sel_model = self.selectionModel()
|
|
76
|
+
|
|
77
|
+
# If right-clicked inside current multi-selection -> keep it
|
|
78
|
+
# Else select the row under cursor temporarily
|
|
79
|
+
if index.isValid():
|
|
80
|
+
if multi:
|
|
81
|
+
if index not in selected:
|
|
82
|
+
self._backup_selection = list(sel_model.selectedIndexes())
|
|
83
|
+
sel_model.clearSelection()
|
|
84
|
+
sel_model.select(index, sel_model.Select | sel_model.Rows)
|
|
85
|
+
else:
|
|
86
|
+
self._backup_selection = None
|
|
87
|
+
else:
|
|
88
|
+
if not selected or index not in selected:
|
|
89
|
+
self._backup_selection = list(sel_model.selectedIndexes())
|
|
90
|
+
sel_model.clearSelection()
|
|
91
|
+
sel_model.select(index, sel_model.Select | sel_model.Rows)
|
|
92
|
+
else:
|
|
93
|
+
self._backup_selection = None
|
|
94
|
+
else:
|
|
95
|
+
# empty area with some selection -> keep selection
|
|
96
|
+
self._backup_selection = None
|
|
97
|
+
|
|
98
|
+
menu = QMenu(self)
|
|
99
|
+
if self.id == "models.importer.available":
|
|
100
|
+
act_import = QAction(QIcon(":/icons/add.svg"), trans("action.import"), menu)
|
|
101
|
+
act_import.triggered.connect(self._action_import)
|
|
102
|
+
act_import.setEnabled(len(self._selected_rows()) > 0)
|
|
103
|
+
menu.addAction(act_import)
|
|
104
|
+
elif self.id == "models.importer.current":
|
|
105
|
+
act_remove = QAction(QIcon(":/icons/close.svg"), trans("action.delete"), menu)
|
|
106
|
+
act_remove.triggered.connect(self._action_remove)
|
|
107
|
+
act_remove.setEnabled(len(self._selected_rows()) > 0)
|
|
108
|
+
menu.addAction(act_remove)
|
|
109
|
+
else:
|
|
110
|
+
return # unknown list; no menu
|
|
111
|
+
|
|
112
|
+
global_pos = self.viewport().mapToGlobal(pos)
|
|
113
|
+
menu.exec_(global_pos)
|
|
114
|
+
|
|
115
|
+
# Restore original selection if it was temporarily changed and no action executed
|
|
116
|
+
if self.restore_after_ctx_menu and self._backup_selection is not None:
|
|
117
|
+
sel_model.clearSelection()
|
|
118
|
+
for i in self._backup_selection:
|
|
119
|
+
sel_model.select(i, sel_model.Select | sel_model.Rows)
|
|
120
|
+
self._backup_selection = None
|
|
121
|
+
self.restore_after_ctx_menu = True
|
|
122
|
+
|
|
123
|
+
def _action_import(self):
|
|
124
|
+
"""Import selected models from available list (same as '>')."""
|
|
125
|
+
self.restore_after_ctx_menu = False
|
|
126
|
+
self.window.controller.model.importer.add()
|
|
127
|
+
|
|
128
|
+
def _action_remove(self):
|
|
129
|
+
"""Remove selected models from current list (same as '<')."""
|
|
130
|
+
self.restore_after_ctx_menu = False
|
|
131
|
+
self.window.controller.model.importer.remove()
|
|
132
|
+
|
|
133
|
+
|
|
19
134
|
class ModelImporter(QWidget):
|
|
20
135
|
def __init__(self, window=None):
|
|
21
136
|
"""
|
|
@@ -66,11 +181,13 @@ class ModelImporter(QWidget):
|
|
|
66
181
|
arrows_layout.addWidget(self.window.ui.nodes["models.importer.remove"])
|
|
67
182
|
|
|
68
183
|
self.window.ui.nodes["models.importer.available.label"] = QLabel(trans("models.importer.available.label"))
|
|
69
|
-
|
|
184
|
+
# importer-specific list with virtual multi-select + context menu
|
|
185
|
+
self.window.ui.nodes["models.importer.available"] = ImporterList(self.window, id="models.importer.available")
|
|
70
186
|
self.window.ui.nodes["models.importer.available"].clicked.disconnect()
|
|
71
187
|
|
|
72
188
|
self.window.ui.nodes["models.importer.current.label"] = QLabel(trans("models.importer.current.label"))
|
|
73
|
-
|
|
189
|
+
# importer-specific list with virtual multi-select + context menu
|
|
190
|
+
self.window.ui.nodes["models.importer.current"] = ImporterList(self.window, id="models.importer.current")
|
|
74
191
|
self.window.ui.nodes["models.importer.current"].clicked.disconnect()
|
|
75
192
|
|
|
76
193
|
self.window.ui.nodes["models.importer.available.all"] = QCheckBox(trans("models.importer.all"), self.window)
|
|
@@ -110,6 +227,7 @@ class ModelImporter(QWidget):
|
|
|
110
227
|
name += f" ({data[n].name})"
|
|
111
228
|
index = self.window.ui.models[id].index(i, 0)
|
|
112
229
|
tooltip = data[n].id
|
|
230
|
+
# store model ID in tooltip role for stable retrieval from view
|
|
113
231
|
self.window.ui.models[id].setData(index, tooltip, QtCore.Qt.ToolTipRole)
|
|
114
232
|
self.window.ui.models[id].setData(self.window.ui.models[id].index(i, 0), name)
|
|
115
233
|
i += 1
|
|
@@ -134,6 +252,7 @@ class ModelImporter(QWidget):
|
|
|
134
252
|
name = "* " + name # mark imported models
|
|
135
253
|
index = self.window.ui.models[id].index(i, 0)
|
|
136
254
|
tooltip = data[n].id
|
|
255
|
+
# store model ID in tooltip role for stable retrieval from view
|
|
137
256
|
self.window.ui.models[id].setData(index, tooltip, QtCore.Qt.ToolTipRole)
|
|
138
257
|
self.window.ui.models[id].setData(self.window.ui.models[id].index(i, 0), name)
|
|
139
258
|
i += 1
|
|
@@ -147,4 +266,4 @@ class ModelImporter(QWidget):
|
|
|
147
266
|
:param current: data with current models
|
|
148
267
|
"""
|
|
149
268
|
self.update_available(available)
|
|
150
|
-
self.update_current(current)
|
|
269
|
+
self.update_current(current)
|