pygpt-net 2.6.67__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 (69) hide show
  1. pygpt_net/CHANGELOG.txt +12 -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/ctx/common.py +27 -17
  11. pygpt_net/controller/ctx/ctx.py +182 -101
  12. pygpt_net/controller/files/files.py +101 -41
  13. pygpt_net/controller/idx/indexer.py +87 -31
  14. pygpt_net/controller/kernel/kernel.py +13 -2
  15. pygpt_net/controller/mode/mode.py +3 -3
  16. pygpt_net/controller/model/editor.py +70 -15
  17. pygpt_net/controller/model/importer.py +153 -54
  18. pygpt_net/controller/painter/painter.py +2 -2
  19. pygpt_net/controller/presets/experts.py +68 -15
  20. pygpt_net/controller/presets/presets.py +72 -36
  21. pygpt_net/controller/settings/profile.py +76 -35
  22. pygpt_net/controller/settings/workdir.py +70 -39
  23. pygpt_net/core/assistants/files.py +20 -18
  24. pygpt_net/core/filesystem/actions.py +111 -10
  25. pygpt_net/core/filesystem/filesystem.py +2 -1
  26. pygpt_net/core/idx/idx.py +12 -11
  27. pygpt_net/core/idx/worker.py +13 -1
  28. pygpt_net/core/models/models.py +4 -4
  29. pygpt_net/core/profile/profile.py +13 -3
  30. pygpt_net/data/config/config.json +3 -3
  31. pygpt_net/data/config/models.json +3 -3
  32. pygpt_net/data/css/style.dark.css +39 -1
  33. pygpt_net/data/css/style.light.css +39 -1
  34. pygpt_net/data/locale/locale.de.ini +3 -1
  35. pygpt_net/data/locale/locale.en.ini +3 -1
  36. pygpt_net/data/locale/locale.es.ini +3 -1
  37. pygpt_net/data/locale/locale.fr.ini +3 -1
  38. pygpt_net/data/locale/locale.it.ini +3 -1
  39. pygpt_net/data/locale/locale.pl.ini +4 -2
  40. pygpt_net/data/locale/locale.uk.ini +3 -1
  41. pygpt_net/data/locale/locale.zh.ini +3 -1
  42. pygpt_net/provider/api/openai/__init__.py +4 -2
  43. pygpt_net/provider/core/config/patch.py +9 -1
  44. pygpt_net/tools/image_viewer/tool.py +17 -0
  45. pygpt_net/tools/text_editor/tool.py +9 -0
  46. pygpt_net/ui/__init__.py +2 -2
  47. pygpt_net/ui/layout/ctx/ctx_list.py +16 -6
  48. pygpt_net/ui/main.py +3 -1
  49. pygpt_net/ui/widget/calendar/select.py +3 -3
  50. pygpt_net/ui/widget/filesystem/explorer.py +1082 -142
  51. pygpt_net/ui/widget/lists/assistant.py +185 -24
  52. pygpt_net/ui/widget/lists/assistant_store.py +245 -42
  53. pygpt_net/ui/widget/lists/attachment.py +230 -47
  54. pygpt_net/ui/widget/lists/attachment_ctx.py +189 -33
  55. pygpt_net/ui/widget/lists/base_list_combo.py +2 -2
  56. pygpt_net/ui/widget/lists/context.py +1253 -70
  57. pygpt_net/ui/widget/lists/experts.py +110 -8
  58. pygpt_net/ui/widget/lists/model_editor.py +217 -14
  59. pygpt_net/ui/widget/lists/model_importer.py +125 -6
  60. pygpt_net/ui/widget/lists/preset.py +460 -71
  61. pygpt_net/ui/widget/lists/profile.py +149 -27
  62. pygpt_net/ui/widget/lists/uploaded.py +230 -38
  63. pygpt_net/ui/widget/option/combo.py +1046 -32
  64. pygpt_net/ui/widget/option/dictionary.py +35 -7
  65. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/METADATA +14 -57
  66. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/RECORD +69 -69
  67. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/LICENSE +0 -0
  68. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/WHEEL +0 -0
  69. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/entry_points.txt +0 -0
