pygpt-net 2.6.66__py3-none-any.whl → 2.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. pygpt_net/CHANGELOG.txt +18 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/assistant/assistant.py +13 -8
  4. pygpt_net/controller/assistant/batch.py +29 -15
  5. pygpt_net/controller/assistant/files.py +19 -14
  6. pygpt_net/controller/assistant/store.py +63 -41
  7. pygpt_net/controller/attachment/attachment.py +45 -35
  8. pygpt_net/controller/chat/attachment.py +50 -39
  9. pygpt_net/controller/config/field/dictionary.py +26 -14
  10. pygpt_net/controller/config/field/textarea.py +2 -2
  11. pygpt_net/controller/ctx/common.py +27 -17
  12. pygpt_net/controller/ctx/ctx.py +182 -101
  13. pygpt_net/controller/dialogs/info.py +2 -2
  14. pygpt_net/controller/files/files.py +101 -41
  15. pygpt_net/controller/idx/indexer.py +87 -31
  16. pygpt_net/controller/kernel/kernel.py +13 -2
  17. pygpt_net/controller/media/media.py +29 -1
  18. pygpt_net/controller/mode/mode.py +3 -3
  19. pygpt_net/controller/model/editor.py +141 -21
  20. pygpt_net/controller/model/importer.py +153 -54
  21. pygpt_net/controller/painter/painter.py +2 -2
  22. pygpt_net/controller/presets/experts.py +68 -15
  23. pygpt_net/controller/presets/presets.py +72 -36
  24. pygpt_net/controller/settings/editor.py +25 -1
  25. pygpt_net/controller/settings/profile.py +76 -35
  26. pygpt_net/controller/settings/workdir.py +70 -39
  27. pygpt_net/core/assistants/files.py +20 -18
  28. pygpt_net/core/filesystem/actions.py +111 -10
  29. pygpt_net/core/filesystem/filesystem.py +2 -1
  30. pygpt_net/core/idx/idx.py +12 -11
  31. pygpt_net/core/idx/worker.py +13 -1
  32. pygpt_net/core/models/models.py +4 -4
  33. pygpt_net/core/profile/profile.py +13 -3
  34. pygpt_net/core/types/image.py +10 -1
  35. pygpt_net/core/video/video.py +43 -3
  36. pygpt_net/data/config/config.json +3 -3
  37. pygpt_net/data/config/models.json +25 -14
  38. pygpt_net/data/css/style.dark.css +39 -1
  39. pygpt_net/data/css/style.light.css +39 -1
  40. pygpt_net/data/locale/locale.de.ini +4 -1
  41. pygpt_net/data/locale/locale.en.ini +4 -1
  42. pygpt_net/data/locale/locale.es.ini +4 -1
  43. pygpt_net/data/locale/locale.fr.ini +4 -1
  44. pygpt_net/data/locale/locale.it.ini +4 -1
  45. pygpt_net/data/locale/locale.pl.ini +5 -2
  46. pygpt_net/data/locale/locale.uk.ini +4 -1
  47. pygpt_net/data/locale/locale.zh.ini +4 -1
  48. pygpt_net/item/model.py +1 -1
  49. pygpt_net/provider/api/openai/__init__.py +4 -2
  50. pygpt_net/provider/api/openai/video.py +2 -2
  51. pygpt_net/provider/core/config/patch.py +9 -1
  52. pygpt_net/provider/core/model/patch.py +26 -1
  53. pygpt_net/tools/image_viewer/tool.py +17 -0
  54. pygpt_net/tools/text_editor/tool.py +9 -0
  55. pygpt_net/ui/__init__.py +2 -2
  56. pygpt_net/ui/dialog/models.py +10 -1
  57. pygpt_net/ui/layout/ctx/ctx_list.py +16 -6
  58. pygpt_net/ui/layout/toolbox/video.py +14 -6
  59. pygpt_net/ui/main.py +3 -1
  60. pygpt_net/ui/widget/calendar/select.py +3 -3
  61. pygpt_net/ui/widget/filesystem/explorer.py +1082 -142
  62. pygpt_net/ui/widget/lists/assistant.py +185 -24
  63. pygpt_net/ui/widget/lists/assistant_store.py +245 -42
  64. pygpt_net/ui/widget/lists/attachment.py +230 -47
  65. pygpt_net/ui/widget/lists/attachment_ctx.py +189 -33
  66. pygpt_net/ui/widget/lists/base_list_combo.py +2 -2
  67. pygpt_net/ui/widget/lists/context.py +1253 -70
  68. pygpt_net/ui/widget/lists/experts.py +110 -8
  69. pygpt_net/ui/widget/lists/model_editor.py +217 -14
  70. pygpt_net/ui/widget/lists/model_importer.py +125 -6
  71. pygpt_net/ui/widget/lists/preset.py +460 -71
  72. pygpt_net/ui/widget/lists/profile.py +149 -27
  73. pygpt_net/ui/widget/lists/uploaded.py +230 -38
  74. pygpt_net/ui/widget/option/combo.py +1046 -32
  75. pygpt_net/ui/widget/option/dictionary.py +35 -7
  76. pygpt_net/ui/widget/option/input.py +3 -1
  77. {pygpt_net-2.6.66.dist-info → pygpt_net-2.7.0.dist-info}/METADATA +20 -57
  78. {pygpt_net-2.6.66.dist-info → pygpt_net-2.7.0.dist-info}/RECORD +81 -81
  79. {pygpt_net-2.6.66.dist-info → pygpt_net-2.7.0.dist-info}/LICENSE +0 -0
  80. {pygpt_net-2.6.66.dist-info → pygpt_net-2.7.0.dist-info}/WHEEL +0 -0
  81. {pygpt_net-2.6.66.dist-info → pygpt_net-2.7.0.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: 2024.05.01 17:00:00 #
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"] = BaseList(self.window)
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"] = BaseList(self.window)
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].model + ", " + 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].model + ", " + 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.08.24 23:00:00 #
9
+ # Updated Date: 2025.12.27 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtGui import QAction, QIcon
13
- from PySide6.QtWidgets import QMenu
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
- idx = val.row()
33
- self.window.controller.model.editor.select(idx)
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['delete'].triggered.connect(
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
- item = self.indexAt(event.pos())
50
- idx = item.row()
51
- if idx >= 0:
52
- menu.exec_(event.globalPos())
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
- def action_delete(self, event):
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 event: mouse event
241
+ :param item: int row or list of rows
59
242
  """
60
- item = self.indexAt(event.pos())
61
- idx = item.row()
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.08.24 23:00:00 #
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
- self.window.ui.nodes["models.importer.available"] = BaseList(self.window)
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
- self.window.ui.nodes["models.importer.current"] = BaseList(self.window)
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)