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,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.
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
234
|
+
# Selection state for multi / single
|
|
235
|
+
selected_rows = self._selected_rows()
|
|
236
|
+
multi = len(selected_rows) > 1
|
|
93
237
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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,
|
|
286
|
+
def action_rename(self, item):
|
|
110
287
|
"""
|
|
111
288
|
Rename action handler
|
|
112
289
|
|
|
113
|
-
:param
|
|
290
|
+
:param item: int row or list of rows
|
|
114
291
|
"""
|
|
115
|
-
item
|
|
116
|
-
|
|
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,
|
|
302
|
+
def action_download(self, item):
|
|
121
303
|
"""
|
|
122
304
|
Download action handler
|
|
123
305
|
|
|
124
|
-
:param
|
|
306
|
+
:param item: int row or list of rows
|
|
125
307
|
"""
|
|
126
|
-
item
|
|
127
|
-
|
|
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,
|
|
318
|
+
def action_delete(self, item):
|
|
132
319
|
"""
|
|
133
320
|
Delete action handler
|
|
134
321
|
|
|
135
|
-
:param
|
|
322
|
+
:param item: int row or list of rows
|
|
136
323
|
"""
|
|
137
|
-
item
|
|
138
|
-
|
|
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.
|
|
331
|
+
self.restore_after_ctx_menu = False
|
|
332
|
+
self.window.controller.assistant.files.delete(idx)
|