@@ -6,11 +6,12 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.24 23:00:00 #
9
+ # Updated Date: 2025.12.28 04:00:00 #
10
10
  # ================================================== #
11
11
 
12
+ from PySide6.QtCore import Qt, QItemSelectionModel
12
13
  from PySide6.QtGui import QAction, QIcon
13
- from PySide6.QtWidgets import QMenu
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
@@ -28,50 +29,158 @@ class ProfileList(BaseList):
28
29
  self.window = window
29
30
  self.id = id
30
31
 
32
+ # Enable row-based multi-select with native Ctrl/Shift gestures
33
+ self.setSelectionBehavior(QAbstractItemView.SelectRows)
34
+ self.setSelectionMode(QAbstractItemView.ExtendedSelection)
35
+
36
+ # Context menu restore helper
37
+ self._backup_selection = None
38
+ self.restore_after_ctx_menu = True
39
+
40
+ def _selected_rows(self) -> list[int]:
41
+ """Return list of selected row numbers."""
42
+ try:
43
+ return [ix.row() for ix in self.selectionModel().selectedRows()]
44
+ except Exception:
45
+ return []
46
+
47
+ def _has_multi_selection(self) -> bool:
48
+ """Return True when multiple rows are selected."""
49
+ try:
50
+ return len(self.selectionModel().selectedRows()) > 1
51
+ except Exception:
52
+ return False
53
+
54
+ def mousePressEvent(self, event):
55
+ """
56
+ Mouse press event
57
+ - Ctrl: let Qt toggle selection (virtual; no business action).
58
+ - Shift: let Qt range-select (virtual).
59
+ - If multiple are selected: a single left click anywhere clears selection.
60
+ """
61
+ if event.button() == Qt.LeftButton and (event.modifiers() & Qt.ControlModifier):
62
+ idx = self.indexAt(event.pos())
63
+ if idx.isValid():
64
+ super(ProfileList, self).mousePressEvent(event) # native toggle
65
+ else:
66
+ event.accept()
67
+ return
68
+
69
+ if event.button() == Qt.LeftButton and (event.modifiers() & Qt.ShiftModifier):
70
+ idx = self.indexAt(event.pos())
71
+ if idx.isValid():
72
+ super(ProfileList, self).mousePressEvent(event) # native range
73
+ else:
74
+ event.accept()
75
+ return
76
+
77
+ if event.button() == Qt.LeftButton:
78
+ index = self.indexAt(event.pos())
79
+ # Clear multi-selection with a single click (also on empty area)
80
+ if self._has_multi_selection():
81
+ sel_model = self.selectionModel()
82
+ sel_model.clearSelection()
83
+ if not index.isValid():
84
+ event.accept()
85
+ return
86
+ # Default selection behavior
87
+ super(ProfileList, self).mousePressEvent(event)
88
+ return
89
+
90
+ super(ProfileList, self).mousePressEvent(event)
91
+
31
92
  def click(self, val):
32
93
  pass
33
94
 
34
95
  def contextMenuEvent(self, event):
35
96
  """
36
97
  Context menu event
37
-
38
- :param event: context menu event
98
+ - Shows menu on a row, or on empty area when multi-selection is active.
99
+ - For multi-selection, actions pass list[int] of selected rows.
100
+ - For single selection, actions pass single int (legacy behavior).
39
101
  """
40
102
  actions = {}
41
103
  actions['use'] = QAction(QIcon(":/icons/check.svg"), trans('action.use'), self)
42
- actions['use'].triggered.connect(
43
- lambda: self.action_use(event))
44
104
  actions['edit'] = QAction(QIcon(":/icons/edit.svg"), trans('action.edit'), self)
45
- actions['edit'].triggered.connect(
46
- lambda: self.action_edit(event))
47
105
  actions['duplicate'] = QAction(QIcon(":/icons/copy.svg"), trans('action.duplicate'), self)
