pygpt-net 2.7.3__py3-none-any.whl → 2.7.5__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 +15 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +382 -350
- pygpt_net/app_core.py +4 -2
- pygpt_net/controller/__init__.py +5 -1
- pygpt_net/controller/assistant/assistant.py +1 -4
- pygpt_net/controller/assistant/batch.py +5 -504
- pygpt_net/controller/assistant/editor.py +5 -5
- pygpt_net/controller/assistant/files.py +16 -16
- pygpt_net/controller/chat/attachment.py +5 -1
- pygpt_net/controller/chat/handler/google_stream.py +307 -1
- pygpt_net/controller/chat/handler/worker.py +8 -1
- pygpt_net/controller/chat/image.py +15 -3
- pygpt_net/controller/dialogs/confirm.py +73 -101
- pygpt_net/controller/files/files.py +3 -1
- pygpt_net/controller/lang/mapping.py +9 -9
- pygpt_net/controller/layout/layout.py +2 -2
- pygpt_net/controller/painter/capture.py +50 -1
- pygpt_net/controller/presets/presets.py +2 -1
- pygpt_net/controller/remote_store/__init__.py +12 -0
- pygpt_net/{provider/core/assistant_file/db_sqlite → controller/remote_store/google}/__init__.py +2 -2
- pygpt_net/controller/remote_store/google/batch.py +402 -0
- pygpt_net/controller/remote_store/google/store.py +615 -0
- pygpt_net/controller/remote_store/openai/__init__.py +12 -0
- pygpt_net/controller/remote_store/openai/batch.py +524 -0
- pygpt_net/controller/{assistant → remote_store/openai}/store.py +63 -60
- pygpt_net/controller/remote_store/remote_store.py +35 -0
- pygpt_net/controller/theme/nodes.py +2 -1
- pygpt_net/controller/ui/mode.py +5 -1
- pygpt_net/controller/ui/ui.py +36 -2
- pygpt_net/core/assistants/assistants.py +3 -15
- pygpt_net/core/db/database.py +5 -3
- pygpt_net/core/filesystem/url.py +4 -1
- pygpt_net/core/locale/placeholder.py +35 -0
- pygpt_net/core/remote_store/__init__.py +12 -0
- pygpt_net/core/remote_store/google/__init__.py +11 -0
- pygpt_net/core/remote_store/google/files.py +224 -0
- pygpt_net/core/remote_store/google/store.py +248 -0
- pygpt_net/core/remote_store/openai/__init__.py +11 -0
- pygpt_net/core/{assistants → remote_store/openai}/files.py +26 -19
- pygpt_net/core/{assistants → remote_store/openai}/store.py +32 -15
- pygpt_net/core/remote_store/remote_store.py +24 -0
- pygpt_net/core/render/web/helpers.py +5 -0
- pygpt_net/data/config/config.json +8 -5
- pygpt_net/data/config/models.json +77 -3
- pygpt_net/data/config/settings.json +45 -14
- pygpt_net/data/css/web-blocks.css +3 -0
- pygpt_net/data/css/web-chatgpt.css +3 -0
- pygpt_net/data/locale/locale.de.ini +43 -41
- pygpt_net/data/locale/locale.en.ini +56 -44
- pygpt_net/data/locale/locale.es.ini +43 -41
- pygpt_net/data/locale/locale.fr.ini +43 -41
- pygpt_net/data/locale/locale.it.ini +43 -41
- pygpt_net/data/locale/locale.pl.ini +43 -41
- pygpt_net/data/locale/locale.uk.ini +43 -41
- pygpt_net/data/locale/locale.zh.ini +43 -41
- pygpt_net/data/locale/plugin.cmd_history.de.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_history.en.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_history.es.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_history.fr.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_history.it.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_history.pl.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_history.uk.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_history.zh.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_mouse_control.en.ini +14 -0
- pygpt_net/data/locale/plugin.cmd_web.de.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_web.en.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_web.es.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_web.fr.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_web.it.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_web.pl.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_web.uk.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_web.zh.ini +1 -1
- pygpt_net/data/locale/plugin.idx_llama_index.de.ini +2 -2
- pygpt_net/data/locale/plugin.idx_llama_index.en.ini +2 -2
- pygpt_net/data/locale/plugin.idx_llama_index.es.ini +2 -2
- pygpt_net/data/locale/plugin.idx_llama_index.fr.ini +2 -2
- pygpt_net/data/locale/plugin.idx_llama_index.it.ini +2 -2
- pygpt_net/data/locale/plugin.idx_llama_index.pl.ini +2 -2
- pygpt_net/data/locale/plugin.idx_llama_index.uk.ini +2 -2
- pygpt_net/data/locale/plugin.idx_llama_index.zh.ini +2 -2
- pygpt_net/item/assistant.py +1 -211
- pygpt_net/item/ctx.py +3 -1
- pygpt_net/item/store.py +238 -0
- pygpt_net/launcher.py +115 -55
- pygpt_net/migrations/Version20260102190000.py +35 -0
- pygpt_net/migrations/__init__.py +3 -1
- pygpt_net/plugin/cmd_mouse_control/config.py +470 -1
- pygpt_net/plugin/cmd_mouse_control/plugin.py +488 -22
- pygpt_net/plugin/cmd_mouse_control/worker.py +464 -87
- pygpt_net/plugin/cmd_mouse_control/worker_sandbox.py +729 -0
- pygpt_net/plugin/idx_llama_index/config.py +2 -2
- pygpt_net/preload.py +243 -0
- pygpt_net/provider/api/google/__init__.py +16 -54
- pygpt_net/provider/api/google/chat.py +546 -129
- pygpt_net/provider/api/google/computer.py +190 -0
- pygpt_net/provider/api/google/image.py +74 -6
- pygpt_net/provider/api/google/realtime/realtime.py +2 -2
- pygpt_net/provider/api/google/remote_tools.py +93 -0
- pygpt_net/provider/api/google/store.py +546 -0
- pygpt_net/provider/api/google/video.py +9 -4
- pygpt_net/provider/api/google/worker/__init__.py +0 -0
- pygpt_net/provider/api/google/worker/importer.py +392 -0
- pygpt_net/provider/api/openai/computer.py +10 -1
- pygpt_net/provider/api/openai/image.py +42 -19
- pygpt_net/provider/api/openai/store.py +6 -6
- pygpt_net/provider/api/openai/video.py +27 -2
- pygpt_net/provider/api/openai/worker/importer.py +24 -24
- pygpt_net/provider/api/x_ai/image.py +25 -2
- pygpt_net/provider/core/config/patch.py +23 -1
- pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +3 -3
- pygpt_net/provider/core/model/patch.py +17 -3
- pygpt_net/provider/core/preset/json_file.py +13 -7
- pygpt_net/provider/core/{assistant_file → remote_file}/__init__.py +1 -1
- pygpt_net/provider/core/{assistant_file → remote_file}/base.py +9 -9
- pygpt_net/provider/core/remote_file/db_sqlite/__init__.py +12 -0
- pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/patch.py +1 -1
- pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/provider.py +23 -20
- pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/storage.py +35 -27
- pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/utils.py +5 -4
- pygpt_net/provider/core/{assistant_store → remote_store}/__init__.py +1 -1
- pygpt_net/provider/core/{assistant_store → remote_store}/base.py +10 -10
- pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/__init__.py +1 -1
- pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/patch.py +1 -1
- pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/provider.py +16 -15
- pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/storage.py +30 -23
- pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/utils.py +5 -4
- pygpt_net/provider/core/{assistant_store → remote_store}/json_file.py +9 -9
- pygpt_net/provider/llms/google.py +2 -2
- pygpt_net/ui/base/config_dialog.py +3 -2
- pygpt_net/ui/dialog/assistant.py +3 -3
- pygpt_net/ui/dialog/plugins.py +3 -1
- pygpt_net/ui/dialog/remote_store_google.py +539 -0
- pygpt_net/ui/dialog/{assistant_store.py → remote_store_openai.py} +95 -95
- pygpt_net/ui/dialogs.py +5 -3
- pygpt_net/ui/layout/chat/attachments_uploaded.py +3 -3
- pygpt_net/ui/layout/chat/input.py +20 -2
- pygpt_net/ui/layout/chat/painter.py +6 -4
- pygpt_net/ui/layout/toolbox/computer_env.py +26 -8
- pygpt_net/ui/layout/toolbox/image.py +5 -5
- pygpt_net/ui/layout/toolbox/video.py +5 -4
- pygpt_net/ui/main.py +84 -3
- pygpt_net/ui/menu/tools.py +13 -5
- pygpt_net/ui/widget/dialog/base.py +3 -10
- pygpt_net/ui/widget/dialog/remote_store_google.py +56 -0
- pygpt_net/ui/widget/dialog/{assistant_store.py → remote_store_openai.py} +9 -9
- pygpt_net/ui/widget/element/button.py +4 -4
- pygpt_net/ui/widget/lists/remote_store_google.py +248 -0
- pygpt_net/ui/widget/lists/{assistant_store.py → remote_store_openai.py} +21 -21
- pygpt_net/ui/widget/option/checkbox_list.py +47 -9
- pygpt_net/ui/widget/option/combo.py +158 -4
- pygpt_net/ui/widget/textarea/input_extra.py +664 -0
- {pygpt_net-2.7.3.dist-info → pygpt_net-2.7.5.dist-info}/METADATA +48 -9
- {pygpt_net-2.7.3.dist-info → pygpt_net-2.7.5.dist-info}/RECORD +157 -130
- {pygpt_net-2.7.3.dist-info → pygpt_net-2.7.5.dist-info}/LICENSE +0 -0
- {pygpt_net-2.7.3.dist-info → pygpt_net-2.7.5.dist-info}/WHEEL +0 -0
- {pygpt_net-2.7.3.dist-info → pygpt_net-2.7.5.dist-info}/entry_points.txt +0 -0
|
@@ -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:
|
|
9
|
+
# Updated Date: 2026.01.02 19:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtGui import QAction, QIcon
|
|
@@ -18,7 +18,7 @@ from pygpt_net.utils import trans
|
|
|
18
18
|
import pygpt_net.icons_rc
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
class
|
|
21
|
+
class RemoteStoreOpenAIEditorList(BaseList):
|
|
22
22
|
def __init__(self, window=None, id=None):
|
|
23
23
|
"""
|
|
24
24
|
Store select menu (in editor)
|
|
@@ -26,7 +26,7 @@ class AssistantVectorStoreEditorList(BaseList):
|
|
|
26
26
|
:param window: main window
|
|
27
27
|
:param id: parent id
|
|
28
28
|
"""
|
|
29
|
-
super(
|
|
29
|
+
super(RemoteStoreOpenAIEditorList, self).__init__(window)
|
|
30
30
|
self.window = window
|
|
31
31
|
self.id = id
|
|
32
32
|
|
|
@@ -94,7 +94,7 @@ class AssistantVectorStoreEditorList(BaseList):
|
|
|
94
94
|
self._suppress_item_click = True
|
|
95
95
|
self._was_shift_click = True
|
|
96
96
|
if idx.isValid():
|
|
97
|
-
super(
|
|
97
|
+
super(RemoteStoreOpenAIEditorList, self).mousePressEvent(event)
|
|
98
98
|
else:
|
|
99
99
|
event.accept()
|
|
100
100
|
return
|
|
@@ -112,7 +112,7 @@ class AssistantVectorStoreEditorList(BaseList):
|
|
|
112
112
|
return
|
|
113
113
|
# continue with default single selection for clicked row
|
|
114
114
|
|
|
115
|
-
super(
|
|
115
|
+
super(RemoteStoreOpenAIEditorList, self).mousePressEvent(event)
|
|
116
116
|
return
|
|
117
117
|
|
|
118
118
|
# Right click: prepare selection for context menu
|
|
@@ -134,14 +134,14 @@ class AssistantVectorStoreEditorList(BaseList):
|
|
|
134
134
|
event.accept()
|
|
135
135
|
return
|
|
136
136
|
|
|
137
|
-
super(
|
|
137
|
+
super(RemoteStoreOpenAIEditorList, self).mousePressEvent(event)
|
|
138
138
|
|
|
139
139
|
def mouseReleaseEvent(self, event):
|
|
140
140
|
# If the click was a Shift-based range selection, bypass business click
|
|
141
141
|
if event.button() == Qt.LeftButton and self._was_shift_click:
|
|
142
142
|
self._was_shift_click = False
|
|
143
143
|
self._suppress_item_click = False
|
|
144
|
-
super(
|
|
144
|
+
super(RemoteStoreOpenAIEditorList, self).mouseReleaseEvent(event)
|
|
145
145
|
return
|
|
146
146
|
|
|
147
147
|
# Finish "virtual" Ctrl toggle on same row (no business click)
|
|
@@ -163,12 +163,12 @@ class AssistantVectorStoreEditorList(BaseList):
|
|
|
163
163
|
idx = self.indexAt(event.pos())
|
|
164
164
|
if not self._has_multi_selection():
|
|
165
165
|
if idx.isValid() and not self._suppress_item_click:
|
|
166
|
-
self.window.controller.
|
|
166
|
+
self.window.controller.remote_store.openai.select(idx.row())
|
|
167
167
|
self._suppress_item_click = False
|
|
168
|
-
super(
|
|
168
|
+
super(RemoteStoreOpenAIEditorList, self).mouseReleaseEvent(event)
|
|
169
169
|
return
|
|
170
170
|
|
|
171
|
-
super(
|
|
171
|
+
super(RemoteStoreOpenAIEditorList, self).mouseReleaseEvent(event)
|
|
172
172
|
|
|
173
173
|
def click(self, val):
|
|
174
174
|
# Not used; single-selection business click is handled in mouseReleaseEvent
|
|
@@ -187,18 +187,18 @@ class AssistantVectorStoreEditorList(BaseList):
|
|
|
187
187
|
actions = {}
|
|
188
188
|
actions['refresh'] = QAction(
|
|
189
189
|
QIcon(":/icons/reload.svg"),
|
|
190
|
-
trans('dialog.
|
|
190
|
+
trans('dialog.remote_store.menu.current.refresh_store'),
|
|
191
191
|
self
|
|
192
192
|
)
|
|
193
193
|
actions['delete'] = QAction(QIcon(":/icons/delete.svg"), trans('action.delete'), self)
|
|
194
194
|
actions['clear'] = QAction(
|
|
195
195
|
QIcon(":/icons/close.svg"),
|
|
196
|
-
trans('dialog.
|
|
196
|
+
trans('dialog.remote_store.menu.current.clear_files'),
|
|
197
197
|
self
|
|
198
198
|
)
|
|
199
199
|
actions['truncate'] = QAction(
|
|
200
200
|
QIcon(":/icons/delete.svg"),
|
|
201
|
-
trans('dialog.
|
|
201
|
+
trans('dialog.remote_store.menu.current.truncate_files'),
|
|
202
202
|
self
|
|
203
203
|
)
|
|
204
204
|
|
|
@@ -263,12 +263,12 @@ class AssistantVectorStoreEditorList(BaseList):
|
|
|
263
263
|
if isinstance(item, (list, tuple)):
|
|
264
264
|
if item:
|
|
265
265
|
self.restore_after_ctx_menu = False
|
|
266
|
-
self.window.controller.
|
|
266
|
+
self.window.controller.remote_store.openai.delete_by_idx(list(item))
|
|
267
267
|
return
|
|
268
268
|
idx = int(item)
|
|
269
269
|
if idx >= 0:
|
|
270
270
|
self.restore_after_ctx_menu = False
|
|
271
|
-
self.window.controller.
|
|
271
|
+
self.window.controller.remote_store.openai.delete_by_idx(idx)
|
|
272
272
|
|
|
273
273
|
def action_clear(self, item):
|
|
274
274
|
"""
|
|
@@ -279,12 +279,12 @@ class AssistantVectorStoreEditorList(BaseList):
|
|
|
279
279
|
if isinstance(item, (list, tuple)):
|
|
280
280
|
if item:
|
|
281
281
|
self.restore_after_ctx_menu = False
|
|
282
|
-
self.window.controller.
|
|
282
|
+
self.window.controller.remote_store.openai.batch.clear_store_files_by_idx(list(item))
|
|
283
283
|
return
|
|
284
284
|
idx = int(item)
|
|
285
285
|
if idx >= 0:
|
|
286
286
|
self.restore_after_ctx_menu = False
|
|
287
|
-
self.window.controller.
|
|
287
|
+
self.window.controller.remote_store.openai.batch.clear_store_files_by_idx(idx)
|
|
288
288
|
|
|
289
289
|
def action_truncate(self, item):
|
|
290
290
|
"""
|
|
@@ -295,12 +295,12 @@ class AssistantVectorStoreEditorList(BaseList):
|
|
|
295
295
|
if isinstance(item, (list, tuple)):
|
|
296
296
|
if item:
|
|
297
297
|
self.restore_after_ctx_menu = False
|
|
298
|
-
self.window.controller.
|
|
298
|
+
self.window.controller.remote_store.openai.batch.truncate_store_files_by_idx(list(item))
|
|
299
299
|
return
|
|
300
300
|
idx = int(item)
|
|
301
301
|
if idx >= 0:
|
|
302
302
|
self.restore_after_ctx_menu = False
|
|
303
|
-
self.window.controller.
|
|
303
|
+
self.window.controller.remote_store.openai.batch.truncate_store_files_by_idx(idx)
|
|
304
304
|
|
|
305
305
|
def action_refresh(self, item):
|
|
306
306
|
"""
|
|
@@ -309,9 +309,9 @@ class AssistantVectorStoreEditorList(BaseList):
|
|
|
309
309
|
if isinstance(item, (list, tuple)):
|
|
310
310
|
if item:
|
|
311
311
|
self.restore_after_ctx_menu = False
|
|
312
|
-
self.window.controller.
|
|
312
|
+
self.window.controller.remote_store.openai.refresh_by_idx(list(item))
|
|
313
313
|
return
|
|
314
314
|
idx = int(item)
|
|
315
315
|
if idx >= 0:
|
|
316
316
|
self.restore_after_ctx_menu = False
|
|
317
|
-
self.window.controller.
|
|
317
|
+
self.window.controller.remote_store.openai.refresh_by_idx(idx)
|
|
@@ -6,10 +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:
|
|
9
|
+
# Updated Date: 2026.01.01 15:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtWidgets import QCheckBox, QWidget, QPushButton
|
|
13
|
+
from PySide6.QtGui import QIcon
|
|
14
|
+
from PySide6.QtCore import QSize, Qt
|
|
13
15
|
|
|
14
16
|
from pygpt_net.ui.base.flow_layout import FlowLayout
|
|
15
17
|
from pygpt_net.utils import trans
|
|
@@ -43,6 +45,10 @@ class OptionCheckboxList(QWidget):
|
|
|
43
45
|
self.keys = {}
|
|
44
46
|
self.boxes = {}
|
|
45
47
|
|
|
48
|
+
# overlay button config
|
|
49
|
+
self._overlay_margin = 4 # px
|
|
50
|
+
self.btn_select = None
|
|
51
|
+
|
|
46
52
|
# init from option data
|
|
47
53
|
if self.option is not None:
|
|
48
54
|
if "label" in self.option and self.option["label"] is not None \
|
|
@@ -84,20 +90,49 @@ class OptionCheckboxList(QWidget):
|
|
|
84
90
|
for widget in widgets:
|
|
85
91
|
self.layout.addWidget(widget)
|
|
86
92
|
|
|
87
|
-
# select/unselect all button
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
# do not add the select/unselect all button to the flow layout
|
|
94
|
+
# it will be overlaid in the top-right corner to avoid affecting layout flow
|
|
95
|
+
self.layout.setContentsMargins(0, 0, 0, 0)
|
|
96
|
+
self.setLayout(self.layout)
|
|
97
|
+
|
|
98
|
+
# overlay select/unselect all button pinned to top-right corner
|
|
99
|
+
self.btn_select = QPushButton(self)
|
|
100
|
+
self.btn_select.setObjectName("btn_select_all_overlay")
|
|
101
|
+
self.btn_select.setToolTip(trans("action.select_unselect_all"))
|
|
102
|
+
self.btn_select.setIcon(QIcon(":/icons/asterisk.svg"))
|
|
103
|
+
self.btn_select.setIconSize(QSize(16, 16))
|
|
104
|
+
self.btn_select.setFixedSize(22, 22)
|
|
105
|
+
self.btn_select.setFocusPolicy(Qt.NoFocus)
|
|
106
|
+
self.btn_select.setCursor(Qt.PointingHandCursor)
|
|
107
|
+
self.btn_select.setStyleSheet("QPushButton { border: none; padding: 0; background: transparent; }")
|
|
108
|
+
self.btn_select.clicked.connect(
|
|
91
109
|
lambda: self.window.controller.config.checkbox_list.on_select_all(
|
|
92
110
|
self.parent_id,
|
|
93
111
|
self.id,
|
|
94
112
|
self.option
|
|
95
113
|
)
|
|
96
114
|
)
|
|
97
|
-
self.
|
|
115
|
+
self.btn_select.raise_()
|
|
116
|
+
self._place_select_button()
|
|
98
117
|
|
|
99
|
-
|
|
100
|
-
|
|
118
|
+
def _place_select_button(self) -> None:
|
|
119
|
+
"""
|
|
120
|
+
Place the overlay select/unselect button in the top-right corner.
|
|
121
|
+
"""
|
|
122
|
+
if not self.btn_select:
|
|
123
|
+
return
|
|
124
|
+
m = self._overlay_margin
|
|
125
|
+
w = self.btn_select.width()
|
|
126
|
+
x = max(0, self.width() - w - m)
|
|
127
|
+
y = m
|
|
128
|
+
self.btn_select.move(x, y)
|
|
129
|
+
|
|
130
|
+
def resizeEvent(self, event):
|
|
131
|
+
"""
|
|
132
|
+
Keep the overlay button pinned to the top-right on resize.
|
|
133
|
+
"""
|
|
134
|
+
super().resizeEvent(event)
|
|
135
|
+
self._place_select_button()
|
|
101
136
|
|
|
102
137
|
def update_boxes_list(self, keys: dict) -> None:
|
|
103
138
|
"""
|
|
@@ -141,6 +176,9 @@ class OptionCheckboxList(QWidget):
|
|
|
141
176
|
self.boxes[key] = checkbox
|
|
142
177
|
self.layout.addWidget(checkbox)
|
|
143
178
|
|
|
179
|
+
# keep the overlay button in place after list update
|
|
180
|
+
self._place_select_button()
|
|
181
|
+
|
|
144
182
|
def setIcon(self, icon: str):
|
|
145
183
|
"""
|
|
146
184
|
Set icon
|
|
@@ -185,4 +223,4 @@ class OptionCheckboxList(QWidget):
|
|
|
185
223
|
"""
|
|
186
224
|
if key in self.boxes:
|
|
187
225
|
return self.boxes[key].isChecked()
|
|
188
|
-
return False
|
|
226
|
+
return False
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date:
|
|
9
|
+
# Updated Date: 2026.01.01 15:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
|
+
import sys
|
|
11
12
|
|
|
12
13
|
from PySide6.QtCore import Qt, QEvent, QTimer, QRect, Property
|
|
13
14
|
from PySide6.QtWidgets import (
|
|
@@ -21,12 +22,13 @@ from PySide6.QtWidgets import (
|
|
|
21
22
|
QListView,
|
|
22
23
|
QStyledItemDelegate,
|
|
23
24
|
QStyleOptionViewItem,
|
|
25
|
+
QApplication,
|
|
24
26
|
)
|
|
25
27
|
from PySide6.QtGui import (
|
|
26
28
|
QFontMetrics,
|
|
27
29
|
QStandardItem,
|
|
28
30
|
QStandardItemModel,
|
|
29
|
-
QIcon,
|
|
31
|
+
QIcon,
|
|
30
32
|
QColor,
|
|
31
33
|
QPainter,
|
|
32
34
|
QPen,
|
|
@@ -399,6 +401,9 @@ class SearchableCombo(SeparatorComboBox):
|
|
|
399
401
|
self._swallow_release_once: bool = False # kept for compatibility; not used in the new flow
|
|
400
402
|
self._open_on_release: bool = False # open popup on mouse release (non-arrow path)
|
|
401
403
|
|
|
404
|
+
# One-shot guard to ignore replayed click when user closes popup by clicking this same combo
|
|
405
|
+
self._prevent_reopen_once: bool = False
|
|
406
|
+
|
|
402
407
|
# Popup fitting helpers
|
|
403
408
|
self._fit_in_progress: bool = False
|
|
404
409
|
self._popup_parent_window = None
|
|
@@ -508,6 +513,11 @@ class SearchableCombo(SeparatorComboBox):
|
|
|
508
513
|
QTimer.singleShot(0, self._fit_popup_to_window)
|
|
509
514
|
self._refresh_popup_view()
|
|
510
515
|
|
|
516
|
+
# Minimal robustness: re-ensure header after popup is fully shown (helps on Windows when switching combos)
|
|
517
|
+
QTimer.singleShot(0, self._ensure_header_after_show)
|
|
518
|
+
QTimer.singleShot(15, self._ensure_header_after_show)
|
|
519
|
+
QTimer.singleShot(60, self._ensure_header_after_show)
|
|
520
|
+
|
|
511
521
|
def hidePopup(self):
|
|
512
522
|
"""Close popup and restore normal display text; remove header/margins."""
|
|
513
523
|
super().hidePopup()
|
|
@@ -673,6 +683,12 @@ class SearchableCombo(SeparatorComboBox):
|
|
|
673
683
|
Use release-to-open on the non-arrow area to avoid immediate close when the popup opens upward.
|
|
674
684
|
Keep the arrow area with the default toggle behaviour from the base class.
|
|
675
685
|
"""
|
|
686
|
+
# One-shot guard: if popup was closed by clicking this same combo, ignore the replayed click
|
|
687
|
+
if event.button() == Qt.LeftButton and self._prevent_reopen_once:
|
|
688
|
+
self._prevent_reopen_once = False
|
|
689
|
+
event.accept()
|
|
690
|
+
return
|
|
691
|
+
|
|
676
692
|
if event.button() == Qt.LeftButton and self.isEnabled():
|
|
677
693
|
arrow_rect = self._arrow_rect()
|
|
678
694
|
if arrow_rect.contains(event.pos()):
|
|
@@ -722,6 +738,13 @@ class SearchableCombo(SeparatorComboBox):
|
|
|
722
738
|
event.accept()
|
|
723
739
|
return
|
|
724
740
|
if event.key() == Qt.Key_Escape:
|
|
741
|
+
# Explicitly close so the next click opens immediately
|
|
742
|
+
self.hidePopup()
|
|
743
|
+
event.accept()
|
|
744
|
+
return
|
|
745
|
+
if event.key() == Qt.Key_Right:
|
|
746
|
+
# Inject highlighted item's text into the search header
|
|
747
|
+
self._apply_highlighted_to_search_input()
|
|
725
748
|
event.accept()
|
|
726
749
|
return
|
|
727
750
|
super().keyPressEvent(event)
|
|
@@ -737,6 +760,53 @@ class SearchableCombo(SeparatorComboBox):
|
|
|
737
760
|
- Keep popup horizontally inside the parent window while resizing/moving.
|
|
738
761
|
"""
|
|
739
762
|
if obj is self._popup_container and self._popup_container is not None:
|
|
763
|
+
# Open target combo immediately when user clicks outside current popup
|
|
764
|
+
if event.type() == QEvent.MouseButtonPress:
|
|
765
|
+
try:
|
|
766
|
+
# Detect left-click outside popup window
|
|
767
|
+
left_btn = (getattr(event, "button", lambda: None)() == Qt.LeftButton)
|
|
768
|
+
pos_local = getattr(event, "position", lambda: None)()
|
|
769
|
+
inside = self._popup_container.rect().contains(pos_local.toPoint()) if pos_local is not None else True
|
|
770
|
+
if left_btn and not inside:
|
|
771
|
+
# Find widget under cursor and see if it's a different combo
|
|
772
|
+
gp = getattr(event, "globalPosition", None)
|
|
773
|
+
gp_pt = gp().toPoint() if callable(gp) else getattr(event, "globalPos", lambda: None)()
|
|
774
|
+
if gp_pt is None:
|
|
775
|
+
gp_pt = QApplication.instance().cursor().pos() if QApplication.instance() else None
|
|
776
|
+
target = QApplication.widgetAt(gp_pt) if gp_pt is not None else None
|
|
777
|
+
target_combo = self._ascend_to_combo(target)
|
|
778
|
+
if target_combo is self:
|
|
779
|
+
# Clicked back on the owner combo: arm one-shot guard to suppress reopen
|
|
780
|
+
self._prevent_reopen_once = True
|
|
781
|
+
elif isinstance(target_combo, SearchableCombo) and target_combo.isEnabled():
|
|
782
|
+
def _open_other():
|
|
783
|
+
if target_combo and target_combo.isEnabled():
|
|
784
|
+
try:
|
|
785
|
+
target_combo.setFocus(Qt.MouseFocusReason)
|
|
786
|
+
except Exception:
|
|
787
|
+
pass
|
|
788
|
+
target_combo.showPopup()
|
|
789
|
+
if sys.platform != "win32":
|
|
790
|
+
QTimer.singleShot(0, _open_other)
|
|
791
|
+
else:
|
|
792
|
+
container = self._popup_container
|
|
793
|
+
opened_on_destroy = False
|
|
794
|
+
try:
|
|
795
|
+
if container is not None:
|
|
796
|
+
container.destroyed.connect(lambda *_: QTimer.singleShot(0, _open_other))
|
|
797
|
+
opened_on_destroy = True
|
|
798
|
+
except Exception:
|
|
799
|
+
pass
|
|
800
|
+
if not opened_on_destroy:
|
|
801
|
+
QTimer.singleShot(50, _open_other)
|
|
802
|
+
try:
|
|
803
|
+
self.hidePopup()
|
|
804
|
+
except Exception:
|
|
805
|
+
pass
|
|
806
|
+
except Exception:
|
|
807
|
+
pass
|
|
808
|
+
return False
|
|
809
|
+
|
|
740
810
|
if event.type() in (QEvent.Resize, QEvent.Show):
|
|
741
811
|
self._place_popup_header()
|
|
742
812
|
# Also ensure fitting after container geometry changes
|
|
@@ -753,6 +823,12 @@ class SearchableCombo(SeparatorComboBox):
|
|
|
753
823
|
if event.type() == QEvent.KeyPress:
|
|
754
824
|
key = event.key()
|
|
755
825
|
if key == Qt.Key_Escape:
|
|
826
|
+
# Explicitly close popup from header
|
|
827
|
+
self.hidePopup()
|
|
828
|
+
return True
|
|
829
|
+
if key == Qt.Key_Right:
|
|
830
|
+
# Insert highlighted item's value into the search input
|
|
831
|
+
self._apply_highlighted_to_search_input()
|
|
756
832
|
return True
|
|
757
833
|
if key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown, Qt.Key_Home, Qt.Key_End):
|
|
758
834
|
self._handle_navigation_key(key)
|
|
@@ -765,15 +841,33 @@ class SearchableCombo(SeparatorComboBox):
|
|
|
765
841
|
view = self.view()
|
|
766
842
|
if view is not None and (obj is view or obj is getattr(view, "viewport", lambda: None)()):
|
|
767
843
|
if event.type() == QEvent.KeyPress:
|
|
768
|
-
|
|
844
|
+
key = event.key()
|
|
845
|
+
if key in (Qt.Key_Return, Qt.Key_Enter):
|
|
769
846
|
self._commit_view_current()
|
|
770
847
|
return True
|
|
771
|
-
if
|
|
848
|
+
if key == Qt.Key_Escape:
|
|
849
|
+
# Close from view/viewport ESC and keep control flow consistent
|
|
850
|
+
self.hidePopup()
|
|
851
|
+
return True
|
|
852
|
+
if key == Qt.Key_Right:
|
|
853
|
+
# Insert highlighted item's value into the search input
|
|
854
|
+
self._apply_highlighted_to_search_input()
|
|
772
855
|
return True
|
|
773
856
|
return False
|
|
774
857
|
|
|
775
858
|
return super().eventFilter(obj, event)
|
|
776
859
|
|
|
860
|
+
# Helpers to walk up to owning combo under a point
|
|
861
|
+
def _ascend_to_combo(self, widget):
|
|
862
|
+
"""Ascend from a widget to the nearest SearchableCombo owner if present."""
|
|
863
|
+
w = widget
|
|
864
|
+
while w is not None and not isinstance(w, SearchableCombo):
|
|
865
|
+
try:
|
|
866
|
+
w = w.parentWidget()
|
|
867
|
+
except Exception:
|
|
868
|
+
return None
|
|
869
|
+
return w
|
|
870
|
+
|
|
777
871
|
# ----- Magnifier helpers -----
|
|
778
872
|
|
|
779
873
|
def _ensure_magnifier_on(self, line_edit: QLineEdit | None):
|
|
@@ -938,6 +1032,44 @@ class SearchableCombo(SeparatorComboBox):
|
|
|
938
1032
|
self._popup_header.setCursorPosition(len(self._popup_header.text()))
|
|
939
1033
|
self._ensure_clear_button_visible(self._popup_header)
|
|
940
1034
|
|
|
1035
|
+
# ----- Minimal ensure after show (mainly for Windows combo-to-combo switch) -----
|
|
1036
|
+
|
|
1037
|
+
def _ensure_header_after_show(self):
|
|
1038
|
+
"""
|
|
1039
|
+
Re-apply critical bits after the popup is up:
|
|
1040
|
+
- header exists and is parented to the popup container,
|
|
1041
|
+
- viewport top margin equals header height,
|
|
1042
|
+
- header is placed and focused.
|
|
1043
|
+
"""
|
|
1044
|
+
if not self._popup_open or not self.search:
|
|
1045
|
+
return
|
|
1046
|
+
view = self.view()
|
|
1047
|
+
if view is None:
|
|
1048
|
+
return
|
|
1049
|
+
container = self._popup_container or view.window()
|
|
1050
|
+
if container is None:
|
|
1051
|
+
return
|
|
1052
|
+
|
|
1053
|
+
if self._popup_header is None:
|
|
1054
|
+
self._prepare_popup_header()
|
|
1055
|
+
return
|
|
1056
|
+
|
|
1057
|
+
try:
|
|
1058
|
+
if self._popup_header.parent() is not container:
|
|
1059
|
+
self._popup_header.setParent(container)
|
|
1060
|
+
except Exception:
|
|
1061
|
+
pass
|
|
1062
|
+
|
|
1063
|
+
try:
|
|
1064
|
+
view.setViewportMargins(0, self._popup_header_h, 0, 0)
|
|
1065
|
+
except Exception:
|
|
1066
|
+
pass
|
|
1067
|
+
|
|
1068
|
+
self._place_popup_header()
|
|
1069
|
+
self._ensure_magnifier_on(self._popup_header)
|
|
1070
|
+
self._ensure_clear_button_visible(self._popup_header)
|
|
1071
|
+
self._focus_header_async()
|
|
1072
|
+
|
|
941
1073
|
# ----- Search behaviour -----
|
|
942
1074
|
|
|
943
1075
|
def _on_search_text_changed(self, text: str):
|
|
@@ -1288,6 +1420,28 @@ class SearchableCombo(SeparatorComboBox):
|
|
|
1288
1420
|
except Exception:
|
|
1289
1421
|
pass
|
|
1290
1422
|
|
|
1423
|
+
# ----- Convenience: inject highlighted row text into the search header -----
|
|
1424
|
+
|
|
1425
|
+
def _apply_highlighted_to_search_input(self):
|
|
1426
|
+
"""
|
|
1427
|
+
Put the currently highlighted row's display text into the popup search input.
|
|
1428
|
+
Does nothing if the popup or header is not available. The normal textChanged
|
|
1429
|
+
handler will update scrolling and internal state.
|
|
1430
|
+
"""
|
|
1431
|
+
if not self._popup_open or self._popup_header is None:
|
|
1432
|
+
return
|
|
1433
|
+
view = self.view()
|
|
1434
|
+
if view is None:
|
|
1435
|
+
return
|
|
1436
|
+
idx = view.currentIndex()
|
|
1437
|
+
row = self._sanitize_index(idx.row() if idx.isValid() else -1)
|
|
1438
|
+
if row == -1:
|
|
1439
|
+
return
|
|
1440
|
+
text = self.itemText(row) or ""
|
|
1441
|
+
self._popup_header.setText(text)
|
|
1442
|
+
self._ensure_clear_button_visible(self._popup_header)
|
|
1443
|
+
self._popup_header.setCursorPosition(len(text))
|
|
1444
|
+
|
|
1291
1445
|
|
|
1292
1446
|
class NoScrollCombo(SearchableCombo):
|
|
1293
1447
|
"""A combo box that disables mouse wheel scrolling, extended with optional search support."""
|