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
|
@@ -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.25 12:05:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
@@ -140,20 +140,16 @@ class PortItem(QGraphicsObject):
|
|
|
140
140
|
except Exception:
|
|
141
141
|
pass
|
|
142
142
|
cap_val = self._allowed_capacity()
|
|
143
|
+
cfg = self.node_item.editor.config
|
|
143
144
|
if isinstance(cap_val, int):
|
|
144
145
|
if cap_val < 0:
|
|
145
|
-
cap_str =
|
|
146
|
+
cap_str = cfg.cap_unlimited()
|
|
146
147
|
else:
|
|
147
148
|
cap_str = str(cap_val)
|
|
148
149
|
else:
|
|
149
|
-
cap_str =
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
f"Port: {self.side.upper()} • {self.prop_id}\n"
|
|
153
|
-
f"Allowed connections: {cap_str}\n\n"
|
|
154
|
-
f"Click: start a new connection\n"
|
|
155
|
-
f"Ctrl+Click: rewire/detach existing"
|
|
156
|
-
)
|
|
150
|
+
cap_str = cfg.cap_na()
|
|
151
|
+
side_label = cfg.side_label(self.side).upper()
|
|
152
|
+
tip = cfg.port_tooltip(node_name, side_label, self.prop_id, cap_str)
|
|
157
153
|
self.setToolTip(tip)
|
|
158
154
|
try:
|
|
159
155
|
self._label_cap.setToolTip(tip)
|
|
@@ -225,6 +221,11 @@ class PortItem(QGraphicsObject):
|
|
|
225
221
|
def mousePressEvent(self, e):
|
|
226
222
|
"""Emit portClicked on left click to begin a connection or rewire."""
|
|
227
223
|
if e.button() == Qt.LeftButton:
|
|
224
|
+
try:
|
|
225
|
+
# Bring parent node to front when clicking the port
|
|
226
|
+
self.node_item.editor.raise_node_to_top(self.node_item)
|
|
227
|
+
except Exception:
|
|
228
|
+
pass
|
|
228
229
|
self.node_item.editor._dbg(f"Port clicked: side={self.side}, node={self.node_item.node.name}({self.node_item.node.uuid}), prop={self.prop_id}, connected_count={self._connected_count}")
|
|
229
230
|
self.portClicked.emit(self)
|
|
230
231
|
e.accept()
|
|
@@ -355,7 +356,7 @@ class EdgeItem(QGraphicsPathItem):
|
|
|
355
356
|
ss = self._editor.window().styleSheet()
|
|
356
357
|
if ss:
|
|
357
358
|
menu.setStyleSheet(ss)
|
|
358
|
-
act_del = QAction(
|
|
359
|
+
act_del = QAction(self._editor.config.edge_context_delete(), menu)
|
|
359
360
|
menu.addAction(act_del)
|
|
360
361
|
chosen = menu.exec(event.screenPos())
|
|
361
362
|
if chosen == act_del:
|
|
@@ -6,14 +6,14 @@
|
|
|
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.25 15:32:39 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
import re
|
|
14
|
-
from typing import Dict, Optional, List, Any
|
|
14
|
+
from typing import Dict, Optional, List, Any, Tuple
|
|
15
15
|
|
|
16
|
-
from PySide6.QtCore import Qt, QPointF, QRectF, QSizeF, Signal,QEvent
|
|
16
|
+
from PySide6.QtCore import Qt, QPointF, QRectF, QSizeF, Signal, QEvent
|
|
17
17
|
from PySide6.QtGui import QAction, QBrush, QColor, QPainter, QPainterPath, QPen
|
|
18
18
|
from PySide6.QtWidgets import (
|
|
19
19
|
QWidget, QApplication, QGraphicsItem, QGraphicsWidget, QGraphicsProxyWidget, QStyleOptionGraphicsItem,
|
|
@@ -86,12 +86,65 @@ class SingleLineTextEdit(QTextEdit):
|
|
|
86
86
|
c.setPosition(min(pos, len(t2)))
|
|
87
87
|
self.setTextCursor(c)
|
|
88
88
|
|
|
89
|
+
def _apply_menu_theme(self, menu: QMenu):
|
|
90
|
+
"""Apply app/window stylesheet + palette + font to context menu."""
|
|
91
|
+
try:
|
|
92
|
+
wnd = self.window()
|
|
93
|
+
except Exception:
|
|
94
|
+
wnd = None
|
|
95
|
+
stylesheet = ""
|
|
96
|
+
pal = None
|
|
97
|
+
font = None
|
|
98
|
+
try:
|
|
99
|
+
if wnd:
|
|
100
|
+
stylesheet = wnd.styleSheet() or ""
|
|
101
|
+
pal = wnd.palette()
|
|
102
|
+
font = wnd.font()
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
try:
|
|
106
|
+
app = QApplication.instance()
|
|
107
|
+
if app:
|
|
108
|
+
if not stylesheet and app.styleSheet():
|
|
109
|
+
stylesheet = app.styleSheet()
|
|
110
|
+
if pal is None:
|
|
111
|
+
pal = app.palette()
|
|
112
|
+
if font is None:
|
|
113
|
+
font = app.font()
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
try:
|
|
117
|
+
if pal:
|
|
118
|
+
menu.setPalette(pal)
|
|
119
|
+
if font:
|
|
120
|
+
menu.setFont(font)
|
|
121
|
+
if stylesheet:
|
|
122
|
+
menu.setStyleSheet(stylesheet)
|
|
123
|
+
menu.ensurePolished()
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
def contextMenuEvent(self, e):
|
|
128
|
+
"""Ensure standard context menu follows app-wide (e.g., Qt Material) styling."""
|
|
129
|
+
try:
|
|
130
|
+
menu = self.createStandardContextMenu()
|
|
131
|
+
except Exception:
|
|
132
|
+
return super().contextMenuEvent(e)
|
|
133
|
+
self._apply_menu_theme(menu)
|
|
134
|
+
try:
|
|
135
|
+
menu.exec(e.globalPos())
|
|
136
|
+
finally:
|
|
137
|
+
try:
|
|
138
|
+
menu.deleteLater()
|
|
139
|
+
except Exception:
|
|
140
|
+
pass
|
|
141
|
+
|
|
89
142
|
|
|
90
143
|
class NodeContentWidget(QWidget):
|
|
91
144
|
"""Form-like widget that renders property editors for a node.
|
|
92
145
|
|
|
93
146
|
The widget builds appropriate Qt editors based on PropertyModel.type:
|
|
94
|
-
- "str": QLineEdit
|
|
147
|
+
- "str": QLineEdit-like (SingleLineTextEdit with placeholder support)
|
|
95
148
|
- "text": QTextEdit
|
|
96
149
|
- "int": QSpinBox
|
|
97
150
|
- "float": QDoubleSpinBox
|
|
@@ -103,6 +156,8 @@ class NodeContentWidget(QWidget):
|
|
|
103
156
|
|
|
104
157
|
Notes:
|
|
105
158
|
- For Base ID-like properties, the field is read-only; a composed display value is shown.
|
|
159
|
+
- Placeholder and description are applied when provided in the type spec or model
|
|
160
|
+
(description is shown as tooltip on both the editor and its label).
|
|
106
161
|
- ShortcutOverride is handled so typing in editors does not trigger scene shortcuts.
|
|
107
162
|
|
|
108
163
|
Signal:
|
|
@@ -125,7 +180,24 @@ class NodeContentWidget(QWidget):
|
|
|
125
180
|
self._editors: Dict[str, QWidget] = {}
|
|
126
181
|
for pid, pm in node.properties.items():
|
|
127
182
|
editable = self._editable_from_spec(pid, pm)
|
|
183
|
+
|
|
184
|
+
# Resolve UI hints: placeholder and description from spec (fallback to model)
|
|
185
|
+
placeholder = self._spec_text_attr(pid, "placeholder")
|
|
186
|
+
try:
|
|
187
|
+
if not placeholder and hasattr(pm, "placeholder") and getattr(pm, "placeholder") not in (None, ""):
|
|
188
|
+
placeholder = str(getattr(pm, "placeholder"))
|
|
189
|
+
except Exception:
|
|
190
|
+
pass
|
|
191
|
+
description = self._spec_text_attr(pid, "description")
|
|
192
|
+
try:
|
|
193
|
+
if not description and hasattr(pm, "description") and getattr(pm, "description") not in (None, ""):
|
|
194
|
+
description = str(getattr(pm, "description"))
|
|
195
|
+
except Exception:
|
|
196
|
+
pass
|
|
197
|
+
|
|
128
198
|
w: QWidget
|
|
199
|
+
extra_tooltip: Optional[str] = None # used e.g. for capacity hints
|
|
200
|
+
|
|
129
201
|
if pm.type == "str":
|
|
130
202
|
te = SingleLineTextEdit()
|
|
131
203
|
te.setFocusPolicy(Qt.StrongFocus)
|
|
@@ -145,17 +217,31 @@ class NodeContentWidget(QWidget):
|
|
|
145
217
|
te.setReadOnly(not editable)
|
|
146
218
|
|
|
147
219
|
te.setPlainText(txt)
|
|
220
|
+
if placeholder:
|
|
221
|
+
try:
|
|
222
|
+
te.setPlaceholderText(placeholder)
|
|
223
|
+
except Exception:
|
|
224
|
+
pass
|
|
148
225
|
te.textChanged.connect(lambda pid=pid, te=te: self.valueChanged.emit(pid, te.toPlainText()))
|
|
149
226
|
te.editingFinished.connect(lambda pid=pid, te=te: self.valueChanged.emit(pid, te.toPlainText()))
|
|
150
227
|
w = te
|
|
228
|
+
|
|
151
229
|
elif pm.type == "text":
|
|
152
230
|
te = QTextEdit()
|
|
153
231
|
te.setFocusPolicy(Qt.StrongFocus)
|
|
154
232
|
if pm.value is not None:
|
|
155
233
|
te.setPlainText(str(pm.value))
|
|
156
234
|
te.setReadOnly(not editable)
|
|
235
|
+
if placeholder:
|
|
236
|
+
try:
|
|
237
|
+
te.setPlaceholderText(placeholder)
|
|
238
|
+
except Exception:
|
|
239
|
+
pass
|
|
157
240
|
te.textChanged.connect(lambda pid=pid, te=te: self.valueChanged.emit(pid, te.toPlainText()))
|
|
241
|
+
# Ensure context menu follows global (Material) style
|
|
242
|
+
self._install_styled_context_menu(te)
|
|
158
243
|
w = te
|
|
244
|
+
|
|
159
245
|
elif pm.type == "int":
|
|
160
246
|
w = QSpinBox()
|
|
161
247
|
w.setFocusPolicy(Qt.StrongFocus)
|
|
@@ -163,7 +249,10 @@ class NodeContentWidget(QWidget):
|
|
|
163
249
|
if pm.value is not None:
|
|
164
250
|
w.setValue(int(pm.value))
|
|
165
251
|
w.setEnabled(editable)
|
|
252
|
+
|
|
253
|
+
# QSpinBox has no placeholder; use tooltip only
|
|
166
254
|
w.valueChanged.connect(lambda v, pid=pid: self.valueChanged.emit(pid, int(v)))
|
|
255
|
+
|
|
167
256
|
elif pm.type == "float":
|
|
168
257
|
w = QDoubleSpinBox()
|
|
169
258
|
w.setFocusPolicy(Qt.StrongFocus)
|
|
@@ -173,6 +262,7 @@ class NodeContentWidget(QWidget):
|
|
|
173
262
|
w.setValue(float(pm.value))
|
|
174
263
|
w.setEnabled(editable)
|
|
175
264
|
w.valueChanged.connect(lambda v, pid=pid: self.valueChanged.emit(pid, float(v)))
|
|
265
|
+
|
|
176
266
|
elif pm.type == "bool":
|
|
177
267
|
w = QCheckBox()
|
|
178
268
|
w.setFocusPolicy(Qt.StrongFocus)
|
|
@@ -180,6 +270,7 @@ class NodeContentWidget(QWidget):
|
|
|
180
270
|
w.setChecked(bool(pm.value))
|
|
181
271
|
w.setEnabled(editable)
|
|
182
272
|
w.toggled.connect(lambda v, pid=pid: self.valueChanged.emit(pid, bool(v)))
|
|
273
|
+
|
|
183
274
|
elif pm.type == "combo":
|
|
184
275
|
w = QComboBox()
|
|
185
276
|
w.setFocusPolicy(Qt.StrongFocus)
|
|
@@ -188,20 +279,92 @@ class NodeContentWidget(QWidget):
|
|
|
188
279
|
if pm.value is not None and pm.value in (pm.options or []):
|
|
189
280
|
w.setCurrentText(pm.value)
|
|
190
281
|
w.setEnabled(editable)
|
|
282
|
+
# QComboBox placeholder works when editable; we keep non-editable by default
|
|
191
283
|
w.currentTextChanged.connect(lambda v, pid=pid: self.valueChanged.emit(pid, v))
|
|
284
|
+
|
|
192
285
|
else:
|
|
193
286
|
# Render a read-only capacity placeholder for port-like properties ("input/output").
|
|
194
287
|
cap_text = self._capacity_text_for_property(pid, pm)
|
|
195
288
|
w = QLabel(cap_text)
|
|
196
289
|
w.setEnabled(False)
|
|
197
|
-
|
|
290
|
+
extra_tooltip = self.editor.config.port_capacity_tooltip(cap_text)
|
|
198
291
|
|
|
199
292
|
name = self._display_name_for_property(pid, pm)
|
|
200
293
|
if name == "Base ID":
|
|
201
|
-
name =
|
|
294
|
+
name = self.editor.config.label_id()
|
|
202
295
|
self.form.addRow(name, w)
|
|
203
296
|
self._editors[pid] = w
|
|
204
297
|
|
|
298
|
+
# Apply tooltip(s) after the row is created so we can also set the label tooltip
|
|
299
|
+
if description or extra_tooltip:
|
|
300
|
+
final_tt_parts: List[str] = []
|
|
301
|
+
if description:
|
|
302
|
+
final_tt_parts.append(description)
|
|
303
|
+
if extra_tooltip:
|
|
304
|
+
final_tt_parts.append(extra_tooltip)
|
|
305
|
+
final_tt = "\n".join(final_tt_parts)
|
|
306
|
+
try:
|
|
307
|
+
w.setToolTip(final_tt)
|
|
308
|
+
except Exception:
|
|
309
|
+
pass
|
|
310
|
+
try:
|
|
311
|
+
lbl = self.form.labelForField(w)
|
|
312
|
+
if lbl is not None:
|
|
313
|
+
lbl.setToolTip(description or "")
|
|
314
|
+
except Exception:
|
|
315
|
+
pass
|
|
316
|
+
|
|
317
|
+
def _install_styled_context_menu(self, te: QTextEdit):
|
|
318
|
+
"""Install a custom context menu handler that applies global styles."""
|
|
319
|
+
try:
|
|
320
|
+
te.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
321
|
+
te.customContextMenuRequested.connect(
|
|
322
|
+
lambda pos, _te=te: self._show_styled_standard_menu(_te, pos)
|
|
323
|
+
)
|
|
324
|
+
except Exception:
|
|
325
|
+
pass
|
|
326
|
+
|
|
327
|
+
def _show_styled_standard_menu(self, te: QTextEdit, pos):
|
|
328
|
+
"""Create standard menu and apply app/window stylesheet + palette + font."""
|
|
329
|
+
try:
|
|
330
|
+
menu = te.createStandardContextMenu()
|
|
331
|
+
except Exception:
|
|
332
|
+
return
|
|
333
|
+
stylesheet = ""
|
|
334
|
+
pal = None
|
|
335
|
+
font = None
|
|
336
|
+
try:
|
|
337
|
+
# Prefer editor helpers (consistent with the rest of NodeEditor)
|
|
338
|
+
stylesheet = self.editor._current_stylesheet()
|
|
339
|
+
pal = self.editor._current_palette()
|
|
340
|
+
font = self.editor._current_font()
|
|
341
|
+
except Exception:
|
|
342
|
+
try:
|
|
343
|
+
wnd = te.window()
|
|
344
|
+
if wnd:
|
|
345
|
+
stylesheet = wnd.styleSheet() or ""
|
|
346
|
+
pal = wnd.palette()
|
|
347
|
+
font = wnd.font()
|
|
348
|
+
except Exception:
|
|
349
|
+
pass
|
|
350
|
+
try:
|
|
351
|
+
if pal:
|
|
352
|
+
menu.setPalette(pal)
|
|
353
|
+
if font:
|
|
354
|
+
menu.setFont(font)
|
|
355
|
+
if stylesheet:
|
|
356
|
+
menu.setStyleSheet(stylesheet)
|
|
357
|
+
menu.ensurePolished()
|
|
358
|
+
except Exception:
|
|
359
|
+
pass
|
|
360
|
+
try:
|
|
361
|
+
menu.exec(te.mapToGlobal(pos))
|
|
362
|
+
finally:
|
|
363
|
+
try:
|
|
364
|
+
menu.deleteLater()
|
|
365
|
+
except Exception:
|
|
366
|
+
pass
|
|
367
|
+
|
|
205
368
|
def event(self, e):
|
|
206
369
|
"""
|
|
207
370
|
Filter ShortcutOverride so editor keystrokes are not eaten by the scene.
|
|
@@ -481,6 +644,55 @@ class NodeContentWidget(QWidget):
|
|
|
481
644
|
|
|
482
645
|
return None
|
|
483
646
|
|
|
647
|
+
# --- Spec helpers for UI hints ---
|
|
648
|
+
|
|
649
|
+
def _get_prop_spec(self, pid: str) -> Optional[Any]:
|
|
650
|
+
"""Return a property specification object from the registry for this node type."""
|
|
651
|
+
try:
|
|
652
|
+
reg = self.graph.registry
|
|
653
|
+
if not reg:
|
|
654
|
+
return None
|
|
655
|
+
type_spec = reg.get(self.node.type)
|
|
656
|
+
if not type_spec:
|
|
657
|
+
return None
|
|
658
|
+
|
|
659
|
+
for attr in ("properties", "props", "fields", "ports", "inputs", "outputs"):
|
|
660
|
+
try:
|
|
661
|
+
cont = getattr(type_spec, attr, None)
|
|
662
|
+
if isinstance(cont, dict) and pid in cont:
|
|
663
|
+
return cont[pid]
|
|
664
|
+
except Exception:
|
|
665
|
+
pass
|
|
666
|
+
for meth in ("get_property", "property_spec", "get_prop", "prop", "property"):
|
|
667
|
+
if hasattr(type_spec, meth):
|
|
668
|
+
try:
|
|
669
|
+
return getattr(type_spec, meth)(pid)
|
|
670
|
+
except Exception:
|
|
671
|
+
pass
|
|
672
|
+
except Exception:
|
|
673
|
+
pass
|
|
674
|
+
return None
|
|
675
|
+
|
|
676
|
+
def _spec_text_attr(self, pid: str, key: str) -> Optional[str]:
|
|
677
|
+
"""Get a string attribute (e.g., 'placeholder', 'description') from spec if available."""
|
|
678
|
+
obj = self._get_prop_spec(pid)
|
|
679
|
+
if obj is None:
|
|
680
|
+
return None
|
|
681
|
+
try:
|
|
682
|
+
v = getattr(obj, key, None)
|
|
683
|
+
if isinstance(v, str) and v != "":
|
|
684
|
+
return v
|
|
685
|
+
except Exception:
|
|
686
|
+
pass
|
|
687
|
+
try:
|
|
688
|
+
if isinstance(obj, dict):
|
|
689
|
+
v = obj.get(key)
|
|
690
|
+
if isinstance(v, str) and v != "":
|
|
691
|
+
return v
|
|
692
|
+
except Exception:
|
|
693
|
+
pass
|
|
694
|
+
return None
|
|
695
|
+
|
|
484
696
|
|
|
485
697
|
class NodeItem(QGraphicsWidget):
|
|
486
698
|
"""Rounded node with title and ports aligned to property rows.
|
|
@@ -854,6 +1066,14 @@ class NodeItem(QGraphicsWidget):
|
|
|
854
1066
|
if self._resizing:
|
|
855
1067
|
return
|
|
856
1068
|
|
|
1069
|
+
# When global grab mode is active, suppress resize/move cursors
|
|
1070
|
+
view = self.editor.view
|
|
1071
|
+
if getattr(view, "_global_grab_mode", False):
|
|
1072
|
+
self._hover_resize_mode = "none"
|
|
1073
|
+
self.unsetCursor()
|
|
1074
|
+
self.update()
|
|
1075
|
+
return
|
|
1076
|
+
|
|
857
1077
|
mode = self._hit_resize_zone(pos)
|
|
858
1078
|
self._hover_resize_mode = mode
|
|
859
1079
|
|
|
@@ -891,9 +1111,9 @@ class NodeItem(QGraphicsWidget):
|
|
|
891
1111
|
self.update()
|
|
892
1112
|
|
|
893
1113
|
def hoverMoveEvent(self, event):
|
|
894
|
-
"""While panning is active, suppress resize hints; otherwise update hover state."""
|
|
1114
|
+
"""While panning is active or global grab is enabled, suppress resize hints; otherwise update hover state."""
|
|
895
1115
|
view = self.editor.view
|
|
896
|
-
if getattr(view, "_panning", False):
|
|
1116
|
+
if getattr(view, "_panning", False) or getattr(view, "_global_grab_mode", False):
|
|
897
1117
|
self._hover_resize_mode = "none"
|
|
898
1118
|
self.unsetCursor()
|
|
899
1119
|
self.update()
|
|
@@ -912,6 +1132,17 @@ class NodeItem(QGraphicsWidget):
|
|
|
912
1132
|
|
|
913
1133
|
def mousePressEvent(self, event):
|
|
914
1134
|
"""Start resize when pressing the proper zone; otherwise begin move drag."""
|
|
1135
|
+
# Always bring clicked node to front (dynamic z-order)
|
|
1136
|
+
try:
|
|
1137
|
+
self.editor.raise_node_to_top(self)
|
|
1138
|
+
except Exception:
|
|
1139
|
+
pass
|
|
1140
|
+
|
|
1141
|
+
# If global grab is active, do not initiate node drag/resize (view handles panning)
|
|
1142
|
+
if getattr(self.editor.view, "_global_grab_mode", False) and event.button() == Qt.LeftButton:
|
|
1143
|
+
event.ignore()
|
|
1144
|
+
return
|
|
1145
|
+
|
|
915
1146
|
if event.button() == Qt.LeftButton:
|
|
916
1147
|
mode = self._hit_resize_zone(event.pos())
|
|
917
1148
|
if mode != "none":
|
|
@@ -971,7 +1202,7 @@ class NodeItem(QGraphicsWidget):
|
|
|
971
1202
|
return
|
|
972
1203
|
|
|
973
1204
|
if self._dragging and event.button() == Qt.LeftButton:
|
|
974
|
-
if self._overlaps:
|
|
1205
|
+
if bool(getattr(self.editor, "enable_collisions", True)) and self._overlaps:
|
|
975
1206
|
self.setPos(self._last_valid_pos)
|
|
976
1207
|
else:
|
|
977
1208
|
if self.pos() != self._start_pos:
|
|
@@ -1047,6 +1278,14 @@ class NodeItem(QGraphicsWidget):
|
|
|
1047
1278
|
if _qt_is_valid(self._overlay):
|
|
1048
1279
|
self._overlay.update()
|
|
1049
1280
|
return False
|
|
1281
|
+
|
|
1282
|
+
if obj is self._content and et == QEvent.MouseButtonPress:
|
|
1283
|
+
# Bring to front when clicking inside embedded editors (proxy widget area)
|
|
1284
|
+
try:
|
|
1285
|
+
self.editor.raise_node_to_top(self)
|
|
1286
|
+
except Exception:
|
|
1287
|
+
pass
|
|
1288
|
+
return False
|
|
1050
1289
|
except Exception:
|
|
1051
1290
|
pass
|
|
1052
1291
|
return False
|
|
@@ -1065,6 +1304,11 @@ class NodeItem(QGraphicsWidget):
|
|
|
1065
1304
|
if not getattr(self, "_dragging", False):
|
|
1066
1305
|
return value
|
|
1067
1306
|
|
|
1307
|
+
# When collisions are disabled, skip overlap checks entirely
|
|
1308
|
+
if not bool(getattr(self.editor, "enable_collisions", True)):
|
|
1309
|
+
self._overlaps = False
|
|
1310
|
+
return value
|
|
1311
|
+
|
|
1068
1312
|
sc = self.scene()
|
|
1069
1313
|
if sc is None or not _qt_is_valid(sc):
|
|
1070
1314
|
return value
|
|
@@ -1115,6 +1359,17 @@ class NodeItem(QGraphicsWidget):
|
|
|
1115
1359
|
pass
|
|
1116
1360
|
return super().itemChange(change, value)
|
|
1117
1361
|
|
|
1362
|
+
if change == QGraphicsItem.ItemSelectedHasChanged:
|
|
1363
|
+
# Bring newly selected node to front as well (e.g., rubber-band selection)
|
|
1364
|
+
try:
|
|
1365
|
+
ed = getattr(self, "editor", None)
|
|
1366
|
+
if ed and getattr(ed, "_alive", True) and not getattr(ed, "_closing", False):
|
|
1367
|
+
if self.isSelected():
|
|
1368
|
+
ed.raise_node_to_top(self)
|
|
1369
|
+
except Exception:
|
|
1370
|
+
pass
|
|
1371
|
+
return super().itemChange(change, value)
|
|
1372
|
+
|
|
1118
1373
|
return super().itemChange(change, value)
|
|
1119
1374
|
|
|
1120
1375
|
def contextMenuEvent(self, event):
|
|
@@ -1123,15 +1378,15 @@ class NodeItem(QGraphicsWidget):
|
|
|
1123
1378
|
ss = self.editor.window().styleSheet()
|
|
1124
1379
|
if ss:
|
|
1125
1380
|
menu.setStyleSheet(ss)
|
|
1126
|
-
act_rename = QAction(
|
|
1127
|
-
act_delete = QAction(
|
|
1381
|
+
act_rename = QAction(self.editor.config.node_context_rename(), menu)
|
|
1382
|
+
act_delete = QAction(self.editor.config.node_context_delete(), menu)
|
|
1128
1383
|
menu.addAction(act_rename)
|
|
1129
1384
|
menu.addSeparator()
|
|
1130
1385
|
menu.addAction(act_delete)
|
|
1131
1386
|
chosen = menu.exec(event.screenPos())
|
|
1132
1387
|
if chosen == act_rename:
|
|
1133
1388
|
from PySide6.QtWidgets import QInputDialog
|
|
1134
|
-
new_name, ok = QInputDialog.getText(self.editor.window(),
|
|
1389
|
+
new_name, ok = QInputDialog.getText(self.editor.window(), self.editor.config.rename_dialog_title(), self.editor.config.rename_dialog_label(), text=self.node.name)
|
|
1135
1390
|
if ok and new_name:
|
|
1136
1391
|
self.node.name = new_name
|
|
1137
1392
|
self.update()
|