48
- actions['duplicate'].triggered.connect(
49
- lambda: self.action_duplicate(event))
50
-
51
-
52
106
  actions['reset'] = QAction(QIcon(":/icons/close.svg"), trans('action.reset'), self)
53
- actions['reset'].triggered.connect(
54
- lambda: self.action_reset(event))
55
107
  actions['delete'] = QAction(QIcon(":/icons/delete.svg"), trans('action.profile.delete'), self)
56
- actions['delete'].triggered.connect(
57
- lambda: self.action_delete(event))
58
108
  actions['delete_all'] = QAction(QIcon(":/icons/delete.svg"), trans('action.profile.delete_all'), self)
59
- actions['delete_all'].triggered.connect(
60
- lambda: self.action_delete_all(event))
109
+
110
+ # Hit test
111
+ index = self.indexAt(event.pos())
112
+ idx = index.row() if index.isValid() else -1
113
+
114
+ # Current selection state
115
+ sel_model = self.selectionModel()
116
+ selected_indexes = list(sel_model.selectedRows()) if sel_model else []
117
+ selected_rows = [ix.row() for ix in selected_indexes]
118
+ multi = len(selected_rows) > 1
119
+
120
+ # Allow menu on empty area only when multi-selection is active
121
+ if not index.isValid() and not multi:
122
+ return
123
+
124
+ # If right-click outside the current multi-selection, temporarily select clicked row
125
+ backup_selection = None
126
+ if index.isValid():
127
+ if multi and idx in selected_rows:
128
+ backup_selection = None # keep selection as-is
129
+ else:
130
+ backup_selection = list(sel_model.selectedIndexes())
131
+ sel_model.clearSelection()
132
+ sel_model.select(index, QItemSelectionModel.Select | QItemSelectionModel.Rows)
133
+ selected_rows = [idx]
134
+ multi = False
61
135
 
62
136
  menu = QMenu(self)
63
- menu.addAction(actions['edit'])
64
- menu.addAction(actions['use'])
65
- menu.addAction(actions['duplicate'])
66
- menu.addAction(actions['reset'])
67
- menu.addAction(actions['delete'])
68
- menu.addAction(actions['delete_all'])
69
137
 
70
- item = self.indexAt(event.pos())
71
- idx = item.row()
72
- if idx >= 0:
138
+ if not multi:
139
+ # Single: keep legacy behavior (pass single index via event)
140
+ menu.addAction(actions['edit'])
141
+ actions['edit'].triggered.connect(lambda: self.action_edit(event))
142
+
143
+ menu.addAction(actions['use'])
144
+ actions['use'].triggered.connect(lambda: self.action_use(event))
145
+
146
+ menu.addAction(actions['duplicate'])
147
+ actions['duplicate'].triggered.connect(lambda: self.action_duplicate(event))
148
+
149
+ menu.addAction(actions['reset'])
150
+ actions['reset'].triggered.connect(lambda: self.action_reset(event))
151
+
152
+ menu.addAction(actions['delete'])
153
+ actions['delete'].triggered.connect(lambda: self.action_delete(event))
154
+
155
+ menu.addAction(actions['delete_all'])
156
+ actions['delete_all'].triggered.connect(lambda: self.action_delete_all(event))
157
+ else:
158
+ # Multi: keep only actions that make sense in bulk; Use/Edit/Duplicate are single-only
159
+ rows = list(selected_rows)
160
+
161
+ menu.addAction(actions['reset'])
162
+ actions['reset'].triggered.connect(lambda checked=False, r=rows: self._action_reset_multi(r))
163
+
164
+ menu.addAction(actions['delete'])
165
+ actions['delete'].triggered.connect(lambda checked=False, r=rows: self._action_delete_multi(r))
166
+
167
+ menu.addAction(actions['delete_all'])
168
+ actions['delete_all'].triggered.connect(lambda checked=False, r=rows: self._action_delete_all_multi(r))
169
+
170
+ # Show menu
171
+ if index.isValid() or multi:
73
172
  menu.exec_(event.globalPos())
