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.
- pygpt_net/CHANGELOG.txt +12 -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 +182 -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/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 +2 -1
- pygpt_net/core/idx/idx.py +12 -11
- pygpt_net/core/idx/worker.py +13 -1
- pygpt_net/core/models/models.py +4 -4
- pygpt_net/core/profile/profile.py +13 -3
- pygpt_net/data/config/config.json +3 -3
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/css/style.dark.css +39 -1
- pygpt_net/data/css/style.light.css +39 -1
- pygpt_net/data/locale/locale.de.ini +3 -1
- pygpt_net/data/locale/locale.en.ini +3 -1
- pygpt_net/data/locale/locale.es.ini +3 -1
- pygpt_net/data/locale/locale.fr.ini +3 -1
- pygpt_net/data/locale/locale.it.ini +3 -1
- pygpt_net/data/locale/locale.pl.ini +4 -2
- pygpt_net/data/locale/locale.uk.ini +3 -1
- pygpt_net/data/locale/locale.zh.ini +3 -1
- pygpt_net/provider/api/openai/__init__.py +4 -2
- pygpt_net/provider/core/config/patch.py +9 -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/layout/ctx/ctx_list.py +16 -6
- pygpt_net/ui/main.py +3 -1
- pygpt_net/ui/widget/calendar/select.py +3 -3
- pygpt_net/ui/widget/filesystem/explorer.py +1082 -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 +1046 -32
- pygpt_net/ui/widget/option/dictionary.py +35 -7
- {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/METADATA +14 -57
- {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/RECORD +69 -69
- {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/entry_points.txt +0 -0
|
@@ -6,14 +6,14 @@
|
|
|
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 functools import partial
|
|
13
13
|
|
|
14
|
-
from PySide6.QtCore import Qt
|
|
14
|
+
from PySide6.QtCore import Qt, QPoint, QItemSelectionModel
|
|
15
15
|
from PySide6.QtGui import QAction, QIcon, QResizeEvent, QImage
|
|
16
|
-
from PySide6.QtWidgets import QMenu, QApplication
|
|
16
|
+
from PySide6.QtWidgets import QMenu, QApplication, QAbstractItemView
|
|
17
17
|
|
|
18
18
|
from pygpt_net.item.attachment import AttachmentItem
|
|
19
19
|
from pygpt_net.ui.widget.lists.base import BaseList
|
|
@@ -40,6 +40,20 @@ class AttachmentList(BaseList):
|
|
|
40
40
|
self.setHeaderHidden(False)
|
|
41
41
|
self.clicked.disconnect(self.click)
|
|
42
42
|
|
|
43
|
+
# Multi-select: rows + Ctrl/Shift gestures, but "virtual" (no business click)
|
|
44
|
+
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
|
45
|
+
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
|
46
|
+
|
|
47
|
+
# Guards for virtual multi-select (do not trigger controller on Ctrl/Shift)
|
|
48
|
+
self._suppress_item_click = False # one-shot suppression of business click
|
|
49
|
+
self._ctrl_multi_active = False # Ctrl gesture in progress
|
|
50
|
+
self._ctrl_multi_index = None # index pressed with Ctrl
|
|
51
|
+
self._was_shift_click = False # Shift gesture flag
|
|
52
|
+
|
|
53
|
+
# Context menu selection restore
|
|
54
|
+
self._backup_selection = None
|
|
55
|
+
self.restore_after_ctx_menu = True
|
|
56
|
+
|
|
43
57
|
hdr = self.header()
|
|
44
58
|
hdr.setStretchLastSection(False)
|
|
45
59
|
|
|
@@ -70,19 +84,92 @@ class AttachmentList(BaseList):
|
|
|
70
84
|
self._last_width = w
|
|
71
85
|
self.adjustColumnWidths()
|
|
72
86
|
|
|
87
|
+
def _selected_rows(self) -> list[int]:
|
|
88
|
+
"""Return list of selected row numbers."""
|
|
89
|
+
try:
|
|
90
|
+
return [ix.row() for ix in self.selectionModel().selectedRows()]
|
|
91
|
+
except Exception:
|
|
92
|
+
return []
|
|
93
|
+
|
|
94
|
+
def _has_multi_selection(self) -> bool:
|
|
95
|
+
"""Check whether multiple rows are currently selected."""
|
|
96
|
+
try:
|
|
97
|
+
return len(self.selectionModel().selectedRows()) > 1
|
|
98
|
+
except Exception:
|
|
99
|
+
return False
|
|
100
|
+
|
|
73
101
|
def mousePressEvent(self, event):
|
|
74
102
|
"""
|
|
75
103
|
Mouse press event
|
|
76
104
|
|
|
77
105
|
:param event: mouse event
|
|
78
106
|
"""
|
|
107
|
+
# Ctrl + Left: toggle visual selection only (virtual multi-select)
|
|
108
|
+
if event.button() == Qt.LeftButton and (event.modifiers() & Qt.ControlModifier):
|
|
109
|
+
idx = self.indexAt(event.pos())
|
|
110
|
+
self._ctrl_multi_active = True
|
|
111
|
+
self._ctrl_multi_index = idx if idx.isValid() else None
|
|
112
|
+
self._suppress_item_click = True
|
|
113
|
+
event.accept()
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
# Shift + Left: let Qt perform range selection; suppress business click
|
|
117
|
+
if event.button() == Qt.LeftButton and (event.modifiers() & Qt.ShiftModifier):
|
|
118
|
+
idx = self.indexAt(event.pos())
|
|
119
|
+
self._suppress_item_click = True
|
|
120
|
+
self._was_shift_click = True
|
|
121
|
+
if idx.isValid():
|
|
122
|
+
super(AttachmentList, self).mousePressEvent(event)
|
|
123
|
+
else:
|
|
124
|
+
event.accept()
|
|
125
|
+
return
|
|
126
|
+
|
|
79
127
|
if event.button() == Qt.LeftButton:
|
|
80
128
|
index = self.indexAt(event.pos())
|
|
81
|
-
|
|
129
|
+
|
|
130
|
+
# If multi selected: single left click anywhere clears selection
|
|
131
|
+
if self._has_multi_selection():
|
|
132
|
+
sel_model = self.selectionModel()
|
|
133
|
+
sel_model.clearSelection()
|
|
134
|
+
if not index.isValid():
|
|
135
|
+
event.accept()
|
|
136
|
+
return
|
|
137
|
+
# continue to select clicked row as single
|
|
138
|
+
|
|
139
|
+
# Default path: allow Qt to select row visually
|
|
140
|
+
super(AttachmentList, self).mousePressEvent(event)
|
|
141
|
+
|
|
142
|
+
# Business click only when not suppressed and not multi-selection
|
|
143
|
+
if index.isValid() and not self._suppress_item_click and not self._has_multi_selection():
|
|
82
144
|
mode = self.window.core.config.get('mode')
|
|
83
145
|
self.window.controller.attachment.select(mode, index.row())
|
|
146
|
+
return
|
|
147
|
+
|
|
84
148
|
super(AttachmentList, self).mousePressEvent(event)
|
|
85
149
|
|
|
150
|
+
def mouseReleaseEvent(self, event):
|
|
151
|
+
# Finish Shift-based range selection (keep it virtual)
|
|
152
|
+
if event.button() == Qt.LeftButton and self._was_shift_click:
|
|
153
|
+
self._was_shift_click = False
|
|
154
|
+
super(AttachmentList, self).mouseReleaseEvent(event)
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
# Finish Ctrl-based toggle selection (virtual)
|
|
158
|
+
if event.button() == Qt.LeftButton and self._ctrl_multi_active:
|
|
159
|
+
try:
|
|
160
|
+
idx = self.indexAt(event.pos())
|
|
161
|
+
if idx.isValid() and self._ctrl_multi_index and idx == self._ctrl_multi_index:
|
|
162
|
+
sel_model = self.selectionModel()
|
|
163
|
+
sel_model.select(idx, QItemSelectionModel.Toggle | QItemSelectionModel.Rows)
|
|
164
|
+
finally:
|
|
165
|
+
self._ctrl_multi_active = False
|
|
166
|
+
self._ctrl_multi_index = None
|
|
167
|
+
self._suppress_item_click = False
|
|
168
|
+
event.accept()
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
super(AttachmentList, self).mouseReleaseEvent(event)
|
|
172
|
+
|
|
86
173
|
def click(self, val):
|
|
87
174
|
"""
|
|
88
175
|
Click event
|
|
@@ -107,61 +194,136 @@ class AttachmentList(BaseList):
|
|
|
107
194
|
:param event: context menu event
|
|
108
195
|
"""
|
|
109
196
|
mode = self.window.core.config.get('mode')
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if
|
|
197
|
+
pos = event.pos()
|
|
198
|
+
index = self.indexAt(pos)
|
|
199
|
+
idx = index.row() if index.isValid() else -1
|
|
200
|
+
|
|
201
|
+
# Read current selection
|
|
202
|
+
sel_model = self.selectionModel()
|
|
203
|
+
selected_indexes = list(sel_model.selectedRows()) if sel_model else []
|
|
204
|
+
selected_rows = [ix.row() for ix in selected_indexes]
|
|
205
|
+
multi = len(selected_rows) > 1
|
|
206
|
+
|
|
207
|
+
# Allow menu on empty area only when multi-selection is active
|
|
208
|
+
if not index.isValid() and not multi:
|
|
113
209
|
return
|
|
114
210
|
|
|
115
|
-
|
|
116
|
-
|
|
211
|
+
# If right-click on a row outside current multi-selection, temporarily select that row
|
|
212
|
+
backup_selection = None
|
|
213
|
+
if index.isValid():
|
|
214
|
+
if multi and idx in selected_rows:
|
|
215
|
+
backup_selection = None # keep multi-selection
|
|
216
|
+
else:
|
|
217
|
+
backup_selection = list(sel_model.selectedIndexes())
|
|
218
|
+
sel_model.clearSelection()
|
|
219
|
+
sel_model.select(index, QItemSelectionModel.Select | QItemSelectionModel.Rows)
|
|
220
|
+
selected_rows = [idx]
|
|
221
|
+
multi = False
|
|
222
|
+
|
|
223
|
+
# Resolve attachment(s)
|
|
224
|
+
selected_attachments = []
|
|
225
|
+
try:
|
|
226
|
+
for r in selected_rows:
|
|
227
|
+
a = self.window.controller.attachment.get_by_idx(mode, r)
|
|
228
|
+
if a:
|
|
229
|
+
selected_attachments.append(a)
|
|
230
|
+
except Exception:
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
attachment = None
|
|
234
|
+
if idx >= 0:
|
|
235
|
+
attachment = self.window.controller.attachment.get_by_idx(mode, idx)
|
|
117
236
|
|
|
118
|
-
|
|
237
|
+
# Compute flags for file-based actions when multi
|
|
238
|
+
all_files = all(a and a.type == AttachmentItem.TYPE_FILE for a in selected_attachments) if selected_attachments else False
|
|
119
239
|
|
|
240
|
+
menu = QMenu(self)
|
|
120
241
|
actions = {}
|
|
121
|
-
actions['open'] = QAction(self._ICON_VIEW, trans('action.open'), menu)
|
|
122
|
-
actions['open'].triggered.connect(partial(self._action_open_idx, idx))
|
|
123
242
|
|
|
243
|
+
# Open / Open dir
|
|
244
|
+
actions['open'] = QAction(self._ICON_VIEW, trans('action.open'), menu)
|
|
124
245
|
actions['open_dir'] = QAction(self._ICON_FOLDER, trans('action.open_dir'), menu)
|
|
125
|
-
actions['open_dir'].triggered.connect(partial(self._action_open_dir_idx, idx))
|
|
126
246
|
|
|
247
|
+
# Rename (single only)
|
|
127
248
|
actions['rename'] = QAction(self._ICON_EDIT, trans('action.rename'), menu)
|
|
128
|
-
actions['rename'].triggered.connect(partial(self._action_rename_idx, idx))
|
|
129
249
|
|
|
250
|
+
# Delete (single or multi)
|
|
130
251
|
actions['delete'] = QAction(self._ICON_DELETE, trans('action.delete'), menu)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
252
|
+
|
|
253
|
+
fs_actions = None
|
|
254
|
+
path = None
|
|
255
|
+
|
|
256
|
+
# Single selection path (preserve previous behavior)
|
|
257
|
+
if not multi and attachment:
|
|
258
|
+
path = attachment.path
|
|
259
|
+
if attachment.type == AttachmentItem.TYPE_FILE:
|
|
260
|
+
fs_actions = self.window.core.filesystem.actions
|
|
261
|
+
|
|
262
|
+
# Preview actions (single only)
|
|
263
|
+
if fs_actions.has_preview(path):
|
|
264
|
+
preview_actions = fs_actions.get_preview(self, path)
|
|
265
|
+
for preview_action in preview_actions or []:
|
|
266
|
+
try:
|
|
267
|
+
preview_action.setParent(menu)
|
|
268
|
+
except Exception:
|
|
269
|
+
pass
|
|
270
|
+
menu.addAction(preview_action)
|
|
271
|
+
|
|
272
|
+
# Open / Open dir
|
|
273
|
+
actions['open'].triggered.connect(partial(self._action_open_idx, idx))
|
|
274
|
+
actions['open_dir'].triggered.connect(partial(self._action_open_dir_idx, idx))
|
|
275
|
+
menu.addAction(actions['open'])
|
|
276
|
+
menu.addAction(actions['open_dir'])
|
|
277
|
+
|
|
278
|
+
# Use submenu (single only)
|
|
279
|
+
if fs_actions.has_use(path):
|
|
280
|
+
use_actions = fs_actions.get_use(self, path)
|
|
281
|
+
use_menu = QMenu(trans('action.use'), menu)
|
|
282
|
+
for use_action in use_actions or []:
|
|
283
|
+
try:
|
|
284
|
+
use_action.setParent(use_menu)
|
|
285
|
+
except Exception:
|
|
286
|
+
pass
|
|
287
|
+
use_menu.addAction(use_action)
|
|
288
|
+
menu.addMenu(use_menu)
|
|
289
|
+
|
|
290
|
+
# Rename (single only)
|
|
291
|
+
actions['rename'].triggered.connect(partial(self._action_rename_idx, idx))
|
|
292
|
+
menu.addAction(actions['rename'])
|
|
293
|
+
|
|
294
|
+
# Delete
|
|
295
|
+
actions['delete'].triggered.connect(partial(self._action_delete_idx, idx))
|
|
296
|
+
menu.addAction(actions['delete'])
|
|
297
|
+
|
|
298
|
+
else:
|
|
299
|
+
# Multi-selection aggregated actions
|
|
300
|
+
rows = list(selected_rows)
|
|
301
|
+
|
|
302
|
+
# Open / Open dir only when all selected are files
|
|
303
|
+
if all_files:
|
|
304
|
+
actions['open'].triggered.connect(partial(self._action_open_multi, rows))
|
|
305
|
+
menu.addAction(actions['open'])
|
|
306
|
+
|
|
307
|
+
actions['open_dir'].triggered.connect(partial(self._action_open_dir_multi, rows))
|
|
308
|
+
menu.addAction(actions['open_dir'])
|
|
309
|
+
|
|
310
|
+
# Rename disabled for multi; do not add
|
|
311
|
+
|
|
312
|
+
# Delete for multi (pass list)
|
|
313
|
+
actions['delete'].triggered.connect(partial(self._action_delete_multi, rows))
|
|
314
|
+
menu.addAction(actions['delete'])
|
|
315
|
+
|
|
316
|
+
# Do not trigger controller selection here; keep it virtual
|
|
163
317
|
menu.exec_(event.globalPos())
|
|
164
318
|
|
|
319
|
+
# Restore original selection after context menu if it was temporarily changed
|
|
320
|
+
if backup_selection is not None and self.restore_after_ctx_menu:
|
|
321
|
+
sel_model.clearSelection()
|
|
322
|
+
for i in backup_selection:
|
|
323
|
+
sel_model.select(i, QItemSelectionModel.Select | QItemSelectionModel.Rows)
|
|
324
|
+
self._backup_selection = None
|
|
325
|
+
self.restore_after_ctx_menu = True
|
|
326
|
+
|
|
165
327
|
def keyPressEvent(self, event):
|
|
166
328
|
"""
|
|
167
329
|
Key press event to handle undo action
|
|
@@ -238,6 +400,8 @@ class AttachmentList(BaseList):
|
|
|
238
400
|
if idx >= 0:
|
|
239
401
|
self.window.controller.attachment.delete(idx)
|
|
240
402
|
|
|
403
|
+
# Single-index context actions (backward compatible)
|
|
404
|
+
|
|
241
405
|
def _action_open_idx(self, idx, checked=False):
|
|
242
406
|
if idx >= 0:
|
|
243
407
|
mode = self.window.core.config.get('mode')
|
|
@@ -255,4 +419,23 @@ class AttachmentList(BaseList):
|
|
|
255
419
|
|
|
256
420
|
def _action_delete_idx(self, idx, checked=False):
|
|
257
421
|
if idx >= 0:
|
|
258
|
-
self.window.controller.attachment.delete(idx)
|
|
422
|
+
self.window.controller.attachment.delete(idx)
|
|
423
|
+
|
|
424
|
+
# Multi-index context actions (aggregated)
|
|
425
|
+
|
|
426
|
+
def _action_open_multi(self, rows: list[int], checked=False):
|
|
427
|
+
"""Open multiple attachments: pass list of row indexes."""
|
|
428
|
+
if rows:
|
|
429
|
+
mode = self.window.core.config.get('mode')
|
|
430
|
+
self.window.controller.attachment.open(mode, list(rows))
|
|
431
|
+
|
|
432
|
+
def _action_open_dir_multi(self, rows: list[int], checked=False):
|
|
433
|
+
"""Open directories for multiple attachments: pass list of row indexes."""
|
|
434
|
+
if rows:
|
|
435
|
+
mode = self.window.core.config.get('mode')
|
|
436
|
+
self.window.controller.attachment.open_dir(mode, list(rows))
|
|
437
|
+
|
|
438
|
+
def _action_delete_multi(self, rows: list[int], checked=False):
|
|
439
|
+
"""Delete multiple attachments: pass list of row indexes."""
|
|
440
|
+
if rows:
|
|
441
|
+
self.window.controller.attachment.delete(list(rows))
|
|
@@ -6,13 +6,14 @@
|
|
|
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 functools import partial
|
|
13
13
|
|
|
14
|
-
from PySide6.
|
|
15
|
-
from PySide6.
|
|
14
|
+
from PySide6.QtCore import Qt, QItemSelectionModel
|
|
15
|
+
from PySide6.QtGui import QAction, QIcon, QResizeEvent
|
|
16
|
+
from PySide6.QtWidgets import QMenu, QAbstractItemView
|
|
16
17
|
|
|
17
18
|
from pygpt_net.ui.widget.lists.base import BaseList
|
|
18
19
|
from pygpt_net.utils import trans
|
|
@@ -33,6 +34,18 @@ class AttachmentCtxList(BaseList):
|
|
|
33
34
|
self.setHeaderHidden(False)
|
|
34
35
|
self.clicked.disconnect(self.click)
|
|
35
36
|
|
|
37
|
+
# Flat rows + virtual multi-select (Ctrl/Shift)
|
|
38
|
+
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
|
39
|
+
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
|
40
|
+
|
|
41
|
+
# Virtual multi-select guards
|
|
42
|
+
self._suppress_item_click = False
|
|
43
|
+
self._was_shift_click = False
|
|
44
|
+
|
|
45
|
+
# Context menu selection restore
|
|
46
|
+
self._backup_selection = None
|
|
47
|
+
self.restore_after_ctx_menu = True
|
|
48
|
+
|
|
36
49
|
self.header = self.header()
|
|
37
50
|
self.header.setStretchLastSection(False)
|
|
38
51
|
|
|
@@ -50,18 +63,84 @@ class AttachmentCtxList(BaseList):
|
|
|
50
63
|
super().resizeEvent(event)
|
|
51
64
|
self.adjustColumnWidths()
|
|
52
65
|
|
|
66
|
+
def _selected_rows(self) -> list[int]:
|
|
67
|
+
"""Return list of selected row numbers."""
|
|
68
|
+
try:
|
|
69
|
+
return [ix.row() for ix in self.selectionModel().selectedRows()]
|
|
70
|
+
except Exception:
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
def _has_multi_selection(self) -> bool:
|
|
74
|
+
"""Check whether multiple rows are currently selected."""
|
|
75
|
+
try:
|
|
76
|
+
return len(self.selectionModel().selectedRows()) > 1
|
|
77
|
+
except Exception:
|
|
78
|
+
return False
|
|
79
|
+
|
|
53
80
|
def mousePressEvent(self, event):
|
|
54
81
|
"""
|
|
55
82
|
Mouse press event
|
|
56
83
|
|
|
57
84
|
:param event: mouse event
|
|
58
85
|
"""
|
|
59
|
-
|
|
86
|
+
# Ctrl + Left: let Qt toggle selection; block business click afterwards
|
|
87
|
+
if event.button() == Qt.LeftButton and (event.modifiers() & Qt.ControlModifier):
|
|
88
|
+
idx = self.indexAt(event.pos())
|
|
89
|
+
self._suppress_item_click = True
|
|
90
|
+
if idx.isValid():
|
|
91
|
+
super(AttachmentCtxList, self).mousePressEvent(event) # native toggle
|
|
92
|
+
else:
|
|
93
|
+
event.accept()
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
# Shift + Left: let Qt perform range selection; suppress business click
|
|
97
|
+
if event.button() == Qt.LeftButton and (event.modifiers() & Qt.ShiftModifier):
|
|
98
|
+
idx = self.indexAt(event.pos())
|
|
99
|
+
self._suppress_item_click = True
|
|
100
|
+
self._was_shift_click = True
|
|
101
|
+
if idx.isValid():
|
|
102
|
+
super(AttachmentCtxList, self).mousePressEvent(event)
|
|
103
|
+
else:
|
|
104
|
+
event.accept()
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
if event.button() == Qt.LeftButton:
|
|
60
108
|
index = self.indexAt(event.pos())
|
|
61
|
-
|
|
109
|
+
|
|
110
|
+
# When multiple are selected, a single left click anywhere clears selection
|
|
111
|
+
if self._has_multi_selection():
|
|
112
|
+
sel_model = self.selectionModel()
|
|
113
|
+
sel_model.clearSelection()
|
|
114
|
+
if not index.isValid():
|
|
115
|
+
event.accept()
|
|
116
|
+
return
|
|
117
|
+
# continue to select clicked row as single
|
|
118
|
+
|
|
119
|
+
# Default visual selection
|
|
120
|
+
super(AttachmentCtxList, self).mousePressEvent(event)
|
|
121
|
+
|
|
122
|
+
# Business click only when not suppressed and not multi-selection
|
|
123
|
+
if index.isValid() and not self._suppress_item_click and not self._has_multi_selection():
|
|
62
124
|
self.window.controller.assistant.files.select(index.row())
|
|
125
|
+
return
|
|
126
|
+
|
|
63
127
|
super(AttachmentCtxList, self).mousePressEvent(event)
|
|
64
128
|
|
|
129
|
+
def mouseReleaseEvent(self, event):
|
|
130
|
+
# Finish Shift-based range selection (virtual)
|
|
131
|
+
if event.button() == Qt.LeftButton and self._was_shift_click:
|
|
132
|
+
self._was_shift_click = False
|
|
133
|
+
super(AttachmentCtxList, self).mouseReleaseEvent(event)
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
# Clear Ctrl suppression flag on release (selection was already toggled by Qt on press)
|
|
137
|
+
if event.button() == Qt.LeftButton and self._suppress_item_click:
|
|
138
|
+
self._suppress_item_click = False
|
|
139
|
+
super(AttachmentCtxList, self).mouseReleaseEvent(event)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
super(AttachmentCtxList, self).mouseReleaseEvent(event)
|
|
143
|
+
|
|
65
144
|
def click(self, val):
|
|
66
145
|
"""
|
|
67
146
|
Click event
|
|
@@ -89,42 +168,101 @@ class AttachmentCtxList(BaseList):
|
|
|
89
168
|
|
|
90
169
|
actions = {}
|
|
91
170
|
|
|
92
|
-
|
|
93
|
-
idx =
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
171
|
+
index = self.indexAt(event.pos())
|
|
172
|
+
idx = index.row() if index.isValid() else -1
|
|
173
|
+
|
|
174
|
+
# Selection state
|
|
175
|
+
sel_model = self.selectionModel()
|
|
176
|
+
selected_indexes = list(sel_model.selectedRows()) if sel_model else []
|
|
177
|
+
selected_rows = [ix.row() for ix in selected_indexes]
|
|
178
|
+
multi = len(selected_rows) > 1
|
|
179
|
+
|
|
180
|
+
# Allow menu on empty area only when multi-selection is active
|
|
181
|
+
if not index.isValid() and not multi:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
# Use class-level selection flags (PySide6) for compatibility and clarity
|
|
185
|
+
SelectionFlag = getattr(QItemSelectionModel, "SelectionFlag", QItemSelectionModel)
|
|
186
|
+
|
|
187
|
+
# Temporarily adjust selection if right-clicked outside current multi-selection
|
|
188
|
+
backup_selection = None
|
|
189
|
+
if index.isValid():
|
|
190
|
+
if multi and idx in selected_rows:
|
|
191
|
+
backup_selection = None # keep multi-selection
|
|
192
|
+
else:
|
|
193
|
+
backup_selection = list(sel_model.selectedIndexes())
|
|
194
|
+
sel_model.clearSelection()
|
|
195
|
+
sel_model.select(index, SelectionFlag.Select | SelectionFlag.Rows)
|
|
196
|
+
selected_rows = [idx]
|
|
197
|
+
multi = False
|
|
198
|
+
|
|
199
|
+
# Aggregate capabilities across selection (union)
|
|
200
|
+
has_file_any = False
|
|
201
|
+
has_src_any = False
|
|
202
|
+
has_dest_any = False
|
|
203
|
+
|
|
204
|
+
rows_for_flags = selected_rows if selected_rows else ([idx] if idx >= 0 else [])
|
|
205
|
+
|
|
206
|
+
for r in rows_for_flags:
|
|
207
|
+
try:
|
|
208
|
+
if self.window.controller.chat.attachment.has_file_by_idx(r):
|
|
209
|
+
has_file_any = True
|
|
210
|
+
if self.window.controller.chat.attachment.has_src_by_idx(r):
|
|
211
|
+
has_src_any = True
|
|
212
|
+
if self.window.controller.chat.attachment.has_dest_by_idx(r):
|
|
213
|
+
has_dest_any = True
|
|
214
|
+
except Exception:
|
|
215
|
+
pass
|
|
103
216
|
|
|
104
217
|
actions['open'] = QAction(QIcon(":/icons/view.svg"), trans('action.open'), self)
|
|
105
|
-
actions['open'].triggered.connect(partial(ignore_trigger, self.action_open, event))
|
|
106
|
-
|
|
107
218
|
actions['open_dir_src'] = QAction(QIcon(":/icons/folder.svg"), trans('action.open_dir_src'), self)
|
|
108
|
-
actions['open_dir_src'].triggered.connect(partial(ignore_trigger, self.action_open_dir_src, event))
|
|
109
|
-
|
|
110
219
|
actions['open_dir_dest'] = QAction(QIcon(":/icons/folder.svg"), trans('action.open_dir_storage'), self)
|
|
111
|
-
actions['open_dir_dest'].triggered.connect(partial(ignore_trigger, self.action_open_dir_dest, event))
|
|
112
|
-
|
|
113
220
|
actions['delete'] = QAction(QIcon(":/icons/delete.svg"), trans('action.delete'), self)
|
|
114
|
-
actions['delete'].triggered.connect(partial(ignore_trigger, self.action_delete, event))
|
|
115
221
|
|
|
116
222
|
menu = QMenu(self)
|
|
117
|
-
if has_file:
|
|
118
|
-
menu.addAction(actions['open'])
|
|
119
|
-
if has_src:
|
|
120
|
-
menu.addAction(actions['open_dir_src'])
|
|
121
|
-
if has_dest:
|
|
122
|
-
menu.addAction(actions['open_dir_dest'])
|
|
123
|
-
menu.addAction(actions['delete'])
|
|
124
223
|
|
|
125
|
-
if
|
|
126
|
-
|
|
127
|
-
|
|
224
|
+
if not multi:
|
|
225
|
+
# Single selection: keep legacy path and event-based handlers
|
|
226
|
+
if idx >= 0:
|
|
227
|
+
if has_file_any:
|
|
228
|
+
actions['open'].triggered.connect(partial(ignore_trigger, self.action_open, event))
|
|
229
|
+
menu.addAction(actions['open'])
|
|
230
|
+
if has_src_any:
|
|
231
|
+
actions['open_dir_src'].triggered.connect(partial(ignore_trigger, self.action_open_dir_src, event))
|
|
232
|
+
menu.addAction(actions['open_dir_src'])
|
|
233
|
+
if has_dest_any:
|
|
234
|
+
actions['open_dir_dest'].triggered.connect(partial(ignore_trigger, self.action_open_dir_dest, event))
|
|
235
|
+
menu.addAction(actions['open_dir_dest'])
|
|
236
|
+
|
|
237
|
+
actions['delete'].triggered.connect(partial(ignore_trigger, self.action_delete, event))
|
|
238
|
+
menu.addAction(actions['delete'])
|
|
239
|
+
else:
|
|
240
|
+
# Multi selection: show union of available actions and pass list of rows
|
|
241
|
+
rows = list(selected_rows)
|
|
242
|
+
|
|
243
|
+
if has_file_any:
|
|
244
|
+
actions['open'].triggered.connect(partial(self._action_open_multi, rows))
|
|
245
|
+
menu.addAction(actions['open'])
|
|
246
|
+
if has_src_any:
|
|
247
|
+
actions['open_dir_src'].triggered.connect(partial(self._action_open_dir_src_multi, rows))
|
|
248
|
+
menu.addAction(actions['open_dir_src'])
|
|
249
|
+
if has_dest_any:
|
|
250
|
+
actions['open_dir_dest'].triggered.connect(partial(self._action_open_dir_dest_multi, rows))
|
|
251
|
+
menu.addAction(actions['open_dir_dest'])
|
|
252
|
+
|
|
253
|
+
actions['delete'].triggered.connect(partial(self._action_delete_multi, rows))
|
|
254
|
+
menu.addAction(actions['delete'])
|
|
255
|
+
|
|
256
|
+
# Do not perform business selection here; keep selection virtual
|
|
257
|
+
menu.exec_(event.globalPos())
|
|
258
|
+
|
|
259
|
+
# Restore original selection after context menu if it was temporarily changed
|
|
260
|
+
if backup_selection is not None and self.restore_after_ctx_menu:
|
|
261
|
+
sel_model.clearSelection()
|
|
262
|
+
for i in backup_selection:
|
|
263
|
+
sel_model.select(i, SelectionFlag.Select | SelectionFlag.Rows)
|
|
264
|
+
self._backup_selection = None
|
|
265
|
+
self.restore_after_ctx_menu = True
|
|
128
266
|
|
|
129
267
|
def action_delete(self, event):
|
|
130
268
|
"""
|
|
@@ -169,3 +307,21 @@ class AttachmentCtxList(BaseList):
|
|
|
169
307
|
idx = item.row()
|
|
170
308
|
if idx >= 0:
|
|
171
309
|
self.window.controller.chat.attachment.open_dir_dest_by_idx(idx)
|
|
310
|
+
|
|
311
|
+
# ---- Multi-index context actions (aggregated; pass list[int]) ----
|
|
312
|
+
|
|
313
|
+
def _action_delete_multi(self, rows: list[int], checked=False):
|
|
314
|
+
if rows:
|
|
315
|
+
self.window.controller.chat.attachment.delete_by_idx(list(rows))
|
|
316
|
+
|
|
317
|
+
def _action_open_multi(self, rows: list[int], checked=False):
|
|
318
|
+
if rows:
|
|
319
|
+
self.window.controller.chat.attachment.open_by_idx(list(rows))
|
|
320
|
+
|
|
321
|
+
def _action_open_dir_src_multi(self, rows: list[int], checked=False):
|
|
322
|
+
if rows:
|
|
323
|
+
self.window.controller.chat.attachment.open_dir_src_by_idx(list(rows))
|
|
324
|
+
|
|
325
|
+
def _action_open_dir_dest_multi(self, rows: list[int], checked=False):
|
|
326
|
+
if rows:
|
|
327
|
+
self.window.controller.chat.attachment.open_dir_dest_by_idx(list(rows))
|
|
@@ -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 02:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtWidgets import QHBoxLayout, QWidget, QComboBox
|
|
@@ -29,7 +29,7 @@ class BaseListCombo(QWidget):
|
|
|
29
29
|
self.current_id = None
|
|
30
30
|
self.keys = []
|
|
31
31
|
self.real_time = False
|
|
32
|
-
self.combo = NoScrollCombo()
|
|
32
|
+
self.combo = NoScrollCombo(parent=self.window)
|
|
33
33
|
self.combo.currentIndexChanged.connect(self.on_combo_change)
|
|
34
34
|
self.initialized = False
|
|
35
35
|
self.locked = False
|