pygpt-net 2.6.60__py3-none-any.whl → 2.6.62__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 +14 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/controller/chat/common.py +115 -6
- pygpt_net/controller/chat/input.py +4 -1
- pygpt_net/controller/chat/response.py +8 -2
- pygpt_net/controller/presets/presets.py +121 -6
- pygpt_net/controller/settings/editor.py +0 -15
- pygpt_net/controller/settings/profile.py +16 -4
- pygpt_net/controller/settings/workdir.py +30 -5
- pygpt_net/controller/theme/common.py +4 -2
- pygpt_net/controller/theme/markdown.py +4 -7
- pygpt_net/controller/theme/theme.py +2 -1
- pygpt_net/controller/ui/ui.py +32 -7
- pygpt_net/core/agents/custom/__init__.py +7 -1
- pygpt_net/core/agents/custom/llama_index/factory.py +17 -6
- pygpt_net/core/agents/custom/llama_index/runner.py +52 -4
- pygpt_net/core/agents/custom/llama_index/utils.py +12 -1
- pygpt_net/core/agents/custom/router.py +45 -6
- pygpt_net/core/agents/custom/runner.py +11 -5
- pygpt_net/core/agents/custom/schema.py +3 -1
- pygpt_net/core/agents/custom/utils.py +13 -1
- pygpt_net/core/agents/runners/llama_workflow.py +65 -5
- pygpt_net/core/agents/runners/openai_workflow.py +2 -1
- pygpt_net/core/db/viewer.py +11 -5
- pygpt_net/core/node_editor/graph.py +18 -9
- pygpt_net/core/node_editor/models.py +9 -2
- pygpt_net/core/node_editor/types.py +15 -1
- pygpt_net/core/presets/presets.py +216 -29
- pygpt_net/core/render/markdown/parser.py +0 -2
- pygpt_net/core/render/web/renderer.py +76 -11
- pygpt_net/data/config/config.json +5 -6
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/config/settings.json +2 -38
- pygpt_net/data/css/style.dark.css +18 -0
- pygpt_net/data/css/style.light.css +20 -1
- pygpt_net/data/locale/locale.de.ini +66 -1
- pygpt_net/data/locale/locale.en.ini +64 -3
- pygpt_net/data/locale/locale.es.ini +66 -1
- pygpt_net/data/locale/locale.fr.ini +66 -1
- pygpt_net/data/locale/locale.it.ini +66 -1
- pygpt_net/data/locale/locale.pl.ini +67 -2
- pygpt_net/data/locale/locale.uk.ini +66 -1
- pygpt_net/data/locale/locale.zh.ini +66 -1
- pygpt_net/data/locale/plugin.cmd_system.en.ini +62 -66
- pygpt_net/item/ctx.py +23 -1
- pygpt_net/provider/agents/llama_index/flow_from_schema.py +2 -2
- pygpt_net/provider/agents/llama_index/workflow/codeact.py +9 -6
- pygpt_net/provider/agents/llama_index/workflow/openai.py +38 -11
- pygpt_net/provider/agents/llama_index/workflow/planner.py +36 -16
- pygpt_net/provider/agents/llama_index/workflow/supervisor.py +60 -10
- pygpt_net/provider/agents/openai/agent.py +3 -1
- pygpt_net/provider/agents/openai/agent_b2b.py +13 -9
- pygpt_net/provider/agents/openai/agent_planner.py +6 -2
- pygpt_net/provider/agents/openai/agent_with_experts.py +4 -1
- pygpt_net/provider/agents/openai/agent_with_experts_feedback.py +4 -2
- pygpt_net/provider/agents/openai/agent_with_feedback.py +4 -2
- pygpt_net/provider/agents/openai/evolve.py +6 -2
- pygpt_net/provider/agents/openai/supervisor.py +3 -1
- pygpt_net/provider/api/openai/agents/response.py +1 -0
- pygpt_net/provider/core/config/patch.py +18 -1
- pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +0 -6
- pygpt_net/tools/agent_builder/tool.py +48 -26
- pygpt_net/tools/agent_builder/ui/dialogs.py +36 -28
- pygpt_net/ui/__init__.py +2 -4
- pygpt_net/ui/dialog/about.py +58 -38
- pygpt_net/ui/dialog/db.py +142 -3
- pygpt_net/ui/dialog/preset.py +47 -8
- pygpt_net/ui/layout/toolbox/presets.py +64 -16
- pygpt_net/ui/main.py +2 -2
- pygpt_net/ui/widget/dialog/confirm.py +27 -3
- pygpt_net/ui/widget/dialog/db.py +0 -0
- pygpt_net/ui/widget/draw/painter.py +90 -1
- pygpt_net/ui/widget/lists/preset.py +908 -60
- pygpt_net/ui/widget/node_editor/command.py +10 -10
- pygpt_net/ui/widget/node_editor/config.py +157 -0
- pygpt_net/ui/widget/node_editor/editor.py +223 -153
- pygpt_net/ui/widget/node_editor/item.py +12 -11
- pygpt_net/ui/widget/node_editor/node.py +246 -13
- pygpt_net/ui/widget/node_editor/view.py +179 -63
- pygpt_net/ui/widget/tabs/output.py +1 -1
- pygpt_net/ui/widget/textarea/input.py +157 -23
- pygpt_net/utils.py +114 -2
- {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.62.dist-info}/METADATA +26 -100
- {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.62.dist-info}/RECORD +86 -85
- {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.62.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.62.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.62.dist-info}/entry_points.txt +0 -0
pygpt_net/ui/dialog/db.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.09.
|
|
9
|
+
# Updated Date: 2025.09.26 03:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
|
-
from PySide6.QtCore import Qt, QTimer, QSignalBlocker
|
|
12
|
+
from PySide6.QtCore import Qt, QTimer, QSignalBlocker, QObject, QEvent
|
|
13
13
|
from PySide6.QtGui import QAction, QIcon, QIntValidator
|
|
14
14
|
from PySide6.QtWidgets import QGridLayout, QScrollArea, QSplitter, QComboBox, QLineEdit, QPushButton, \
|
|
15
|
-
QHBoxLayout, QVBoxLayout, QLabel, QWidget, QSizePolicy, QCheckBox, QMenuBar, QAbstractItemView, QHeaderView, QStyledItemDelegate
|
|
15
|
+
QHBoxLayout, QVBoxLayout, QLabel, QWidget, QSizePolicy, QCheckBox, QMenuBar, QAbstractItemView, QHeaderView, QStyledItemDelegate, QMenu, QApplication
|
|
16
16
|
|
|
17
17
|
from pygpt_net.ui.widget.dialog.db import DatabaseDialog
|
|
18
18
|
from pygpt_net.ui.widget.lists.db import DatabaseList, DatabaseTableModel
|
|
@@ -32,6 +32,98 @@ class _FastTextDelegate(QStyledItemDelegate):
|
|
|
32
32
|
return s
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
class _ContextMenuStyler(QObject):
|
|
36
|
+
"""
|
|
37
|
+
Ensures that any QMenu spawned from the observed widgets adopts the same
|
|
38
|
+
application stylesheet, palette and font (e.g. Qt Material), even if
|
|
39
|
+
the menu is created without a parent or uses platform defaults.
|
|
40
|
+
The filter is installed only on specific widgets (no app-wide filters).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, style_source: QWidget):
|
|
44
|
+
super().__init__(style_source)
|
|
45
|
+
self._source = style_source
|
|
46
|
+
self._scan_timer = QTimer(self)
|
|
47
|
+
self._scan_timer.setInterval(50) # fast enough to catch freshly spawned menus
|
|
48
|
+
self._scan_timer.timeout.connect(self._scan_and_style)
|
|
49
|
+
self._scan_ticks = 0
|
|
50
|
+
self._scan_ticks_max = 8 # up to ~400ms window
|
|
51
|
+
|
|
52
|
+
def eventFilter(self, obj, event):
|
|
53
|
+
# Trigger styling round right before/around menu show moments
|
|
54
|
+
if event.type() in (QEvent.ContextMenu, QEvent.MouseButtonPress, QEvent.KeyPress):
|
|
55
|
+
if event.type() == QEvent.ContextMenu:
|
|
56
|
+
self._start_scan()
|
|
57
|
+
elif event.type() == QEvent.MouseButtonPress and hasattr(event, "button") and event.button() == Qt.RightButton:
|
|
58
|
+
self._start_scan()
|
|
59
|
+
elif event.type() == QEvent.KeyPress:
|
|
60
|
+
key = getattr(event, "key", lambda: None)()
|
|
61
|
+
mods = getattr(event, "modifiers", lambda: Qt.NoModifier)()
|
|
62
|
+
if key == Qt.Key_Menu or (key == Qt.Key_F10 and (mods & Qt.ShiftModifier)):
|
|
63
|
+
self._start_scan()
|
|
64
|
+
return False # do not consume events
|
|
65
|
+
|
|
66
|
+
def _start_scan(self):
|
|
67
|
+
# Start a short, repeated scan to reliably catch the menu top-level window
|
|
68
|
+
self._scan_ticks = 0
|
|
69
|
+
if not self._scan_timer.isActive():
|
|
70
|
+
# immediate pass + scheduled ones
|
|
71
|
+
QTimer.singleShot(0, self._scan_and_style)
|
|
72
|
+
self._scan_timer.start()
|
|
73
|
+
|
|
74
|
+
def _scan_and_style(self):
|
|
75
|
+
self._scan_ticks += 1
|
|
76
|
+
app = QApplication.instance()
|
|
77
|
+
if app is None:
|
|
78
|
+
self._scan_timer.stop()
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
found_any = False
|
|
82
|
+
for w in app.topLevelWidgets():
|
|
83
|
+
if isinstance(w, QMenu):
|
|
84
|
+
found_any = True
|
|
85
|
+
self._ensure_menu_style(w)
|
|
86
|
+
|
|
87
|
+
# Stop once we scanned enough frames, or if no menus appear after a short while
|
|
88
|
+
if self._scan_ticks >= self._scan_ticks_max or (self._scan_ticks >= 2 and not found_any):
|
|
89
|
+
self._scan_timer.stop()
|
|
90
|
+
|
|
91
|
+
def _ensure_menu_style(self, menu: QMenu):
|
|
92
|
+
# Avoid re-styling the same widget repeatedly
|
|
93
|
+
if bool(menu.property("_pygpt_menu_styled")):
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
app = QApplication.instance()
|
|
97
|
+
try:
|
|
98
|
+
# Apply application-level stylesheet (covers Qt Material CSS selectors for QMenu)
|
|
99
|
+
if app is not None:
|
|
100
|
+
ss = app.styleSheet()
|
|
101
|
+
if ss:
|
|
102
|
+
# Assign directly to the menu; do not append, to avoid duplication on repeated shows
|
|
103
|
+
menu.setStyleSheet(ss)
|
|
104
|
+
|
|
105
|
+
# Copy palette, font and style from the source window to guarantee readable contrast
|
|
106
|
+
src = self._source.window() if self._source is not None else None
|
|
107
|
+
if src is None and app is not None:
|
|
108
|
+
src = app.activeWindow()
|
|
109
|
+
|
|
110
|
+
if src is not None:
|
|
111
|
+
menu.setPalette(src.palette())
|
|
112
|
+
menu.setFont(src.font())
|
|
113
|
+
# Also align QStyle to prevent platform defaults from clashing with stylesheet
|
|
114
|
+
menu.setStyle(src.style())
|
|
115
|
+
|
|
116
|
+
# Mark as processed
|
|
117
|
+
menu.setProperty("_pygpt_menu_styled", True)
|
|
118
|
+
|
|
119
|
+
# Update to reflect changes immediately if the menu is already visible
|
|
120
|
+
if menu.isVisible():
|
|
121
|
+
menu.update()
|
|
122
|
+
except Exception:
|
|
123
|
+
# Fail-safe: never break the context menu on styling issues
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
|
|
35
127
|
class Database:
|
|
36
128
|
def __init__(self, window=None):
|
|
37
129
|
"""
|
|
@@ -94,6 +186,9 @@ class Database:
|
|
|
94
186
|
self.batch_actions_menu.addAction(self.delete_all_action)
|
|
95
187
|
self.batch_actions_menu.addAction(self.truncate_action)
|
|
96
188
|
|
|
189
|
+
# Ensure QMenu used by menu bar follows the application/theme styling
|
|
190
|
+
self._apply_menu_style(self.batch_actions_menu)
|
|
191
|
+
|
|
97
192
|
layout = QGridLayout()
|
|
98
193
|
layout.addWidget(splitter, 1, 0)
|
|
99
194
|
layout.setMenuBar(self.menu_bar)
|
|
@@ -102,6 +197,22 @@ class Database:
|
|
|
102
197
|
self.window.ui.dialog['debug.db'].setLayout(layout)
|
|
103
198
|
self.window.ui.dialog['debug.db'].setWindowTitle("Debug: Database (SQLite)")
|
|
104
199
|
|
|
200
|
+
def _apply_menu_style(self, menu: QMenu):
|
|
201
|
+
"""Ensure QMenu follows application/theme styling consistently."""
|
|
202
|
+
try:
|
|
203
|
+
app = QApplication.instance()
|
|
204
|
+
if app is not None:
|
|
205
|
+
ss = app.styleSheet()
|
|
206
|
+
if ss:
|
|
207
|
+
menu.setStyleSheet(ss)
|
|
208
|
+
if self.window is not None:
|
|
209
|
+
menu.setStyle(self.window.style())
|
|
210
|
+
menu.setPalette(self.window.palette())
|
|
211
|
+
menu.setFont(self.window.font())
|
|
212
|
+
except Exception:
|
|
213
|
+
# Never break UI if styling fails
|
|
214
|
+
pass
|
|
215
|
+
|
|
105
216
|
def _on_splitter_moved(self, pos, index):
|
|
106
217
|
if self._text_viewer is not None:
|
|
107
218
|
self._text_viewer.setUpdatesEnabled(False)
|
|
@@ -267,6 +378,34 @@ class DataBrowser(QWidget):
|
|
|
267
378
|
self.setLayout(main_layout)
|
|
268
379
|
|
|
269
380
|
view = self.get_list_widget()
|
|
381
|
+
|
|
382
|
+
# Robust, local-only context menu styling (no app-wide filters)
|
|
383
|
+
self._menu_styler = _ContextMenuStyler(self.window if self.window is not None else self)
|
|
384
|
+
if hasattr(view, "installEventFilter"):
|
|
385
|
+
view.installEventFilter(self._menu_styler)
|
|
386
|
+
# Context menu usually originates from the viewport in item views
|
|
387
|
+
if hasattr(view, "viewport") and callable(view.viewport):
|
|
388
|
+
vp = view.viewport()
|
|
389
|
+
if vp is not None and hasattr(vp, "installEventFilter"):
|
|
390
|
+
vp.installEventFilter(self._menu_styler)
|
|
391
|
+
# Also watch headers (they may expose their own context menus)
|
|
392
|
+
if hasattr(view, "horizontalHeader"):
|
|
393
|
+
hh = view.horizontalHeader()
|
|
394
|
+
if hh is not None:
|
|
395
|
+
hh.installEventFilter(self._menu_styler)
|
|
396
|
+
if hasattr(hh, "viewport"):
|
|
397
|
+
hvp = hh.viewport()
|
|
398
|
+
if hvp is not None:
|
|
399
|
+
hvp.installEventFilter(self._menu_styler)
|
|
400
|
+
if hasattr(view, "verticalHeader"):
|
|
401
|
+
vh = view.verticalHeader()
|
|
402
|
+
if vh is not None:
|
|
403
|
+
vh.installEventFilter(self._menu_styler)
|
|
404
|
+
if hasattr(vh, "viewport"):
|
|
405
|
+
vvp = vh.viewport()
|
|
406
|
+
if vvp is not None:
|
|
407
|
+
vvp.installEventFilter(self._menu_styler)
|
|
408
|
+
|
|
270
409
|
if hasattr(view, "setUniformRowHeights"):
|
|
271
410
|
view.setUniformRowHeights(True)
|
|
272
411
|
if hasattr(view, "setWordWrap"):
|
pygpt_net/ui/dialog/preset.py
CHANGED
|
@@ -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.09.25 13:04:51 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtCore import Qt
|
|
@@ -156,6 +156,8 @@ class Preset(BaseConfigDialog):
|
|
|
156
156
|
modes = QWidget()
|
|
157
157
|
modes.setLayout(rows_mode)
|
|
158
158
|
modes.setContentsMargins(0, 0, 0, 0)
|
|
159
|
+
# Ensure modes column never grows taller than its content; avoids creating vertical gap above the splitter.
|
|
160
|
+
modes.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)
|
|
159
161
|
self.window.ui.nodes['preset.editor.modes'] = modes
|
|
160
162
|
|
|
161
163
|
# experts
|
|
@@ -208,7 +210,7 @@ class Preset(BaseConfigDialog):
|
|
|
208
210
|
# special case with settings icon on right
|
|
209
211
|
node_layout = QHBoxLayout()
|
|
210
212
|
builder_btn = QPushButton(QIcon(":/icons/robot.svg"), "")
|
|
211
|
-
builder_btn.setToolTip(
|
|
213
|
+
builder_btn.setToolTip(trans("agent.builder.tooltip"))
|
|
212
214
|
builder_btn.clicked.connect(lambda: self.window.tools.get("agent_builder").toggle())
|
|
213
215
|
node_layout.setContentsMargins(0, 0, 0, 0)
|
|
214
216
|
node_layout.addLayout(options[key])
|
|
@@ -265,6 +267,8 @@ class Preset(BaseConfigDialog):
|
|
|
265
267
|
widget_base = QWidget()
|
|
266
268
|
widget_base.setLayout(rows)
|
|
267
269
|
widget_base.setMinimumWidth(300)
|
|
270
|
+
# Keep base column at content height; do not stretch vertically.
|
|
271
|
+
widget_base.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)
|
|
268
272
|
|
|
269
273
|
self.window.ui.nodes['preset.editor.experts'].setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
270
274
|
|
|
@@ -274,12 +278,16 @@ class Preset(BaseConfigDialog):
|
|
|
274
278
|
|
|
275
279
|
widget_main = QWidget()
|
|
276
280
|
widget_main.setLayout(main)
|
|
281
|
+
# Critical: ensure the whole top pane (base + modes) stays at content height.
|
|
282
|
+
widget_main.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum)
|
|
277
283
|
|
|
278
284
|
splitter = QSplitter(Qt.Vertical)
|
|
279
285
|
splitter.addWidget(widget_main)
|
|
280
286
|
splitter.addWidget(widget_prompt)
|
|
281
|
-
splitter.
|
|
282
|
-
|
|
287
|
+
splitter.setChildrenCollapsible(False) # avoid accidental collapsing of any pane
|
|
288
|
+
# All extra vertical space goes to bottom (extra agent options); top stays at its sizeHint.
|
|
289
|
+
splitter.setStretchFactor(0, 0)
|
|
290
|
+
splitter.setStretchFactor(1, 1)
|
|
283
291
|
self.window.ui.splitters['editor.presets'] = splitter
|
|
284
292
|
|
|
285
293
|
widget_personalize = QWidget()
|
|
@@ -398,13 +406,17 @@ class AvatarWidget(QWidget):
|
|
|
398
406
|
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
399
407
|
|
|
400
408
|
current_avatar_label = QLabel(trans("preset.personalize.avatar.current"))
|
|
409
|
+
# Center the section title for better visual balance.
|
|
410
|
+
current_avatar_label.setAlignment(Qt.AlignHCenter)
|
|
401
411
|
main_layout.addWidget(current_avatar_label)
|
|
402
412
|
|
|
403
413
|
self.avatar_preview = QLabel(self)
|
|
404
414
|
self.avatar_preview.setFixedSize(200, 200)
|
|
405
|
-
|
|
415
|
+
# Keep a visible border, but round it to visually indicate the circular avatar.
|
|
416
|
+
self.avatar_preview.setStyleSheet("border: 1px solid gray; border-radius: 100px; background: transparent;")
|
|
406
417
|
self.avatar_preview.setAlignment(Qt.AlignCenter)
|
|
407
|
-
|
|
418
|
+
# Center the avatar widget within the column.
|
|
419
|
+
main_layout.addWidget(self.avatar_preview, 0, Qt.AlignHCenter)
|
|
408
420
|
|
|
409
421
|
buttons_layout = QHBoxLayout()
|
|
410
422
|
|
|
@@ -417,6 +429,8 @@ class AvatarWidget(QWidget):
|
|
|
417
429
|
self.remove_button.setEnabled(False)
|
|
418
430
|
buttons_layout.addWidget(self.remove_button)
|
|
419
431
|
buttons_layout.setContentsMargins(0, 10, 0, 0)
|
|
432
|
+
# Center the action buttons below the avatar.
|
|
433
|
+
buttons_layout.setAlignment(Qt.AlignCenter)
|
|
420
434
|
|
|
421
435
|
main_layout.addLayout(buttons_layout)
|
|
422
436
|
main_layout.setContentsMargins(0, 10, 0, 0)
|
|
@@ -440,7 +454,9 @@ class AvatarWidget(QWidget):
|
|
|
440
454
|
pixmap = QPixmap(file_path)
|
|
441
455
|
if not pixmap.isNull():
|
|
442
456
|
cover_pix = self.get_cover_pixmap(pixmap, self.avatar_preview.width(), self.avatar_preview.height())
|
|
443
|
-
|
|
457
|
+
# Render the avatar as a circular pixmap.
|
|
458
|
+
circle_pix = self.get_circular_pixmap(cover_pix, self.avatar_preview.width(), self.avatar_preview.height())
|
|
459
|
+
self.avatar_preview.setPixmap(circle_pix)
|
|
444
460
|
self.remove_button.setEnabled(True)
|
|
445
461
|
|
|
446
462
|
def enable_remove_button(self, enabled: bool = True):
|
|
@@ -472,7 +488,30 @@ class AvatarWidget(QWidget):
|
|
|
472
488
|
y = (scaled_pix.height() - target_height) // 2
|
|
473
489
|
return scaled_pix.copy(x, y, target_width, target_height)
|
|
474
490
|
|
|
491
|
+
def get_circular_pixmap(self, pixmap, target_width: int, target_height: int):
|
|
492
|
+
"""
|
|
493
|
+
Create a circular version of the given pixmap using an antialiased clip path.
|
|
494
|
+
|
|
495
|
+
:param pixmap: Source pixmap already scaled/cropped to target size
|
|
496
|
+
:param target_width: Target width
|
|
497
|
+
:param target_height: Target height
|
|
498
|
+
:return: Circular masked pixmap with transparent corners
|
|
499
|
+
"""
|
|
500
|
+
from PySide6.QtGui import QPainter, QPainterPath, QPixmap # local import to avoid altering global imports
|
|
501
|
+
result = QPixmap(target_width, target_height)
|
|
502
|
+
result.fill(Qt.transparent)
|
|
503
|
+
|
|
504
|
+
painter = QPainter(result)
|
|
505
|
+
painter.setRenderHint(QPainter.Antialiasing, True)
|
|
506
|
+
path = QPainterPath()
|
|
507
|
+
path.addEllipse(0, 0, target_width, target_height)
|
|
508
|
+
painter.setClipPath(path)
|
|
509
|
+
painter.drawPixmap(0, 0, pixmap)
|
|
510
|
+
painter.end()
|
|
511
|
+
|
|
512
|
+
return result
|
|
513
|
+
|
|
475
514
|
def remove_avatar(self):
|
|
476
515
|
"""Remove the current avatar image."""
|
|
477
516
|
self.avatar_preview.clear()
|
|
478
|
-
self.remove_button.setEnabled(False)
|
|
517
|
+
self.remove_button.setEnabled(False)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# ui/layout/presets.py
|
|
1
2
|
#!/usr/bin/env python3
|
|
2
3
|
# -*- coding: utf-8 -*-
|
|
3
4
|
# ================================================== #
|
|
@@ -6,11 +7,11 @@
|
|
|
6
7
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
8
|
# MIT License #
|
|
8
9
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2025.09.
|
|
10
|
+
# Updated Date: 2025.09.26 13:30:00 #
|
|
10
11
|
# ================================================== #
|
|
11
12
|
|
|
12
13
|
from PySide6 import QtCore
|
|
13
|
-
from PySide6.QtGui import QStandardItemModel, Qt, QIcon
|
|
14
|
+
from PySide6.QtGui import QStandardItemModel, QStandardItem, Qt, QIcon
|
|
14
15
|
from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QPushButton, QWidget, QSizePolicy
|
|
15
16
|
|
|
16
17
|
from pygpt_net.core.types import (
|
|
@@ -113,35 +114,52 @@ class Presets:
|
|
|
113
114
|
nodes = self.window.ui.nodes
|
|
114
115
|
models = self.window.ui.models
|
|
115
116
|
|
|
116
|
-
view = nodes[self.id]
|
|
117
|
+
view: PresetList = nodes[self.id]
|
|
117
118
|
model = models.get(self.id)
|
|
118
119
|
|
|
119
|
-
view
|
|
120
|
+
# If view requested selection override, do NOT override it by backup
|
|
121
|
+
selection_override_ids = getattr(view, "_selection_override_ids", None)
|
|
122
|
+
if not selection_override_ids:
|
|
123
|
+
view.backup_selection()
|
|
120
124
|
|
|
121
125
|
if model is None:
|
|
122
126
|
model = self.create_model(self.window)
|
|
123
127
|
models[self.id] = model
|
|
124
128
|
view.setModel(model)
|
|
125
129
|
|
|
130
|
+
# Preserve current scroll position across model rebuild to avoid a visible jump to the top.
|
|
131
|
+
# This is applied while updates are disabled, then restored just before re-enabling them.
|
|
132
|
+
try:
|
|
133
|
+
v = view.verticalScrollBar().value()
|
|
134
|
+
h = view.horizontalScrollBar().value()
|
|
135
|
+
view.set_pending_v_scroll(v)
|
|
136
|
+
view.set_pending_h_scroll(h)
|
|
137
|
+
except Exception:
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
# Block user input during model rebuild to avoid crashes on quick clicks
|
|
141
|
+
view.begin_model_update()
|
|
142
|
+
|
|
143
|
+
# Turn off updates to avoid flicker and transient artifacts
|
|
126
144
|
view.setUpdatesEnabled(False)
|
|
127
145
|
try:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
count = len(data)
|
|
132
|
-
model.setRowCount(count)
|
|
146
|
+
# Rebuild model cleanly to avoid any stale items causing visual glitches
|
|
147
|
+
model.clear()
|
|
148
|
+
model.setColumnCount(1)
|
|
133
149
|
|
|
150
|
+
if data:
|
|
134
151
|
is_expert_mode = (mode == MODE_EXPERT)
|
|
135
152
|
is_agent_mode = (mode == MODE_AGENT)
|
|
136
153
|
count_experts = self.window.core.experts.count_experts if is_agent_mode else None
|
|
137
154
|
startswith_current = "current."
|
|
138
155
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
tooltip_role = QtCore.Qt.ToolTipRole
|
|
156
|
+
role_uuid = QtCore.Qt.UserRole + 1
|
|
157
|
+
role_id = QtCore.Qt.UserRole + 2
|
|
158
|
+
role_is_special = QtCore.Qt.UserRole + 3
|
|
143
159
|
|
|
144
160
|
for i, (key, item) in enumerate(data.items()):
|
|
161
|
+
qitem = QStandardItem()
|
|
162
|
+
|
|
145
163
|
name = item.name
|
|
146
164
|
if is_expert_mode and item.enabled and not key.startswith(startswith_current):
|
|
147
165
|
name = f"[x] {name}"
|
|
@@ -153,9 +171,39 @@ class Presets:
|
|
|
153
171
|
prompt = str(item.prompt)
|
|
154
172
|
tooltip = prompt if len(prompt) <= 80 else f"{prompt[:80]}..."
|
|
155
173
|
|
|
156
|
-
|
|
157
|
-
|
|
174
|
+
qitem.setText(name)
|
|
175
|
+
qitem.setToolTip(tooltip)
|
|
176
|
+
qitem.setData(item.uuid, role_uuid)
|
|
177
|
+
qitem.setData(key, role_id)
|
|
178
|
+
qitem.setData(key.startswith(startswith_current), role_is_special)
|
|
179
|
+
|
|
180
|
+
# Pin row 0 (no drag, no drop)
|
|
181
|
+
# Other rows: drag enabled only; drop is handled by view between rows
|
|
182
|
+
if i != 0 and not key.startswith(startswith_current):
|
|
183
|
+
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled
|
|
184
|
+
else:
|
|
185
|
+
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
|
186
|
+
qitem.setFlags(flags)
|
|
187
|
+
|
|
188
|
+
model.appendRow(qitem)
|
|
158
189
|
finally:
|
|
190
|
+
# Apply pending scroll (if any) before re-enabling updates
|
|
191
|
+
view.apply_pending_scroll()
|
|
159
192
|
view.setUpdatesEnabled(True)
|
|
160
193
|
|
|
161
|
-
|
|
194
|
+
dnd_enabled = bool(self.window.core.config.get('presets.drag_and_drop.enabled'))
|
|
195
|
+
view.set_dnd_enabled(dnd_enabled)
|
|
196
|
+
|
|
197
|
+
# If override requested, force saved selection IDs to those override IDs
|
|
198
|
+
if selection_override_ids:
|
|
199
|
+
view._saved_selection_ids = list(selection_override_ids)
|
|
200
|
+
view._selection_override_ids = None # consume one-shot override
|
|
201
|
+
|
|
202
|
+
# Restore selection by ID (so it follows the same item even if rows moved)
|
|
203
|
+
view.restore_selection()
|
|
204
|
+
# Force repaint in case Qt defers layout until next input
|
|
205
|
+
view.viewport().update()
|
|
206
|
+
|
|
207
|
+
# Clear one-shot pending scroll values and re-enable user interaction
|
|
208
|
+
view.clear_pending_scroll()
|
|
209
|
+
view.end_model_update()
|
pygpt_net/ui/main.py
CHANGED
|
@@ -22,7 +22,7 @@ from pygpt_net.controller import Controller
|
|
|
22
22
|
from pygpt_net.tools import Tools
|
|
23
23
|
from pygpt_net.ui import UI
|
|
24
24
|
from pygpt_net.ui.widget.textarea.web import ChatWebOutput
|
|
25
|
-
from pygpt_net.utils import get_app_meta, freeze_updates, set_env, has_env, get_env
|
|
25
|
+
from pygpt_net.utils import get_app_meta, freeze_updates, set_env, has_env, get_env, trans
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
class MainWindow(QMainWindow, QtStyleTools):
|
|
@@ -356,7 +356,7 @@ class MainWindow(QMainWindow, QtStyleTools):
|
|
|
356
356
|
self.core.presets.save_all()
|
|
357
357
|
print("Exiting...")
|
|
358
358
|
print("")
|
|
359
|
-
print("
|
|
359
|
+
print(f"{trans('exit.msg')} https://pygpt.net/#donate")
|
|
360
360
|
|
|
361
361
|
def changeEvent(self, event):
|
|
362
362
|
"""
|
|
@@ -6,9 +6,10 @@
|
|
|
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: 2025.09.26 10:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
|
+
import sys
|
|
12
13
|
from PySide6.QtCore import Qt
|
|
13
14
|
from PySide6.QtWidgets import QDialog, QLabel, QHBoxLayout, QVBoxLayout, QPushButton
|
|
14
15
|
|
|
@@ -32,6 +33,7 @@ class ConfirmDialog(QDialog):
|
|
|
32
33
|
self.setWindowTitle(trans('dialog.confirm.title'))
|
|
33
34
|
self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint) # always on top
|
|
34
35
|
|
|
36
|
+
# Buttons
|
|
35
37
|
self.window.ui.nodes['dialog.confirm.btn.yes'] = QPushButton(trans('dialog.confirm.yes'))
|
|
36
38
|
self.window.ui.nodes['dialog.confirm.btn.yes'].clicked.connect(
|
|
37
39
|
lambda: self.window.controller.dialogs.confirm.accept(self.type, self.id, self.parent_object))
|
|
@@ -40,9 +42,24 @@ class ConfirmDialog(QDialog):
|
|
|
40
42
|
self.window.ui.nodes['dialog.confirm.btn.no'].clicked.connect(
|
|
41
43
|
lambda: self.window.controller.dialogs.confirm.dismiss(self.type, self.id))
|
|
42
44
|
|
|
45
|
+
# Always make the neutral action (No/Cancel) the default/active one.
|
|
46
|
+
# This ensures Enter triggers the safe option by default.
|
|
47
|
+
self.window.ui.nodes['dialog.confirm.btn.no'].setAutoDefault(True)
|
|
48
|
+
self.window.ui.nodes['dialog.confirm.btn.no'].setDefault(True)
|
|
49
|
+
self.window.ui.nodes['dialog.confirm.btn.no'].setFocus()
|
|
50
|
+
self.window.ui.nodes['dialog.confirm.btn.yes'].setAutoDefault(False)
|
|
51
|
+
self.window.ui.nodes['dialog.confirm.btn.yes'].setDefault(False)
|
|
52
|
+
|
|
53
|
+
# Bottom button row with platform-specific ordering
|
|
54
|
+
# Windows: affirmative on the left, neutral on the right
|
|
55
|
+
# Linux/macOS: neutral on the left, affirmative on the right
|
|
43
56
|
bottom = QHBoxLayout()
|
|
44
|
-
|
|
45
|
-
|
|
57
|
+
if self._affirmative_on_left():
|
|
58
|
+
bottom.addWidget(self.window.ui.nodes['dialog.confirm.btn.yes'])
|
|
59
|
+
bottom.addWidget(self.window.ui.nodes['dialog.confirm.btn.no'])
|
|
60
|
+
else:
|
|
61
|
+
bottom.addWidget(self.window.ui.nodes['dialog.confirm.btn.no'])
|
|
62
|
+
bottom.addWidget(self.window.ui.nodes['dialog.confirm.btn.yes'])
|
|
46
63
|
|
|
47
64
|
self.layout = QVBoxLayout()
|
|
48
65
|
self.message = QLabel("")
|
|
@@ -54,6 +71,13 @@ class ConfirmDialog(QDialog):
|
|
|
54
71
|
self.layout.addLayout(bottom)
|
|
55
72
|
self.setLayout(self.layout)
|
|
56
73
|
|
|
74
|
+
def _affirmative_on_left(self) -> bool:
|
|
75
|
+
"""
|
|
76
|
+
Decide button order depending on the platform.
|
|
77
|
+
Returns True on Windows, False otherwise (Linux/macOS).
|
|
78
|
+
"""
|
|
79
|
+
return sys.platform.startswith('win')
|
|
80
|
+
|
|
57
81
|
def closeEvent(self, event):
|
|
58
82
|
"""
|
|
59
83
|
Close event handler
|
pygpt_net/ui/widget/dialog/db.py
CHANGED
|
File without changes
|
|
@@ -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.09.
|
|
9
|
+
# Updated Date: 2025.09.26 12:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import datetime
|
|
@@ -98,6 +98,11 @@ class PainterWidget(QWidget):
|
|
|
98
98
|
self._autoScrollMinSpeed = 2 # px per tick (min)
|
|
99
99
|
self._autoScrollMaxSpeed = 18 # px per tick (max)
|
|
100
100
|
|
|
101
|
+
# Pan (middle mouse) state
|
|
102
|
+
self._panning = False
|
|
103
|
+
self._panLastGlobalPos = QPoint()
|
|
104
|
+
self._cursorBeforePan = None # store/restore cursor shape while panning
|
|
105
|
+
|
|
101
106
|
# Actions
|
|
102
107
|
self._act_undo = QAction(QIcon(":/icons/undo.svg"), trans('action.undo'), self)
|
|
103
108
|
self._act_undo.triggered.connect(self.undo)
|
|
@@ -1084,6 +1089,69 @@ class PainterWidget(QWidget):
|
|
|
1084
1089
|
if scrolled or dx != 0 or dy != 0:
|
|
1085
1090
|
self.update()
|
|
1086
1091
|
|
|
1092
|
+
# ---------- Pan (middle mouse drag) ----------
|
|
1093
|
+
|
|
1094
|
+
def _can_pan(self) -> bool:
|
|
1095
|
+
"""
|
|
1096
|
+
Return True if widget is inside a scroll area and content is scrollable.
|
|
1097
|
+
"""
|
|
1098
|
+
self._find_scroll_area()
|
|
1099
|
+
if self._scrollArea is None:
|
|
1100
|
+
return False
|
|
1101
|
+
hbar = self._scrollArea.horizontalScrollBar()
|
|
1102
|
+
vbar = self._scrollArea.verticalScrollBar()
|
|
1103
|
+
h_ok = hbar is not None and hbar.maximum() > hbar.minimum()
|
|
1104
|
+
v_ok = vbar is not None and vbar.maximum() > vbar.minimum()
|
|
1105
|
+
return h_ok or v_ok
|
|
1106
|
+
|
|
1107
|
+
def _start_pan(self, global_pos: QPoint):
|
|
1108
|
+
"""
|
|
1109
|
+
Begin view panning with middle mouse button.
|
|
1110
|
+
"""
|
|
1111
|
+
if self._panning:
|
|
1112
|
+
return
|
|
1113
|
+
self._panning = True
|
|
1114
|
+
self._panLastGlobalPos = QPoint(global_pos)
|
|
1115
|
+
# Store current cursor to restore later
|
|
1116
|
+
self._cursorBeforePan = QCursor(self.cursor())
|
|
1117
|
+
# Use a closed hand to indicate grabbing the canvas
|
|
1118
|
+
self.setCursor(QCursor(Qt.ClosedHandCursor))
|
|
1119
|
+
self.grabMouse()
|
|
1120
|
+
|
|
1121
|
+
def _update_pan(self, global_pos: QPoint):
|
|
1122
|
+
"""
|
|
1123
|
+
Update scrollbars based on mouse movement delta in global coordinates.
|
|
1124
|
+
"""
|
|
1125
|
+
if not self._panning or self._scrollArea is None:
|
|
1126
|
+
return
|
|
1127
|
+
dx = global_pos.x() - self._panLastGlobalPos.x()
|
|
1128
|
+
dy = global_pos.y() - self._panLastGlobalPos.y()
|
|
1129
|
+
self._panLastGlobalPos = QPoint(global_pos)
|
|
1130
|
+
|
|
1131
|
+
hbar = self._scrollArea.horizontalScrollBar()
|
|
1132
|
+
vbar = self._scrollArea.verticalScrollBar()
|
|
1133
|
+
|
|
1134
|
+
# Dragging the content to the right should reveal the left side -> subtract deltas
|
|
1135
|
+
if hbar is not None and hbar.maximum() > hbar.minimum():
|
|
1136
|
+
hbar.setValue(int(max(hbar.minimum(), min(hbar.maximum(), hbar.value() - dx))))
|
|
1137
|
+
if vbar is not None and vbar.maximum() > vbar.minimum():
|
|
1138
|
+
vbar.setValue(int(max(vbar.minimum(), min(vbar.maximum(), vbar.value() - dy))))
|
|
1139
|
+
|
|
1140
|
+
def _end_pan(self):
|
|
1141
|
+
"""
|
|
1142
|
+
End panning and restore previous cursor.
|
|
1143
|
+
"""
|
|
1144
|
+
if not self._panning:
|
|
1145
|
+
return
|
|
1146
|
+
self._panning = False
|
|
1147
|
+
self.releaseMouse()
|
|
1148
|
+
try:
|
|
1149
|
+
if self._cursorBeforePan is not None:
|
|
1150
|
+
# Restore previous cursor (do not guess based on mode/crop)
|
|
1151
|
+
self.setCursor(self._cursorBeforePan)
|
|
1152
|
+
finally:
|
|
1153
|
+
self._cursorBeforePan = None
|
|
1154
|
+
|
|
1087
1155
|
# ---------- Events ----------
|
|
1088
1156
|
|
|
1089
1157
|
def wheelEvent(self, event):
|
|
@@ -1109,6 +1177,14 @@ class PainterWidget(QWidget):
|
|
|
1109
1177
|
|
|
1110
1178
|
:param event: Event
|
|
1111
1179
|
"""
|
|
1180
|
+
# Middle button: start panning if scrollable
|
|
1181
|
+
if event.button() == Qt.MiddleButton:
|
|
1182
|
+
if not (self.cropping and self._selecting) and not self.drawing and self._can_pan():
|
|
1183
|
+
gp = event.globalPosition().toPoint()
|
|
1184
|
+
self._start_pan(gp)
|
|
1185
|
+
event.accept()
|
|
1186
|
+
return
|
|
1187
|
+
|
|
1112
1188
|
if event.button() == Qt.LeftButton:
|
|
1113
1189
|
self._mouseDown = True
|
|
1114
1190
|
if self.cropping:
|
|
@@ -1146,6 +1222,13 @@ class PainterWidget(QWidget):
|
|
|
1146
1222
|
|
|
1147
1223
|
:param event: Event
|
|
1148
1224
|
"""
|
|
1225
|
+
# Update panning if active
|
|
1226
|
+
if self._panning and (event.buttons() & Qt.MiddleButton):
|
|
1227
|
+
gp = event.globalPosition().toPoint()
|
|
1228
|
+
self._update_pan(gp)
|
|
1229
|
+
event.accept()
|
|
1230
|
+
return
|
|
1231
|
+
|
|
1149
1232
|
if self.cropping and self._selecting and (event.buttons() & Qt.LeftButton):
|
|
1150
1233
|
self._selectionRect = QRect(self._selectionStart, self._to_canvas_point(event.position()))
|
|
1151
1234
|
self.update()
|
|
@@ -1175,6 +1258,12 @@ class PainterWidget(QWidget):
|
|
|
1175
1258
|
|
|
1176
1259
|
:param event: Event
|
|
1177
1260
|
"""
|
|
1261
|
+
# End panning on middle button release
|
|
1262
|
+
if event.button() == Qt.MiddleButton:
|
|
1263
|
+
self._end_pan()
|
|
1264
|
+
event.accept()
|
|
1265
|
+
return
|
|
1266
|
+
|
|
1178
1267
|
if event.button() in (Qt.LeftButton, Qt.RightButton):
|
|
1179
1268
|
self._mouseDown = False
|
|
1180
1269
|
if self.cropping and self._selecting:
|