74
173
 
174
+ # Restore selection after context menu if it was temporarily changed
175
+ if backup_selection is not None and self.restore_after_ctx_menu:
176
+ sel_model.clearSelection()
177
+ for i in backup_selection:
178
+ sel_model.select(i, QItemSelectionModel.Select | QItemSelectionModel.Rows)
179
+ self._backup_selection = None
180
+ self.restore_after_ctx_menu = True
181
+
182
+ # ---------- Single-item handlers (legacy; keep unchanged signatures) ----------
183
+
75
184
  def action_use(self, event):
76
185
  """
77
186
  Use action handler
@@ -138,3 +247,16 @@ class ProfileList(BaseList):
138
247
  if idx >= 0:
139
248
  self.window.controller.settings.profile.edit_by_idx(idx)
140
249
 
250
+ # ---------- Multi-item handlers (new; pass list[int]) ----------
251
+
252
+ def _action_reset_multi(self, rows: list[int]):
253
+ if rows:
254
+ self.window.controller.settings.profile.reset_by_idx(list(rows))
255
+
256
+ def _action_delete_multi(self, rows: list[int]):
257
+ if rows:
258
+ self.window.controller.settings.profile.delete_by_idx(list(rows))
259
+
260
+ def _action_delete_all_multi(self, rows: list[int]):
261
+ if rows:
262
+ self.window.controller.settings.profile.delete_all_by_idx(list(rows))
@@ -6,11 +6,12 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.24 23:00:00 #
9
+ # Updated Date: 2025.12.27 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtGui import QAction, QIcon, QResizeEvent, Qt
13
- from PySide6.QtWidgets import QMenu
13
+ from PySide6.QtCore import 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
@@ -27,10 +28,30 @@ class UploadedFileList(BaseList):
27
28
  super(UploadedFileList, self).__init__(window)
28
29
  self.window = window
29
30
  self.id = id
31
+
32
+ # double click selects item (business action)
30
33
  self.doubleClicked.connect(self.dblclick)
34
+
35
+ # keep header visible here
31
36
  self.setHeaderHidden(False)
37
+
38
+ # disable default click handler from BaseList; we drive selection manually
32
39
  self.clicked.disconnect(self.click)
33
40
 
41
+ # Flat, row-based, multi-select behavior
42
+ self.setSelectionBehavior(QAbstractItemView.SelectRows)
43
+ self.setSelectionMode(QAbstractItemView.ExtendedSelection)
44
+
45
+ # Virtual multi-select helpers
46
+ self._suppress_item_click = False # suppress business click after Ctrl/Shift selection
47
+ self._ctrl_multi_active = False # Ctrl gesture in progress
48
+ self._ctrl_multi_index = None
49
+ self._was_shift_click = False # Shift range gesture
50
+
51
+ # Context menu selection backup (temporary right-click selection)
52
+ self._backup_selection = None
53
+ self.restore_after_ctx_menu = True
54
+
34
55
  self.header = self.header()
35
56
  self.header.setStretchLastSection(False)
36
57
 
@@ -48,24 +69,139 @@ class UploadedFileList(BaseList):
48
69
  super().resizeEvent(event)
49
70
  self.adjustColumnWidths()
50
71
 
72
+ # ----------------------------
73
+ # Selection helpers
74
+ # ----------------------------
75
+
76
+ def _selected_rows(self) -> list[int]:
77
+ """Return list of selected row numbers."""
78
+ try:
79
+ return sorted([ix.row() for ix in self.selectionModel().selectedRows()])
80
+ except Exception:
81
+ return []
82
+
83
+ def _has_multi_selection(self) -> bool:
84
+ """Check whether more than one row is selected."""
85
+ try:
86
+ return len(self.selectionModel().selectedRows()) > 1
87
+ except Exception:
88
+ return False
89
+
90
+ # ----------------------------
91
+ # Mouse events (virtual multi-select)
92
+ # ----------------------------
93
+
51
94
  def mousePressEvent(self, event):
52
95
  """
