pygpt-net 2.6.60__py3-none-any.whl → 2.6.61__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 +7 -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/presets/presets.py +121 -6
- pygpt_net/controller/settings/editor.py +0 -15
- pygpt_net/controller/theme/markdown.py +2 -5
- pygpt_net/controller/ui/ui.py +4 -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 +35 -2
- 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 +2 -1
- pygpt_net/core/agents/custom/schema.py +3 -1
- pygpt_net/core/agents/custom/utils.py +13 -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 +3 -1
- pygpt_net/core/presets/presets.py +216 -29
- pygpt_net/core/render/markdown/parser.py +0 -2
- 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/locale/locale.de.ini +64 -1
- pygpt_net/data/locale/locale.en.ini +62 -3
- pygpt_net/data/locale/locale.es.ini +64 -1
- pygpt_net/data/locale/locale.fr.ini +64 -1
- pygpt_net/data/locale/locale.it.ini +64 -1
- pygpt_net/data/locale/locale.pl.ini +65 -2
- pygpt_net/data/locale/locale.uk.ini +64 -1
- pygpt_net/data/locale/locale.zh.ini +64 -1
- pygpt_net/data/locale/plugin.cmd_system.en.ini +62 -66
- pygpt_net/provider/agents/llama_index/flow_from_schema.py +2 -2
- pygpt_net/provider/core/config/patch.py +10 -1
- pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +0 -6
- pygpt_net/tools/agent_builder/tool.py +42 -26
- pygpt_net/tools/agent_builder/ui/dialogs.py +60 -11
- 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 +52 -16
- pygpt_net/ui/widget/dialog/db.py +0 -0
- pygpt_net/ui/widget/lists/preset.py +644 -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 +183 -151
- pygpt_net/ui/widget/node_editor/item.py +12 -11
- pygpt_net/ui/widget/node_editor/node.py +267 -12
- pygpt_net/ui/widget/node_editor/view.py +180 -63
- pygpt_net/ui/widget/tabs/output.py +1 -1
- pygpt_net/ui/widget/textarea/input.py +2 -2
- pygpt_net/utils.py +114 -2
- {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.61.dist-info}/METADATA +11 -94
- {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.61.dist-info}/RECORD +59 -58
- {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.61.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.61.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.61.dist-info}/entry_points.txt +0 -0
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
from PySide6.QtCore import Qt, QEvent
|
|
13
13
|
from PySide6.QtGui import QAction, QIcon
|
|
14
|
-
from PySide6.QtWidgets import QVBoxLayout, QMenuBar, QSplitter, QSizePolicy
|
|
14
|
+
from PySide6.QtWidgets import QVBoxLayout, QMenuBar, QSplitter, QSizePolicy, QWidget
|
|
15
15
|
|
|
16
16
|
from pygpt_net.ui.widget.element.labels import HelpLabel
|
|
17
17
|
from pygpt_net.ui.widget.node_editor.editor import NodeEditor
|
|
@@ -41,13 +41,13 @@ class Builder:
|
|
|
41
41
|
self.file_menu = self.menu_bar.addMenu(trans("menu.file"))
|
|
42
42
|
t = self.tool
|
|
43
43
|
|
|
44
|
-
self.actions["open"] = QAction(QIcon(":/icons/reload.svg"), "
|
|
44
|
+
self.actions["open"] = QAction(QIcon(":/icons/reload.svg"), trans("action.reload"), self.menu_bar)
|
|
45
45
|
self.actions["open"].triggered.connect(lambda checked=False, t=t: t.load())
|
|
46
46
|
|
|
47
47
|
self.actions["save"] = QAction(QIcon(":/icons/save.svg"), trans("action.save"), self.menu_bar)
|
|
48
48
|
self.actions["save"].triggered.connect(lambda checked=False, t=t: t.save())
|
|
49
49
|
|
|
50
|
-
self.actions["clear"] = QAction(QIcon(":/icons/
|
|
50
|
+
self.actions["clear"] = QAction(QIcon(":/icons/close.svg"), trans("action.clear"), self.menu_bar)
|
|
51
51
|
self.actions["clear"].triggered.connect(lambda checked=False, t=t: t.clear())
|
|
52
52
|
|
|
53
53
|
self.file_menu.addAction(self.actions["open"])
|
|
@@ -77,7 +77,28 @@ class Builder:
|
|
|
77
77
|
registry=registry
|
|
78
78
|
) # parent == dialog
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
theme = self.window.core.config.get("theme")
|
|
81
|
+
if theme.startswith("light"):
|
|
82
|
+
style = """
|
|
83
|
+
NodeEditor {
|
|
84
|
+
qproperty-gridBackColor: #ffffff;
|
|
85
|
+
qproperty-gridPenColor: #eaeaea;
|
|
86
|
+
|
|
87
|
+
qproperty-nodeBackgroundColor: #2d2f34;
|
|
88
|
+
qproperty-nodeBorderColor: #4b4f57;
|
|
89
|
+
qproperty-nodeSelectionColor: #ff9900;
|
|
90
|
+
qproperty-nodeTitleColor: #3a3d44;
|
|
91
|
+
|
|
92
|
+
qproperty-portInputColor: #66b2ff;
|
|
93
|
+
qproperty-portOutputColor: #70e070;
|
|
94
|
+
qproperty-portConnectedColor: #ffd166;
|
|
95
|
+
|
|
96
|
+
qproperty-edgeColor: #c0c0c0;
|
|
97
|
+
qproperty-edgeSelectedColor: #ff8a5c;
|
|
98
|
+
}
|
|
99
|
+
"""
|
|
100
|
+
else:
|
|
101
|
+
style = """
|
|
81
102
|
NodeEditor {
|
|
82
103
|
qproperty-gridBackColor: #242629;
|
|
83
104
|
qproperty-gridPenColor: #3b3f46;
|
|
@@ -94,8 +115,10 @@ class Builder:
|
|
|
94
115
|
qproperty-edgeColor: #c0c0c0;
|
|
95
116
|
qproperty-edgeSelectedColor: #ff8a5c;
|
|
96
117
|
}
|
|
97
|
-
"""
|
|
118
|
+
"""
|
|
119
|
+
editor.setStyleSheet(style)
|
|
98
120
|
editor.on_clear = self.tool.clear
|
|
121
|
+
editor.editing_allowed = self.tool.editing_allowed
|
|
99
122
|
|
|
100
123
|
u.editor[id] = editor
|
|
101
124
|
|
|
@@ -104,9 +127,28 @@ class Builder:
|
|
|
104
127
|
|
|
105
128
|
agents_list = AgentsWidget(self.window, tool=self.tool, parent=dlg)
|
|
106
129
|
list_widget = agents_list.setup()
|
|
107
|
-
|
|
130
|
+
|
|
131
|
+
# Left side container: list fills all space, help label stays at the bottom
|
|
132
|
+
left_side = QWidget(dlg)
|
|
133
|
+
left_layout = QVBoxLayout(left_side)
|
|
134
|
+
left_layout.setContentsMargins(0, 0, 0, 0)
|
|
135
|
+
left_layout.setSpacing(6)
|
|
136
|
+
|
|
137
|
+
left_help_label = HelpLabel(trans("node.editor.list.tip"))
|
|
138
|
+
left_help_label.setWordWrap(True)
|
|
139
|
+
left_help_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
|
140
|
+
left_help_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
|
141
|
+
left_help_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum)
|
|
142
|
+
|
|
143
|
+
left_layout.addWidget(list_widget)
|
|
144
|
+
left_layout.addWidget(left_help_label)
|
|
145
|
+
left_layout.setStretch(0, 1) # list -> fills all vertical space
|
|
146
|
+
|
|
147
|
+
# Fix the width of the whole left panel (not only the list)
|
|
148
|
+
left_side.setFixedWidth(250)
|
|
149
|
+
|
|
108
150
|
center_splitter = QSplitter(Qt.Horizontal)
|
|
109
|
-
center_splitter.addWidget(
|
|
151
|
+
center_splitter.addWidget(left_side)
|
|
110
152
|
center_splitter.addWidget(u.editor[id])
|
|
111
153
|
center_splitter.setStretchFactor(0, 1)
|
|
112
154
|
center_splitter.setStretchFactor(1, 8)
|
|
@@ -115,10 +157,7 @@ class Builder:
|
|
|
115
157
|
layout.setStretch(0, 1)
|
|
116
158
|
|
|
117
159
|
# Bottom legend as a compact, centered help label
|
|
118
|
-
legend_label = HelpLabel(
|
|
119
|
-
"Right-click: add node / undo / redo • Middle-click: pan view • Ctrl + Mouse wheel: zoom • "
|
|
120
|
-
"Left-click a port: create connection • Ctrl + Left-click a port: rewire or detach • Right-click or DEL a node/connection: remove"
|
|
121
|
-
)
|
|
160
|
+
legend_label = HelpLabel(trans("node.editor.bottom.tip"))
|
|
122
161
|
legend_label.setAlignment(Qt.AlignCenter)
|
|
123
162
|
legend_label.setWordWrap(True)
|
|
124
163
|
legend_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
|
@@ -129,6 +168,7 @@ class Builder:
|
|
|
129
168
|
u.nodes["agent.builder.splitter"] = center_splitter
|
|
130
169
|
u.nodes["agent.builder.list"] = agents_list
|
|
131
170
|
u.nodes["agent.builder.legend"] = legend_label
|
|
171
|
+
u.nodes["agent.builder.list.help"] = left_help_label
|
|
132
172
|
|
|
133
173
|
dlg.setLayout(layout)
|
|
134
174
|
dlg.setWindowTitle(trans("agent.builder.title"))
|
|
@@ -231,6 +271,15 @@ class BuilderDialog(BaseDialog):
|
|
|
231
271
|
except Exception:
|
|
232
272
|
pass
|
|
233
273
|
|
|
274
|
+
# Dispose left-side help label safely
|
|
275
|
+
try:
|
|
276
|
+
help_lbl = u.nodes.pop("agent.builder.list.help", None)
|
|
277
|
+
if help_lbl is not None:
|
|
278
|
+
help_lbl.setParent(None)
|
|
279
|
+
help_lbl.deleteLater()
|
|
280
|
+
except Exception:
|
|
281
|
+
pass
|
|
282
|
+
|
|
234
283
|
# Drop splitter reference
|
|
235
284
|
try:
|
|
236
285
|
u.nodes.pop("agent.builder.splitter", None)
|
pygpt_net/ui/__init__.py
CHANGED
|
@@ -172,15 +172,13 @@ class UI:
|
|
|
172
172
|
|
|
173
173
|
def show_loading(self):
|
|
174
174
|
"""Show loading"""
|
|
175
|
-
|
|
176
|
-
return
|
|
175
|
+
return
|
|
177
176
|
self.window.ui.nodes['anim.loading'].start_anim()
|
|
178
177
|
self.window.ui.nodes['anim.loading'].show()
|
|
179
178
|
|
|
180
179
|
def hide_loading(self):
|
|
181
180
|
"""Hide loading"""
|
|
182
|
-
|
|
183
|
-
return
|
|
181
|
+
return
|
|
184
182
|
self.window.ui.nodes['anim.loading'].stop_anim()
|
|
185
183
|
self.window.ui.nodes['anim.loading'].hide()
|
|
186
184
|
|
pygpt_net/ui/dialog/about.py
CHANGED
|
@@ -19,6 +19,9 @@ from pygpt_net.utils import trans
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class About:
|
|
22
|
+
|
|
23
|
+
RELEASE_YEAR = 2025
|
|
24
|
+
|
|
22
25
|
def __init__(self, window=None):
|
|
23
26
|
"""
|
|
24
27
|
About dialog
|
|
@@ -36,34 +39,65 @@ class About:
|
|
|
36
39
|
"""
|
|
37
40
|
return self.window.core.updater.get_fetch_thanks()
|
|
38
41
|
|
|
42
|
+
def build_versions_str(self, lib_versions: dict, break_after: str = "LlamaIndex") -> str:
|
|
43
|
+
parts = []
|
|
44
|
+
line = []
|
|
45
|
+
for k, v in lib_versions.items():
|
|
46
|
+
line.append(f"{k}: {v}")
|
|
47
|
+
if k == break_after:
|
|
48
|
+
parts.append(", ".join(line))
|
|
49
|
+
line = []
|
|
50
|
+
if line:
|
|
51
|
+
parts.append(", ".join(line))
|
|
52
|
+
return "\n".join(parts)
|
|
53
|
+
|
|
39
54
|
def prepare_content(self) -> str:
|
|
40
55
|
"""
|
|
41
56
|
Get info text
|
|
42
57
|
|
|
43
58
|
:return: info text
|
|
44
59
|
"""
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
60
|
+
lib_versions = {}
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
import platform
|
|
64
|
+
lib_versions['Python'] = platform.python_version()
|
|
65
|
+
except ImportError:
|
|
66
|
+
pass
|
|
49
67
|
|
|
50
68
|
try:
|
|
51
|
-
from llama_index.core import __version__ as llama_index_version
|
|
52
|
-
# from langchain import __version__ as langchain_version
|
|
53
69
|
from openai.version import VERSION as openai_version
|
|
70
|
+
lib_versions['OpenAI API'] = openai_version
|
|
71
|
+
except ImportError:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
from llama_index.core import __version__ as llama_index_version
|
|
76
|
+
lib_versions['LlamaIndex'] = llama_index_version
|
|
77
|
+
except ImportError:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
from anthropic import __version__ as anthropic_version
|
|
82
|
+
lib_versions['Anthropic API'] = anthropic_version
|
|
83
|
+
except ImportError:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
from google.genai import __version__ as google_genai_version
|
|
88
|
+
lib_versions['Google API'] = google_genai_version
|
|
54
89
|
except ImportError:
|
|
55
90
|
pass
|
|
56
91
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
92
|
+
try:
|
|
93
|
+
from xai_sdk import __version__ as xai_sdk_version
|
|
94
|
+
lib_versions['xAI API'] = xai_sdk_version
|
|
95
|
+
except ImportError:
|
|
96
|
+
pass
|
|
60
97
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
# langchain_version,
|
|
65
|
-
llama_index_version,
|
|
66
|
-
)
|
|
98
|
+
versions_str = ""
|
|
99
|
+
if lib_versions:
|
|
100
|
+
versions_str = self.build_versions_str(lib_versions, break_after="LlamaIndex")
|
|
67
101
|
|
|
68
102
|
platform = self.window.core.platforms.get_as_string()
|
|
69
103
|
version = self.window.meta['version']
|
|
@@ -80,29 +114,14 @@ class About:
|
|
|
80
114
|
label_github = trans("dialog.about.github")
|
|
81
115
|
label_docs = trans("dialog.about.docs")
|
|
82
116
|
|
|
83
|
-
data = "{label_version}: {version}, {platform}\n" \
|
|
84
|
-
"{label_build}: {build}\n" \
|
|
85
|
-
"{
|
|
86
|
-
"{label_website}: {website}\n" \
|
|
87
|
-
"{label_github}: {github}\n" \
|
|
88
|
-
"{label_docs}: {docs}\n\n" \
|
|
89
|
-
"(c)
|
|
90
|
-
"{email}\n"
|
|
91
|
-
label_version=label_version,
|
|
92
|
-
version=version,
|
|
93
|
-
platform=platform,
|
|
94
|
-
label_build=label_build,
|
|
95
|
-
build=build.replace('.', '-'),
|
|
96
|
-
label_website=label_website,
|
|
97
|
-
website=website,
|
|
98
|
-
label_github=label_github,
|
|
99
|
-
github=github,
|
|
100
|
-
label_docs=label_docs,
|
|
101
|
-
docs=docs,
|
|
102
|
-
author=author,
|
|
103
|
-
email=email,
|
|
104
|
-
lib_versions=lib_versions,
|
|
105
|
-
)
|
|
117
|
+
data = f"{label_version}: {version}, {platform}\n" \
|
|
118
|
+
f"{label_build}: {build.replace('.', '-')}\n\n" \
|
|
119
|
+
f"{versions_str}\n\n" \
|
|
120
|
+
f"{label_website}: {website}\n" \
|
|
121
|
+
f"{label_github}: {github}\n" \
|
|
122
|
+
f"{label_docs}: {docs}\n\n" \
|
|
123
|
+
f"(c) {self.RELEASE_YEAR} {author}\n" \
|
|
124
|
+
f"{email}\n"
|
|
106
125
|
return data
|
|
107
126
|
|
|
108
127
|
def setup(self):
|
|
@@ -139,6 +158,7 @@ class About:
|
|
|
139
158
|
string = self.prepare_content()
|
|
140
159
|
content = QLabel(string)
|
|
141
160
|
content.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
|
161
|
+
content.setWordWrap(True)
|
|
142
162
|
self.window.ui.nodes['dialog.about.content'] = content
|
|
143
163
|
|
|
144
164
|
thanks = QLabel(trans('about.thanks'))
|
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)
|