pygpt-net 2.7.2__py3-none-any.whl → 2.7.4__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/app.py +382 -350
- pygpt_net/controller/chat/attachment.py +5 -1
- pygpt_net/controller/chat/image.py +40 -5
- pygpt_net/controller/files/files.py +3 -1
- pygpt_net/controller/layout/layout.py +2 -2
- pygpt_net/controller/media/media.py +70 -1
- pygpt_net/controller/theme/nodes.py +2 -1
- pygpt_net/controller/ui/mode.py +5 -1
- pygpt_net/controller/ui/ui.py +17 -2
- pygpt_net/core/filesystem/url.py +4 -1
- pygpt_net/core/render/web/helpers.py +5 -0
- pygpt_net/data/config/config.json +5 -4
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/config/settings.json +0 -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 +6 -0
- pygpt_net/data/locale/locale.en.ini +7 -1
- pygpt_net/data/locale/locale.es.ini +6 -0
- pygpt_net/data/locale/locale.fr.ini +6 -0
- pygpt_net/data/locale/locale.it.ini +6 -0
- pygpt_net/data/locale/locale.pl.ini +7 -1
- pygpt_net/data/locale/locale.uk.ini +6 -0
- pygpt_net/data/locale/locale.zh.ini +6 -0
- pygpt_net/launcher.py +115 -55
- pygpt_net/preload.py +243 -0
- pygpt_net/provider/api/google/image.py +317 -10
- pygpt_net/provider/api/google/video.py +160 -4
- pygpt_net/provider/api/openai/image.py +201 -93
- pygpt_net/provider/api/openai/video.py +99 -24
- pygpt_net/provider/api/x_ai/image.py +25 -2
- pygpt_net/provider/core/config/patch.py +17 -1
- pygpt_net/ui/layout/chat/input.py +20 -2
- pygpt_net/ui/layout/chat/painter.py +6 -4
- pygpt_net/ui/layout/toolbox/image.py +21 -11
- pygpt_net/ui/layout/toolbox/raw.py +2 -2
- pygpt_net/ui/layout/toolbox/video.py +22 -9
- pygpt_net/ui/main.py +84 -3
- pygpt_net/ui/widget/dialog/base.py +3 -10
- pygpt_net/ui/widget/option/combo.py +119 -1
- pygpt_net/ui/widget/textarea/input_extra.py +664 -0
- {pygpt_net-2.7.2.dist-info → pygpt_net-2.7.4.dist-info}/METADATA +27 -20
- {pygpt_net-2.7.2.dist-info → pygpt_net-2.7.4.dist-info}/RECORD +48 -46
- {pygpt_net-2.7.2.dist-info → pygpt_net-2.7.4.dist-info}/LICENSE +0 -0
- {pygpt_net-2.7.2.dist-info → pygpt_net-2.7.4.dist-info}/WHEEL +0 -0
- {pygpt_net-2.7.2.dist-info → pygpt_net-2.7.4.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: 2025.
|
|
9
|
+
# Updated Date: 2025.12.31 16:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from functools import partial
|
|
@@ -26,6 +26,7 @@ from pygpt_net.ui.widget.audio.input_button import AudioInputButton
|
|
|
26
26
|
from pygpt_net.ui.widget.element.labels import HelpLabel
|
|
27
27
|
from pygpt_net.ui.widget.tabs.Input import InputTabs
|
|
28
28
|
from pygpt_net.ui.widget.textarea.input import ChatInput
|
|
29
|
+
from pygpt_net.ui.widget.textarea.input_extra import ExtraInput
|
|
29
30
|
from pygpt_net.utils import trans
|
|
30
31
|
|
|
31
32
|
|
|
@@ -55,6 +56,7 @@ class Input:
|
|
|
55
56
|
:return: QWidget
|
|
56
57
|
"""
|
|
57
58
|
input = self.setup_input()
|
|
59
|
+
input_extra = self.setup_input_extra()
|
|
58
60
|
files = self.setup_attachments()
|
|
59
61
|
files_uploaded = self.setup_attachments_uploaded()
|
|
60
62
|
files_ctx = self.setup_attachments_ctx()
|
|
@@ -66,6 +68,7 @@ class Input:
|
|
|
66
68
|
tabs.addTab(files, trans('attachments.tab'))
|
|
67
69
|
tabs.addTab(files_uploaded, trans('attachments_uploaded.tab'))
|
|
68
70
|
tabs.addTab(files_ctx, trans('attachments_uploaded.tab'))
|
|
71
|
+
tabs.addTab(input_extra, trans('input.tab.extra'))
|
|
69
72
|
tabs.currentChanged.connect(self.update_min_height)
|
|
70
73
|
|
|
71
74
|
tabs.setTabIcon(0, QIcon(":/icons/input.svg"))
|
|
@@ -97,6 +100,21 @@ class Input:
|
|
|
97
100
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
98
101
|
return widget
|
|
99
102
|
|
|
103
|
+
def setup_input_extra(self) -> QWidget:
|
|
104
|
+
"""
|
|
105
|
+
Setup input tab (extra)
|
|
106
|
+
|
|
107
|
+
:return: QWidget
|
|
108
|
+
"""
|
|
109
|
+
self.window.ui.nodes['input_extra'] = ExtraInput(self.window)
|
|
110
|
+
self.window.ui.nodes['input_extra'].setMinimumHeight(self.min_height_input)
|
|
111
|
+
|
|
112
|
+
widget = QWidget()
|
|
113
|
+
layout = QVBoxLayout(widget)
|
|
114
|
+
layout.addWidget(self.window.ui.nodes['input_extra'])
|
|
115
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
116
|
+
return widget
|
|
117
|
+
|
|
100
118
|
def setup_attachments(self) -> QWidget:
|
|
101
119
|
"""
|
|
102
120
|
Setup attachments
|
|
@@ -245,7 +263,7 @@ class Input:
|
|
|
245
263
|
controller_ui = self.window.controller.ui
|
|
246
264
|
|
|
247
265
|
idx = tabs.currentIndex()
|
|
248
|
-
if idx == 0:
|
|
266
|
+
if idx == 0 or idx == 4:
|
|
249
267
|
# nodes['input'].setMinimumHeight(self.min_height_input)
|
|
250
268
|
tabs.setMinimumHeight(self.min_height_input_tab)
|
|
251
269
|
sizes = controller_ui.splitter_output_size_input
|
|
@@ -15,6 +15,7 @@ from PySide6.QtCore import QSize
|
|
|
15
15
|
|
|
16
16
|
from pygpt_net.ui.widget.draw.painter import PainterWidget
|
|
17
17
|
from pygpt_net.ui.widget.element.labels import HelpLabel
|
|
18
|
+
from pygpt_net.ui.widget.option.combo import NoScrollCombo
|
|
18
19
|
from pygpt_net.utils import trans
|
|
19
20
|
|
|
20
21
|
|
|
@@ -57,7 +58,7 @@ class Painter:
|
|
|
57
58
|
key = 'painter.select.brush.size'
|
|
58
59
|
if nodes.get(key) is None:
|
|
59
60
|
sizes = common.get_sizes()
|
|
60
|
-
cb =
|
|
61
|
+
cb = NoScrollCombo(self.window)
|
|
61
62
|
cb.addItems(sizes)
|
|
62
63
|
cb.currentTextChanged.connect(common.change_brush_size)
|
|
63
64
|
cb.setMinimumContentsLength(10)
|
|
@@ -67,7 +68,7 @@ class Painter:
|
|
|
67
68
|
key = 'painter.select.canvas.size'
|
|
68
69
|
if nodes.get(key) is None:
|
|
69
70
|
canvas_sizes = common.get_canvas_sizes()
|
|
70
|
-
cb =
|
|
71
|
+
cb = NoScrollCombo(self.window)
|
|
71
72
|
cb.addItems(canvas_sizes)
|
|
72
73
|
cb.setMinimumContentsLength(20)
|
|
73
74
|
cb.setSizeAdjustPolicy(QComboBox.AdjustToContents)
|
|
@@ -76,7 +77,7 @@ class Painter:
|
|
|
76
77
|
|
|
77
78
|
key = 'painter.select.brush.color'
|
|
78
79
|
if nodes.get(key) is None:
|
|
79
|
-
cb =
|
|
80
|
+
cb = NoScrollCombo(self.window)
|
|
80
81
|
cb.setIconSize(QSize(16, 16))
|
|
81
82
|
colors = common.get_colors()
|
|
82
83
|
for color_name, color_value in colors.items():
|
|
@@ -86,13 +87,14 @@ class Painter:
|
|
|
86
87
|
cb.addItem(icon, color_name, color_value)
|
|
87
88
|
cb.currentTextChanged.connect(common.change_brush_color)
|
|
88
89
|
cb.setMinimumContentsLength(10)
|
|
90
|
+
cb.setMinimumWidth(205)
|
|
89
91
|
cb.setSizeAdjustPolicy(QComboBox.AdjustToContents)
|
|
90
92
|
nodes[key] = cb
|
|
91
93
|
|
|
92
94
|
# Zoom combo (view-only scale) placed to the right of canvas size
|
|
93
95
|
key = 'painter.select.zoom'
|
|
94
96
|
if nodes.get(key) is None:
|
|
95
|
-
cb =
|
|
97
|
+
cb = NoScrollCombo(self.window)
|
|
96
98
|
cb.setMinimumContentsLength(8)
|
|
97
99
|
cb.setSizeAdjustPolicy(QComboBox.AdjustToContents)
|
|
98
100
|
|
|
@@ -6,13 +6,15 @@
|
|
|
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.12.
|
|
9
|
+
# Updated Date: 2025.12.30 22:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
|
-
from PySide6.QtWidgets import QVBoxLayout, QWidget
|
|
12
|
+
from PySide6.QtWidgets import QVBoxLayout, QWidget, QHBoxLayout
|
|
13
13
|
|
|
14
14
|
from pygpt_net.ui.widget.option.combo import OptionCombo
|
|
15
|
-
from pygpt_net.ui.widget.option.
|
|
15
|
+
from pygpt_net.ui.widget.option.input import OptionInput
|
|
16
|
+
from pygpt_net.ui.widget.option.toggle_label import ToggleLabel
|
|
17
|
+
from pygpt_net.utils import trans
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class Image:
|
|
@@ -33,33 +35,41 @@ class Image:
|
|
|
33
35
|
"""
|
|
34
36
|
option = {
|
|
35
37
|
"type": "int",
|
|
36
|
-
"slider": True,
|
|
37
38
|
"label": "img_variants",
|
|
38
39
|
"min": 1,
|
|
39
40
|
"max": 4,
|
|
40
|
-
"step": 1,
|
|
41
41
|
"value": 1,
|
|
42
|
-
"multiplier": 1,
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
ui = self.window.ui
|
|
46
45
|
conf_global = ui.config['global']
|
|
47
46
|
|
|
48
|
-
container = QWidget()
|
|
47
|
+
container = QWidget(parent=self.window)
|
|
49
48
|
ui.nodes['dalle.options'] = container
|
|
50
49
|
|
|
51
|
-
conf_global['img_variants'] =
|
|
50
|
+
conf_global['img_variants'] = OptionInput(self.window, 'global', 'img_variants', option)
|
|
51
|
+
conf_global['img_variants'].setToolTip(trans("toolbox.img_variants.label"))
|
|
52
52
|
|
|
53
53
|
option_resolutions = self.window.core.image.get_resolution_option()
|
|
54
54
|
conf_global['img_resolution'] = OptionCombo(self.window, 'global', 'img_resolution', option_resolutions)
|
|
55
55
|
conf_global['img_resolution'].setMinimumWidth(160)
|
|
56
56
|
|
|
57
|
+
conf_global['img.remix'] = ToggleLabel(trans("img.remix"), parent=self.window)
|
|
58
|
+
conf_global['img.remix'].box.setToolTip(trans("img.remix.tooltip"))
|
|
59
|
+
conf_global['img.remix'].box.toggled.connect(self.window.controller.media.toggle_remix_image)
|
|
60
|
+
|
|
61
|
+
cols = QHBoxLayout()
|
|
62
|
+
cols.addWidget(conf_global['img_resolution'], 3)
|
|
63
|
+
cols.addWidget(conf_global['img_variants'], 1)
|
|
64
|
+
cols.setContentsMargins(2, 5, 5, 5)
|
|
65
|
+
|
|
57
66
|
rows = QVBoxLayout()
|
|
58
|
-
rows.
|
|
59
|
-
rows.addWidget(conf_global['
|
|
67
|
+
rows.addLayout(cols)
|
|
68
|
+
rows.addWidget(conf_global['img.remix'])
|
|
60
69
|
rows.setContentsMargins(2, 5, 5, 5)
|
|
61
70
|
|
|
62
71
|
container.setLayout(rows)
|
|
63
|
-
container.setContentsMargins(2, 0, 0,
|
|
72
|
+
container.setContentsMargins(2, 0, 0, 10)
|
|
73
|
+
container.setFixedHeight(100)
|
|
64
74
|
|
|
65
75
|
return container
|
|
@@ -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.12.
|
|
9
|
+
# Updated Date: 2025.12.30 22:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QWidget, QCheckBox
|
|
@@ -34,7 +34,7 @@ class Raw:
|
|
|
34
34
|
ui = self.window.ui
|
|
35
35
|
conf_global = ui.config['global']
|
|
36
36
|
|
|
37
|
-
container = QWidget()
|
|
37
|
+
container = QWidget(parent=self.window)
|
|
38
38
|
ui.nodes['media.raw'] = container
|
|
39
39
|
|
|
40
40
|
conf_global['img_raw'] = QCheckBox(trans("img.raw"), parent=container)
|
|
@@ -6,13 +6,15 @@
|
|
|
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.12.
|
|
9
|
+
# Updated Date: 2025.12.30 22:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
|
-
from PySide6.QtWidgets import QWidget, QHBoxLayout
|
|
12
|
+
from PySide6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout
|
|
13
13
|
|
|
14
14
|
from pygpt_net.ui.widget.option.combo import OptionCombo
|
|
15
15
|
from pygpt_net.ui.widget.option.input import OptionInput
|
|
16
|
+
from pygpt_net.ui.widget.option.toggle_label import ToggleLabel
|
|
17
|
+
from pygpt_net.utils import trans
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class Video:
|
|
@@ -34,7 +36,7 @@ class Video:
|
|
|
34
36
|
ui = self.window.ui
|
|
35
37
|
conf_global = ui.config['global']
|
|
36
38
|
|
|
37
|
-
container = QWidget()
|
|
39
|
+
container = QWidget(parent=self.window)
|
|
38
40
|
ui.nodes['video.options'] = container
|
|
39
41
|
|
|
40
42
|
option_ratio = self.window.core.video.get_aspect_ratio_option()
|
|
@@ -44,18 +46,29 @@ class Video:
|
|
|
44
46
|
conf_global['video.aspect_ratio'] = OptionCombo(self.window, 'global', 'video.aspect_ratio', option_ratio)
|
|
45
47
|
conf_global['video.resolution'] = OptionCombo(self.window, 'global', 'video.resolution', option_resolution)
|
|
46
48
|
conf_global['video.duration'] = OptionInput(self.window, 'global', 'video.duration', option_duration)
|
|
49
|
+
conf_global['video.duration'].setToolTip(trans('settings.video.duration.desc'))
|
|
47
50
|
|
|
48
51
|
conf_global['video.aspect_ratio'].setMinimumWidth(120)
|
|
49
52
|
conf_global['video.resolution'].setMinimumWidth(120)
|
|
50
53
|
conf_global['video.duration'].setMinimumWidth(50)
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
conf_global['video.remix'] = ToggleLabel(trans("video.remix"), parent=self.window)
|
|
56
|
+
conf_global['video.remix'].box.setToolTip(trans("video.remix.tooltip"))
|
|
57
|
+
conf_global['video.remix'].box.toggled.connect(self.window.controller.media.toggle_remix_video)
|
|
58
|
+
|
|
59
|
+
cols = QHBoxLayout()
|
|
60
|
+
cols.addWidget(conf_global['video.resolution'], 2)
|
|
61
|
+
cols.addWidget(conf_global['video.aspect_ratio'], 2)
|
|
62
|
+
cols.addWidget(conf_global['video.duration'], 1)
|
|
63
|
+
cols.setContentsMargins(2, 5, 5, 5)
|
|
64
|
+
|
|
65
|
+
rows = QVBoxLayout()
|
|
66
|
+
rows.addLayout(cols)
|
|
67
|
+
rows.addWidget(conf_global['video.remix'])
|
|
68
|
+
rows.setContentsMargins(0, 0, 0, 0)
|
|
57
69
|
|
|
58
70
|
container.setLayout(rows)
|
|
59
|
-
container.setContentsMargins(2, 0, 0,
|
|
71
|
+
container.setContentsMargins(2, 0, 0, 10)
|
|
72
|
+
container.setFixedHeight(90)
|
|
60
73
|
|
|
61
74
|
return container
|
pygpt_net/ui/main.py
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
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.12.
|
|
9
|
+
# Updated Date: 2025.12.31 14:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import os
|
|
13
13
|
|
|
14
14
|
from PySide6.QtCore import QTimer, Signal, Slot, QThreadPool, QEvent, Qt, QLoggingCategory, QEventLoop
|
|
15
|
-
from PySide6.QtGui import QShortcut, QKeySequence
|
|
15
|
+
from PySide6.QtGui import QShortcut, QKeySequence, QKeyEvent
|
|
16
16
|
from PySide6.QtWidgets import QMainWindow, QApplication
|
|
17
17
|
from qt_material import QtStyleTools
|
|
18
18
|
|
|
@@ -37,6 +37,7 @@ class MainWindow(QMainWindow, QtStyleTools):
|
|
|
37
37
|
stateChanged = Signal(str)
|
|
38
38
|
logger_message = Signal(object)
|
|
39
39
|
idx_logger_message = Signal(object)
|
|
40
|
+
appReady = Signal() # emitted after the first paint to indicate the window is ready on screen
|
|
40
41
|
|
|
41
42
|
def __init__(self, app: QApplication, args: dict = None):
|
|
42
43
|
"""
|
|
@@ -61,6 +62,9 @@ class MainWindow(QMainWindow, QtStyleTools):
|
|
|
61
62
|
self.prevState = None
|
|
62
63
|
self.is_post_update = False
|
|
63
64
|
|
|
65
|
+
# app ready emission control
|
|
66
|
+
self._app_ready_emitted = False # ensures single-shot emission
|
|
67
|
+
|
|
64
68
|
# load version info
|
|
65
69
|
self.meta = get_app_meta()
|
|
66
70
|
|
|
@@ -92,6 +96,7 @@ class MainWindow(QMainWindow, QtStyleTools):
|
|
|
92
96
|
|
|
93
97
|
# global shortcuts
|
|
94
98
|
self.shortcuts = []
|
|
99
|
+
self._esc_shortcut = None # keep a direct handle to temporarily disable during rerouting
|
|
95
100
|
|
|
96
101
|
# setup signals
|
|
97
102
|
self.statusChanged.connect(self.update_status)
|
|
@@ -231,6 +236,18 @@ class MainWindow(QMainWindow, QtStyleTools):
|
|
|
231
236
|
super().showEvent(e)
|
|
232
237
|
QTimer.singleShot(0, self.ui.on_show)
|
|
233
238
|
|
|
239
|
+
def paintEvent(self, e):
|
|
240
|
+
"""
|
|
241
|
+
On the first paint, announce that the window is actually visible and ready.
|
|
242
|
+
This is used to synchronize closing the external splash screen.
|
|
243
|
+
"""
|
|
244
|
+
super().paintEvent(e)
|
|
245
|
+
if not self._app_ready_emitted:
|
|
246
|
+
self._app_ready_emitted = True
|
|
247
|
+
QTimer.singleShot(0, self.appReady.emit)
|
|
248
|
+
# set focus to main window after shown
|
|
249
|
+
QTimer.singleShot(0, self.activateWindow)
|
|
250
|
+
|
|
234
251
|
def update(self):
|
|
235
252
|
"""Called on every update (real-time)"""
|
|
236
253
|
# self.controller.on_update()
|
|
@@ -404,6 +421,67 @@ class MainWindow(QMainWindow, QtStyleTools):
|
|
|
404
421
|
self.activateWindow()
|
|
405
422
|
self.ui.tray_menu['restore'].setVisible(False)
|
|
406
423
|
|
|
424
|
+
# ----- Global ESC routing that preserves widget-level ESC handling -----
|
|
425
|
+
|
|
426
|
+
def _deliver_escape_to(self, target) -> bool:
|
|
427
|
+
"""
|
|
428
|
+
Synthesize ESC keypress to the focused/popup widget so it can run its own close logic.
|
|
429
|
+
Temporarily disables the global ESC shortcut to avoid re-triggering itself.
|
|
430
|
+
"""
|
|
431
|
+
if target is None or not target.isVisible():
|
|
432
|
+
return False
|
|
433
|
+
try:
|
|
434
|
+
if self._esc_shortcut is not None:
|
|
435
|
+
self._esc_shortcut.setEnabled(False)
|
|
436
|
+
press = QKeyEvent(QEvent.KeyPress, Qt.Key_Escape, Qt.NoModifier)
|
|
437
|
+
release = QKeyEvent(QEvent.KeyRelease, Qt.Key_Escape, Qt.NoModifier)
|
|
438
|
+
QApplication.sendEvent(target, press)
|
|
439
|
+
QApplication.sendEvent(target, release)
|
|
440
|
+
except Exception:
|
|
441
|
+
pass
|
|
442
|
+
finally:
|
|
443
|
+
if self._esc_shortcut is not None:
|
|
444
|
+
QTimer.singleShot(0, lambda: self._esc_shortcut.setEnabled(True))
|
|
445
|
+
return True
|
|
446
|
+
|
|
447
|
+
def _route_escape_to_focus_or_popup(self) -> bool:
|
|
448
|
+
"""
|
|
449
|
+
Route ESC to the widget that currently owns focus (prefer) or active popup widget.
|
|
450
|
+
Returns True when ESC was delivered to a target.
|
|
451
|
+
"""
|
|
452
|
+
popup = QApplication.activePopupWidget()
|
|
453
|
+
if popup is not None and popup.isVisible():
|
|
454
|
+
try:
|
|
455
|
+
fw = QApplication.focusWidget()
|
|
456
|
+
if self._deliver_escape_to(fw):
|
|
457
|
+
return True
|
|
458
|
+
except Exception:
|
|
459
|
+
pass
|
|
460
|
+
return True
|
|
461
|
+
|
|
462
|
+
modal = QApplication.activeModalWidget()
|
|
463
|
+
if modal is not None and modal.isVisible():
|
|
464
|
+
try:
|
|
465
|
+
modal.close()
|
|
466
|
+
except Exception:
|
|
467
|
+
pass
|
|
468
|
+
return True
|
|
469
|
+
|
|
470
|
+
# No popup or modal to close
|
|
471
|
+
return False
|
|
472
|
+
|
|
473
|
+
def _on_escape_shortcut(self):
|
|
474
|
+
"""
|
|
475
|
+
Global ESC: deliver ESC to the focused/popup widget first so it can handle and cleanup correctly.
|
|
476
|
+
If nothing handles it, run the app-level escape handler.
|
|
477
|
+
"""
|
|
478
|
+
if self._route_escape_to_focus_or_popup():
|
|
479
|
+
return
|
|
480
|
+
try:
|
|
481
|
+
self.controller.access.on_escape()
|
|
482
|
+
except Exception:
|
|
483
|
+
pass
|
|
484
|
+
|
|
407
485
|
def setup_global_shortcuts(self):
|
|
408
486
|
"""Setup global shortcuts"""
|
|
409
487
|
if not hasattr(self, 'core') or not hasattr(self.core, 'config'):
|
|
@@ -417,12 +495,15 @@ class MainWindow(QMainWindow, QtStyleTools):
|
|
|
417
495
|
self.shortcuts.clear()
|
|
418
496
|
else:
|
|
419
497
|
self.shortcuts = []
|
|
498
|
+
self._esc_shortcut = None
|
|
420
499
|
|
|
421
500
|
# Handle the Escape key
|
|
422
501
|
escape_shortcut = QShortcut(QKeySequence(Qt.Key_Escape), self)
|
|
423
502
|
escape_shortcut.setContext(Qt.ApplicationShortcut)
|
|
424
|
-
escape_shortcut.
|
|
503
|
+
# escape_shortcut.setAutoRepeat(False) # avoid spamming when holding the key
|
|
504
|
+
escape_shortcut.activated.connect(self._on_escape_shortcut)
|
|
425
505
|
self.shortcuts.append(escape_shortcut)
|
|
506
|
+
self._esc_shortcut = escape_shortcut
|
|
426
507
|
|
|
427
508
|
config = self.core.config.get("access.shortcuts")
|
|
428
509
|
if config is None:
|
|
@@ -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.31 14:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtWidgets import QApplication
|
|
@@ -75,9 +75,8 @@ class BaseDialog(QDialog):
|
|
|
75
75
|
"size": [self.size().width(), self.size().height()],
|
|
76
76
|
"pos": [self.pos().x(), self.pos().y()]
|
|
77
77
|
}
|
|
78
|
+
data = {}
|
|
78
79
|
if self.store_geometry_enabled():
|
|
79
|
-
data = config.get("layout.dialog.geometry", {})
|
|
80
|
-
else:
|
|
81
80
|
data = config.get_session("layout.dialog.geometry", {})
|
|
82
81
|
|
|
83
82
|
if not isinstance(data, dict):
|
|
@@ -85,8 +84,6 @@ class BaseDialog(QDialog):
|
|
|
85
84
|
data[self.id] = item
|
|
86
85
|
|
|
87
86
|
if self.store_geometry_enabled():
|
|
88
|
-
config.set("layout.dialog.geometry", data)
|
|
89
|
-
else:
|
|
90
87
|
config.set_session("layout.dialog.geometry", data)
|
|
91
88
|
|
|
92
89
|
def restore_geometry(self):
|
|
@@ -96,14 +93,10 @@ class BaseDialog(QDialog):
|
|
|
96
93
|
available_geometry = screen.availableGeometry()
|
|
97
94
|
config = self.window.core.config
|
|
98
95
|
|
|
96
|
+
data = {}
|
|
99
97
|
if self.store_geometry_enabled():
|
|
100
|
-
data = config.get("layout.dialog.geometry", {})
|
|
101
|
-
else:
|
|
102
98
|
data = config.get_session("layout.dialog.geometry", {})
|
|
103
99
|
|
|
104
|
-
if not isinstance(data, dict):
|
|
105
|
-
data = {}
|
|
106
|
-
|
|
107
100
|
item = data.get(self.id, {})
|
|
108
101
|
if isinstance(item, dict) and "size" in item and "pos" in item:
|
|
109
102
|
width, height = item["size"]
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
9
|
# Updated Date: 2025.12.28 00: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,8 @@ 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()
|
|
725
743
|
event.accept()
|
|
726
744
|
return
|
|
727
745
|
super().keyPressEvent(event)
|
|
@@ -737,6 +755,53 @@ class SearchableCombo(SeparatorComboBox):
|
|
|
737
755
|
- Keep popup horizontally inside the parent window while resizing/moving.
|
|
738
756
|
"""
|
|
739
757
|
if obj is self._popup_container and self._popup_container is not None:
|
|
758
|
+
# Open target combo immediately when user clicks outside current popup
|
|
759
|
+
if event.type() == QEvent.MouseButtonPress:
|
|
760
|
+
try:
|
|
761
|
+
# Detect left-click outside popup window
|
|
762
|
+
left_btn = (getattr(event, "button", lambda: None)() == Qt.LeftButton)
|
|
763
|
+
pos_local = getattr(event, "position", lambda: None)()
|
|
764
|
+
inside = self._popup_container.rect().contains(pos_local.toPoint()) if pos_local is not None else True
|
|
765
|
+
if left_btn and not inside:
|
|
766
|
+
# Find widget under cursor and see if it's a different combo
|
|
767
|
+
gp = getattr(event, "globalPosition", None)
|
|
768
|
+
gp_pt = gp().toPoint() if callable(gp) else getattr(event, "globalPos", lambda: None)()
|
|
769
|
+
if gp_pt is None:
|
|
770
|
+
gp_pt = QApplication.instance().cursor().pos() if QApplication.instance() else None
|
|
771
|
+
target = QApplication.widgetAt(gp_pt) if gp_pt is not None else None
|
|
772
|
+
target_combo = self._ascend_to_combo(target)
|
|
773
|
+
if target_combo is self:
|
|
774
|
+
# Clicked back on the owner combo: arm one-shot guard to suppress reopen
|
|
775
|
+
self._prevent_reopen_once = True
|
|
776
|
+
elif isinstance(target_combo, SearchableCombo) and target_combo.isEnabled():
|
|
777
|
+
def _open_other():
|
|
778
|
+
if target_combo and target_combo.isEnabled():
|
|
779
|
+
try:
|
|
780
|
+
target_combo.setFocus(Qt.MouseFocusReason)
|
|
781
|
+
except Exception:
|
|
782
|
+
pass
|
|
783
|
+
target_combo.showPopup()
|
|
784
|
+
if sys.platform != "win32":
|
|
785
|
+
QTimer.singleShot(0, _open_other)
|
|
786
|
+
else:
|
|
787
|
+
container = self._popup_container
|
|
788
|
+
opened_on_destroy = False
|
|
789
|
+
try:
|
|
790
|
+
if container is not None:
|
|
791
|
+
container.destroyed.connect(lambda *_: QTimer.singleShot(0, _open_other))
|
|
792
|
+
opened_on_destroy = True
|
|
793
|
+
except Exception:
|
|
794
|
+
pass
|
|
795
|
+
if not opened_on_destroy:
|
|
796
|
+
QTimer.singleShot(50, _open_other)
|
|
797
|
+
try:
|
|
798
|
+
self.hidePopup()
|
|
799
|
+
except Exception:
|
|
800
|
+
pass
|
|
801
|
+
except Exception:
|
|
802
|
+
pass
|
|
803
|
+
return False
|
|
804
|
+
|
|
740
805
|
if event.type() in (QEvent.Resize, QEvent.Show):
|
|
741
806
|
self._place_popup_header()
|
|
742
807
|
# Also ensure fitting after container geometry changes
|
|
@@ -753,6 +818,8 @@ class SearchableCombo(SeparatorComboBox):
|
|
|
753
818
|
if event.type() == QEvent.KeyPress:
|
|
754
819
|
key = event.key()
|
|
755
820
|
if key == Qt.Key_Escape:
|
|
821
|
+
# Explicitly close popup from header
|
|
822
|
+
self.hidePopup()
|
|
756
823
|
return True
|
|
757
824
|
if key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown, Qt.Key_Home, Qt.Key_End):
|
|
758
825
|
self._handle_navigation_key(key)
|
|
@@ -769,11 +836,24 @@ class SearchableCombo(SeparatorComboBox):
|
|
|
769
836
|
self._commit_view_current()
|
|
770
837
|
return True
|
|
771
838
|
if event.key() == Qt.Key_Escape:
|
|
839
|
+
# Close from view/viewport ESC and keep control flow consistent
|
|
840
|
+
self.hidePopup()
|
|
772
841
|
return True
|
|
773
842
|
return False
|
|
774
843
|
|
|
775
844
|
return super().eventFilter(obj, event)
|
|
776
845
|
|
|
846
|
+
# Helpers to walk up to owning combo under a point
|
|
847
|
+
def _ascend_to_combo(self, widget):
|
|
848
|
+
"""Ascend from a widget to the nearest SearchableCombo owner if present."""
|
|
849
|
+
w = widget
|
|
850
|
+
while w is not None and not isinstance(w, SearchableCombo):
|
|
851
|
+
try:
|
|
852
|
+
w = w.parentWidget()
|
|
853
|
+
except Exception:
|
|
854
|
+
return None
|
|
855
|
+
return w
|
|
856
|
+
|
|
777
857
|
# ----- Magnifier helpers -----
|
|
778
858
|
|
|
779
859
|
def _ensure_magnifier_on(self, line_edit: QLineEdit | None):
|
|
@@ -938,6 +1018,44 @@ class SearchableCombo(SeparatorComboBox):
|
|
|
938
1018
|
self._popup_header.setCursorPosition(len(self._popup_header.text()))
|
|
939
1019
|
self._ensure_clear_button_visible(self._popup_header)
|
|
940
1020
|
|
|
1021
|
+
# ----- Minimal ensure after show (mainly for Windows combo-to-combo switch) -----
|
|
1022
|
+
|
|
1023
|
+
def _ensure_header_after_show(self):
|
|
1024
|
+
"""
|
|
1025
|
+
Re-apply critical bits after the popup is up:
|
|
1026
|
+
- header exists and is parented to the popup container,
|
|
1027
|
+
- viewport top margin equals header height,
|
|
1028
|
+
- header is placed and focused.
|
|
1029
|
+
"""
|
|
1030
|
+
if not self._popup_open or not self.search:
|
|
1031
|
+
return
|
|
1032
|
+
view = self.view()
|
|
1033
|
+
if view is None:
|
|
1034
|
+
return
|
|
1035
|
+
container = self._popup_container or view.window()
|
|
1036
|
+
if container is None:
|
|
1037
|
+
return
|
|
1038
|
+
|
|
1039
|
+
if self._popup_header is None:
|
|
1040
|
+
self._prepare_popup_header()
|
|
1041
|
+
return
|
|
1042
|
+
|
|
1043
|
+
try:
|
|
1044
|
+
if self._popup_header.parent() is not container:
|
|
1045
|
+
self._popup_header.setParent(container)
|
|
1046
|
+
except Exception:
|
|
1047
|
+
pass
|
|
1048
|
+
|
|
1049
|
+
try:
|
|
1050
|
+
view.setViewportMargins(0, self._popup_header_h, 0, 0)
|
|
1051
|
+
except Exception:
|
|
1052
|
+
pass
|
|
1053
|
+
|
|
1054
|
+
self._place_popup_header()
|
|
1055
|
+
self._ensure_magnifier_on(self._popup_header)
|
|
1056
|
+
self._ensure_clear_button_visible(self._popup_header)
|
|
1057
|
+
self._focus_header_async()
|
|
1058
|
+
|
|
941
1059
|
# ----- Search behaviour -----
|
|
942
1060
|
|
|
943
1061
|
def _on_search_text_changed(self, text: str):
|