53
96
  Mouse press event
54
97
 
55
98
  :param event: mouse event
56
99
  """
57
- if event.buttons() == Qt.LeftButton:
58
- index = self.indexAt(event.pos())
59
- if index.isValid():
60
- self.window.controller.assistant.files.select(index.row())
100
+ # Ctrl+Left: virtual toggle without business click
101
+ if event.button() == Qt.LeftButton and (event.modifiers() & Qt.ControlModifier):
102
+ idx = self.indexAt(event.pos())
103
+ if idx.isValid():
104
+ self._ctrl_multi_active = True
105
+ self._ctrl_multi_index = idx
106
+ self._suppress_item_click = True
107
+ event.accept()
108
+ return
109
+ # Ctrl on empty space -> just suppress
110
+ self._suppress_item_click = True
111
+ event.accept()
112
+ return
113
+
114
+ # Shift+Left: let Qt perform range selection, but suppress business click
115
+ if event.button() == Qt.LeftButton and (event.modifiers() & Qt.ShiftModifier):
116
+ idx = self.indexAt(event.pos())
117
+ self._suppress_item_click = True
118
+ self._was_shift_click = True
119
+ if idx.isValid():
120
+ super(UploadedFileList, self).mousePressEvent(event)
121
+ else:
122
+ event.accept()
123
+ return
124
+
125
+ # Plain left click
126
+ if event.button() == Qt.LeftButton:
127
+ idx = self.indexAt(event.pos())
128
+
129
+ # When multiple are selected, a single plain click clears the multi-selection.
130
+ # If clicked on empty area: just clear and return.
131
+ if self._has_multi_selection():
132
+ sel_model = self.selectionModel()
133
+ sel_model.clearSelection()
134
+ if not idx.isValid():
135
+ event.accept()
136
+ return
137
+ # continue with default single selection for clicked row
138
+
139
+ # Perform default selection handling (no business click here)
140
+ super(UploadedFileList, self).mousePressEvent(event)
141
+ return
142
+
143
+ # Right click: prepare selection for context menu
144
+ if event.button() == Qt.RightButton:
145
+ idx = self.indexAt(event.pos())
146
+ sel_model = self.selectionModel()
147
+ selected_rows = [ix.row() for ix in sel_model.selectedRows()]
148
+ multi = len(selected_rows) > 1
149
+
150
+ if idx.isValid():
151
+ if multi and idx.row() in selected_rows:
152
+ # Keep existing multi-selection; do not alter selection on right click
153
+ self._backup_selection = None
154
+ else:
155
+ # Temporarily select the clicked row; backup previous selection to restore later
156
+ self._backup_selection = list(sel_model.selectedIndexes())
157
+ sel_model.clearSelection()
158
+ sel_model.select(idx, QItemSelectionModel.Select | QItemSelectionModel.Rows)
159
+ event.accept()
160
+ return
161
+
61
162
  super(UploadedFileList, self).mousePressEvent(event)
62
163
 
164
+ def mouseReleaseEvent(self, event):
165
+ # If the click was a Shift-based range selection, bypass business click
166
+ if event.button() == Qt.LeftButton and self._was_shift_click:
167
+ self._was_shift_click = False
168
+ self._suppress_item_click = False
169
+ super(UploadedFileList, self).mouseReleaseEvent(event)
170
+ return
171
+
172
+ # Finish "virtual" Ctrl toggle on same row (no business click)
173
+ if event.button() == Qt.LeftButton and self._ctrl_multi_active:
174
+ try:
175
+ idx = self.indexAt(event.pos())
176
+ if idx.isValid() and self._ctrl_multi_index and idx == self._ctrl_multi_index:
177
+ sel_model = self.selectionModel()
178
+ sel_model.select(idx, QItemSelectionModel.Toggle | QItemSelectionModel.Rows)
179
+ finally:
180
+ self._ctrl_multi_active = False
181
+ self._ctrl_multi_index = None
182
+ self._suppress_item_click = False
183
+ event.accept()
184
+ return
185
+
186
+ # Plain left: perform business selection only for single selection
187
+ if event.button() == Qt.LeftButton:
188
+ idx = self.indexAt(event.pos())
189
+ if not self._has_multi_selection():
190
+ if idx.isValid() and not self._suppress_item_click:
191
+ self.window.controller.assistant.files.select(idx.row())
192
+ self._suppress_item_click = False
193
+ super(UploadedFileList, self).mouseReleaseEvent(event)
194
+ return
195
+
196
+ super(UploadedFileList, self).mouseReleaseEvent(event)
197
+
63
198
  def click(self, val):
64
199
  """
65
200
  Click event
66
201
 
67
202
  :param val: click event
68
203
  """
204
+ # Not used; single-selection business click is handled in mouseReleaseEvent
69
205
  pass
70
206
 
71
207
  def dblclick(self, val):
@@ -74,7 +210,13 @@ class UploadedFileList(BaseList):
74
210
 
75
211
  :param val: double click event
76
212
  """
77
- self.window.controller.assistant.files.select(val.row())
213
+ row = val.row()
214
+ if row >= 0:
215
+ self.window.controller.assistant.files.select(row)
216
+
217
+ # ----------------------------
218
+ # Context menu
219
+ # ----------------------------
78
220
 
79
221
  def contextMenuEvent(self, event):
80
222
  """
@@ -83,58 +225,108 @@ class UploadedFileList(BaseList):
83
225
  :param event: context menu event
84
226
  """
85
227
  actions = {}
86
- actions['download'] = QAction(QIcon(":/icons/download.svg"), trans('action.download'), self)
87
- actions['download'].triggered.connect(
88
- lambda: self.action_download(event))
228
+ # actions['download'] = QAction(QIcon(":/icons/download.svg"), trans('action.download'), self)
229
+ menu = QMenu(self)
230
+ # menu.addAction(actions['download']) # not allowed for download files with purpose: assistants :(
231
+ index = self.indexAt(event.pos())
232
+ idx = index.row() if index.isValid() else -1
89
233
 
90
- actions['rename'] = QAction(QIcon(":/icons/edit.svg"), trans('action.rename'), self)
91
- actions['rename'].triggered.connect(
92
- lambda: self.action_rename(event))
234
+ # Selection state for multi / single
235
+ selected_rows = self._selected_rows()
236
+ multi = len(selected_rows) > 1
93
237
 
94
- actions['delete'] = QAction(QIcon(":/icons/delete.svg"), trans('action.delete'), self)
95
- actions['delete'].triggered.connect(
96
- lambda: self.action_delete(event))
238
+ # Allow menu on empty area only when multi-selection is active
239
+ if not index.isValid() and not multi:
240
+ # Restore selection if it was temporarily changed on right click
241
+ if self._backup_selection is not None and self.restore_after_ctx_menu:
242
+ sel_model = self.selectionModel()
243
+ sel_model.clearSelection()
244
+ for i in self._backup_selection:
245
+ sel_model.select(i, QItemSelectionModel.Select | QItemSelectionModel.Rows)
246
+ self._backup_selection = None
247
+ return
97
248
 
98
- menu = QMenu(self)
99
- # menu.addAction(actions['download']) # not allowed for download files with purpose: assistants :(
100
- menu.addAction(actions['rename'])
101
- menu.addAction(actions['delete'])
249
+ # Route actions: pass list on multi, int on single
250
+ if multi:
251
+ # actions['rename'] = QAction(QIcon(":/icons/edit.svg"), trans('action.rename'), self)
252
+ actions['delete'] = QAction(QIcon(":/icons/delete.svg"), trans('action.delete'), self)
253
+ # actions['rename'].triggered.connect(lambda: self.action_rename(list(selected_rows)))
254
+ actions['delete'].triggered.connect(lambda: self.action_delete(list(selected_rows)))
255
+ # menu.addAction(actions['rename'])
256
+ menu.addAction(actions['delete'])
257
+ # actions['download'].triggered.connect(lambda: self.action_download(list(selected_rows)))
258
+ else:
259
+ # Keep legacy behavior: on single right click also select item in controller
260
+ if idx >= 0:
261
+ self.window.controller.assistant.files.select(idx)
262
+ actions['rename'] = QAction(QIcon(":/icons/edit.svg"), trans('action.rename'), self)
263
+ actions['delete'] = QAction(QIcon(":/icons/delete.svg"), trans('action.delete'), self)
264
+ actions['rename'].triggered.connect(lambda: self.action_rename(idx))
265
+ actions['delete'].triggered.connect(lambda: self.action_delete(idx))
266
+ menu.addAction(actions['rename'])
267
+ menu.addAction(actions['delete'])
268
+ # actions['download'].triggered.connect(lambda: self.action_download(idx))
269
+ menu.exec_(event.globalPos())
102
270
 
103
- item = self.indexAt(event.pos())
104
- idx = item.row()
105
- if idx >= 0:
106
- self.window.controller.assistant.files.select(item.row())
107
- menu.exec_(event.globalPos())
271
+ # Restore selection after context menu if it was temporarily changed
272
+ if self.restore_after_ctx_menu and self._backup_selection is not None:
273
+ sel_model = self.selectionModel()
274
+ sel_model.clearSelection()
275
+ for i in self._backup_selection:
276
+ sel_model.select(i, QItemSelectionModel.Select | QItemSelectionModel.Rows)
277
+ self._backup_selection = None
278
+ self.restore_after_ctx_menu = True
279
+
280
+ # ----------------------------
281
+ # Context actions (single or multi)
282
+ # If 'item' is a list/tuple -> pass list of row ints to external code.
283
+ # If 'item' is an int -> pass single row int to external code.
284
+ # ----------------------------
108
285
 
109
- def action_rename(self, event):
286
+ def action_rename(self, item):
110
287
  """
111
288
  Rename action handler
112
289
 
113
- :param event: mouse event
290
+ :param item: int row or list of rows
114
291
  """
115
- item = self.indexAt(event.pos())
116
- idx = item.row()
292
+ if isinstance(item, (list, tuple)):
293
+ if item:
294
+ self.restore_after_ctx_menu = False
295
+ self.window.controller.assistant.files.rename(list(item))
296
+ return
297
+ idx = int(item)
117
298
  if idx >= 0:
299
+ self.restore_after_ctx_menu = False
118
300
  self.window.controller.assistant.files.rename(idx)
119
301
 
120
- def action_download(self, event):
302
+ def action_download(self, item):
121
303
  """
122
304
  Download action handler
123
305
 
124
- :param event: mouse event
306
+ :param item: int row or list of rows
125
307
  """
126
- item = self.indexAt(event.pos())
127
- idx = item.row()
308
+ if isinstance(item, (list, tuple)):
309
+ if item:
310
+ self.restore_after_ctx_menu = False
311
+ self.window.controller.assistant.files.download(list(item))
312
+ return
313
+ idx = int(item)
128
314
  if idx >= 0:
315
+ self.restore_after_ctx_menu = False
129
316
  self.window.controller.assistant.files.download(idx)
130
317
 
131
- def action_delete(self, event):
318
+ def action_delete(self, item):
132
319
  """
133
320
  Delete action handler
134
321
 
135
- :param event: mouse event
322
+ :param item: int row or list of rows
136
323
  """
137
- item = self.indexAt(event.pos())
138
- idx = item.row()
324
+ if isinstance(item, (list, tuple)):
325
+ if item:
326
+ self.restore_after_ctx_menu = False
327
+ self.window.controller.assistant.files.delete(list(item))
328
+ return
329
+ idx = int(item)
139
330
  if idx >= 0:
140
- self.window.controller.assistant.files.delete(idx)
331
+ self.restore_after_ctx_menu = False
332
+ self.window.controller.assistant.files.delete(idx)