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
|
@@ -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.26 10:00:00 #
|
|
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,
|
|
@@ -44,6 +44,9 @@ class SingleLineTextEdit(QTextEdit):
|
|
|
44
44
|
self.setTabChangesFocus(True)
|
|
45
45
|
self.setFrameStyle(QFrame.NoFrame)
|
|
46
46
|
self.document().setDocumentMargin(2)
|
|
47
|
+
# Enforce line-edit-like behavior with no context menu
|
|
48
|
+
# (widget menus are disabled by design; only node menu remains)
|
|
49
|
+
self.setContextMenuPolicy(Qt.NoContextMenu)
|
|
47
50
|
self._update_height()
|
|
48
51
|
|
|
49
52
|
def _update_height(self):
|
|
@@ -86,12 +89,20 @@ class SingleLineTextEdit(QTextEdit):
|
|
|
86
89
|
c.setPosition(min(pos, len(t2)))
|
|
87
90
|
self.setTextCursor(c)
|
|
88
91
|
|
|
92
|
+
def contextMenuEvent(self, e):
|
|
93
|
+
"""Widget-level context menu is intentionally disabled."""
|
|
94
|
+
try:
|
|
95
|
+
e.ignore()
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
return
|
|
99
|
+
|
|
89
100
|
|
|
90
101
|
class NodeContentWidget(QWidget):
|
|
91
102
|
"""Form-like widget that renders property editors for a node.
|
|
92
103
|
|
|
93
104
|
The widget builds appropriate Qt editors based on PropertyModel.type:
|
|
94
|
-
- "str": QLineEdit
|
|
105
|
+
- "str": QLineEdit-like (SingleLineTextEdit with placeholder support)
|
|
95
106
|
- "text": QTextEdit
|
|
96
107
|
- "int": QSpinBox
|
|
97
108
|
- "float": QDoubleSpinBox
|
|
@@ -103,6 +114,8 @@ class NodeContentWidget(QWidget):
|
|
|
103
114
|
|
|
104
115
|
Notes:
|
|
105
116
|
- For Base ID-like properties, the field is read-only; a composed display value is shown.
|
|
117
|
+
- Placeholder and description are applied when provided in the type spec or model
|
|
118
|
+
(description is shown as tooltip on both the editor and its label).
|
|
106
119
|
- ShortcutOverride is handled so typing in editors does not trigger scene shortcuts.
|
|
107
120
|
|
|
108
121
|
Signal:
|
|
@@ -125,7 +138,24 @@ class NodeContentWidget(QWidget):
|
|
|
125
138
|
self._editors: Dict[str, QWidget] = {}
|
|
126
139
|
for pid, pm in node.properties.items():
|
|
127
140
|
editable = self._editable_from_spec(pid, pm)
|
|
141
|
+
|
|
142
|
+
# Resolve UI hints: placeholder and description from spec (fallback to model)
|
|
143
|
+
placeholder = self._spec_text_attr(pid, "placeholder")
|
|
144
|
+
try:
|
|
145
|
+
if not placeholder and hasattr(pm, "placeholder") and getattr(pm, "placeholder") not in (None, ""):
|
|
146
|
+
placeholder = str(getattr(pm, "placeholder"))
|
|
147
|
+
except Exception:
|
|
148
|
+
pass
|
|
149
|
+
description = self._spec_text_attr(pid, "description")
|
|
150
|
+
try:
|
|
151
|
+
if not description and hasattr(pm, "description") and getattr(pm, "description") not in (None, ""):
|
|
152
|
+
description = str(getattr(pm, "description"))
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
155
|
+
|
|
128
156
|
w: QWidget
|
|
157
|
+
extra_tooltip: Optional[str] = None # used e.g. for capacity hints
|
|
158
|
+
|
|
129
159
|
if pm.type == "str":
|
|
130
160
|
te = SingleLineTextEdit()
|
|
131
161
|
te.setFocusPolicy(Qt.StrongFocus)
|
|
@@ -145,17 +175,31 @@ class NodeContentWidget(QWidget):
|
|
|
145
175
|
te.setReadOnly(not editable)
|
|
146
176
|
|
|
147
177
|
te.setPlainText(txt)
|
|
178
|
+
if placeholder:
|
|
179
|
+
try:
|
|
180
|
+
te.setPlaceholderText(placeholder)
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
148
183
|
te.textChanged.connect(lambda pid=pid, te=te: self.valueChanged.emit(pid, te.toPlainText()))
|
|
149
184
|
te.editingFinished.connect(lambda pid=pid, te=te: self.valueChanged.emit(pid, te.toPlainText()))
|
|
150
185
|
w = te
|
|
186
|
+
|
|
151
187
|
elif pm.type == "text":
|
|
152
188
|
te = QTextEdit()
|
|
153
189
|
te.setFocusPolicy(Qt.StrongFocus)
|
|
190
|
+
# Disable widget-level context menu completely (only node menu is available)
|
|
191
|
+
te.setContextMenuPolicy(Qt.NoContextMenu)
|
|
154
192
|
if pm.value is not None:
|
|
155
193
|
te.setPlainText(str(pm.value))
|
|
156
194
|
te.setReadOnly(not editable)
|
|
195
|
+
if placeholder:
|
|
196
|
+
try:
|
|
197
|
+
te.setPlaceholderText(placeholder)
|
|
198
|
+
except Exception:
|
|
199
|
+
pass
|
|
157
200
|
te.textChanged.connect(lambda pid=pid, te=te: self.valueChanged.emit(pid, te.toPlainText()))
|
|
158
201
|
w = te
|
|
202
|
+
|
|
159
203
|
elif pm.type == "int":
|
|
160
204
|
w = QSpinBox()
|
|
161
205
|
w.setFocusPolicy(Qt.StrongFocus)
|
|
@@ -163,7 +207,10 @@ class NodeContentWidget(QWidget):
|
|
|
163
207
|
if pm.value is not None:
|
|
164
208
|
w.setValue(int(pm.value))
|
|
165
209
|
w.setEnabled(editable)
|
|
210
|
+
|
|
211
|
+
# QSpinBox has no placeholder; use tooltip only
|
|
166
212
|
w.valueChanged.connect(lambda v, pid=pid: self.valueChanged.emit(pid, int(v)))
|
|
213
|
+
|
|
167
214
|
elif pm.type == "float":
|
|
168
215
|
w = QDoubleSpinBox()
|
|
169
216
|
w.setFocusPolicy(Qt.StrongFocus)
|
|
@@ -173,6 +220,7 @@ class NodeContentWidget(QWidget):
|
|
|
173
220
|
w.setValue(float(pm.value))
|
|
174
221
|
w.setEnabled(editable)
|
|
175
222
|
w.valueChanged.connect(lambda v, pid=pid: self.valueChanged.emit(pid, float(v)))
|
|
223
|
+
|
|
176
224
|
elif pm.type == "bool":
|
|
177
225
|
w = QCheckBox()
|
|
178
226
|
w.setFocusPolicy(Qt.StrongFocus)
|
|
@@ -180,6 +228,7 @@ class NodeContentWidget(QWidget):
|
|
|
180
228
|
w.setChecked(bool(pm.value))
|
|
181
229
|
w.setEnabled(editable)
|
|
182
230
|
w.toggled.connect(lambda v, pid=pid: self.valueChanged.emit(pid, bool(v)))
|
|
231
|
+
|
|
183
232
|
elif pm.type == "combo":
|
|
184
233
|
w = QComboBox()
|
|
185
234
|
w.setFocusPolicy(Qt.StrongFocus)
|
|
@@ -188,20 +237,41 @@ class NodeContentWidget(QWidget):
|
|
|
188
237
|
if pm.value is not None and pm.value in (pm.options or []):
|
|
189
238
|
w.setCurrentText(pm.value)
|
|
190
239
|
w.setEnabled(editable)
|
|
240
|
+
# QComboBox placeholder works when editable; we keep non-editable by default
|
|
191
241
|
w.currentTextChanged.connect(lambda v, pid=pid: self.valueChanged.emit(pid, v))
|
|
242
|
+
|
|
192
243
|
else:
|
|
193
244
|
# Render a read-only capacity placeholder for port-like properties ("input/output").
|
|
194
245
|
cap_text = self._capacity_text_for_property(pid, pm)
|
|
195
246
|
w = QLabel(cap_text)
|
|
196
247
|
w.setEnabled(False)
|
|
197
|
-
|
|
248
|
+
extra_tooltip = self.editor.config.port_capacity_tooltip(cap_text)
|
|
198
249
|
|
|
199
250
|
name = self._display_name_for_property(pid, pm)
|
|
200
251
|
if name == "Base ID":
|
|
201
|
-
name =
|
|
252
|
+
name = self.editor.config.label_id()
|
|
202
253
|
self.form.addRow(name, w)
|
|
203
254
|
self._editors[pid] = w
|
|
204
255
|
|
|
256
|
+
# Apply tooltip(s) after the row is created so we can also set the label tooltip
|
|
257
|
+
if description or extra_tooltip:
|
|
258
|
+
final_tt_parts: List[str] = []
|
|
259
|
+
if description:
|
|
260
|
+
final_tt_parts.append(description)
|
|
261
|
+
if extra_tooltip:
|
|
262
|
+
final_tt_parts.append(extra_tooltip)
|
|
263
|
+
final_tt = "\n".join(final_tt_parts)
|
|
264
|
+
try:
|
|
265
|
+
w.setToolTip(final_tt)
|
|
266
|
+
except Exception:
|
|
267
|
+
pass
|
|
268
|
+
try:
|
|
269
|
+
lbl = self.form.labelForField(w)
|
|
270
|
+
if lbl is not None:
|
|
271
|
+
lbl.setToolTip(description or "")
|
|
272
|
+
except Exception:
|
|
273
|
+
pass
|
|
274
|
+
|
|
205
275
|
def event(self, e):
|
|
206
276
|
"""
|
|
207
277
|
Filter ShortcutOverride so editor keystrokes are not eaten by the scene.
|
|
@@ -481,6 +551,55 @@ class NodeContentWidget(QWidget):
|
|
|
481
551
|
|
|
482
552
|
return None
|
|
483
553
|
|
|
554
|
+
# --- Spec helpers for UI hints ---
|
|
555
|
+
|
|
556
|
+
def _get_prop_spec(self, pid: str) -> Optional[Any]:
|
|
557
|
+
"""Return a property specification object from the registry for this node type."""
|
|
558
|
+
try:
|
|
559
|
+
reg = self.graph.registry
|
|
560
|
+
if not reg:
|
|
561
|
+
return None
|
|
562
|
+
type_spec = reg.get(self.node.type)
|
|
563
|
+
if not type_spec:
|
|
564
|
+
return None
|
|
565
|
+
|
|
566
|
+
for attr in ("properties", "props", "fields", "ports", "inputs", "outputs"):
|
|
567
|
+
try:
|
|
568
|
+
cont = getattr(type_spec, attr, None)
|
|
569
|
+
if isinstance(cont, dict) and pid in cont:
|
|
570
|
+
return cont[pid]
|
|
571
|
+
except Exception:
|
|
572
|
+
pass
|
|
573
|
+
for meth in ("get_property", "property_spec", "get_prop", "prop", "property"):
|
|
574
|
+
if hasattr(type_spec, meth):
|
|
575
|
+
try:
|
|
576
|
+
return getattr(type_spec, meth)(pid)
|
|
577
|
+
except Exception:
|
|
578
|
+
pass
|
|
579
|
+
except Exception:
|
|
580
|
+
pass
|
|
581
|
+
return None
|
|
582
|
+
|
|
583
|
+
def _spec_text_attr(self, pid: str, key: str) -> Optional[str]:
|
|
584
|
+
"""Get a string attribute (e.g., 'placeholder', 'description') from spec if available."""
|
|
585
|
+
obj = self._get_prop_spec(pid)
|
|
586
|
+
if obj is None:
|
|
587
|
+
return None
|
|
588
|
+
try:
|
|
589
|
+
v = getattr(obj, key, None)
|
|
590
|
+
if isinstance(v, str) and v != "":
|
|
591
|
+
return v
|
|
592
|
+
except Exception:
|
|
593
|
+
pass
|
|
594
|
+
try:
|
|
595
|
+
if isinstance(obj, dict):
|
|
596
|
+
v = obj.get(key)
|
|
597
|
+
if isinstance(v, str) and v != "":
|
|
598
|
+
return v
|
|
599
|
+
except Exception:
|
|
600
|
+
pass
|
|
601
|
+
return None
|
|
602
|
+
|
|
484
603
|
|
|
485
604
|
class NodeItem(QGraphicsWidget):
|
|
486
605
|
"""Rounded node with title and ports aligned to property rows.
|
|
@@ -613,7 +732,7 @@ class NodeItem(QGraphicsWidget):
|
|
|
613
732
|
def _effective_hit_margin(self) -> float:
|
|
614
733
|
"""Return the effective resize-grip 'hit' margin (visual margin minus inset)."""
|
|
615
734
|
margin = float(getattr(self.editor, "_resize_grip_margin", 12.0) or 12.0)
|
|
616
|
-
inset = float(getattr(self.editor, "_resize_grip_hit_inset",
|
|
735
|
+
inset = float(getattr(self.editor, "_resize_grip_hit_inset", 5.0) or 0.0)
|
|
617
736
|
hit = max(4.0, margin - inset)
|
|
618
737
|
return hit
|
|
619
738
|
|
|
@@ -854,6 +973,14 @@ class NodeItem(QGraphicsWidget):
|
|
|
854
973
|
if self._resizing:
|
|
855
974
|
return
|
|
856
975
|
|
|
976
|
+
# When global grab mode is active, suppress resize/move cursors
|
|
977
|
+
view = self.editor.view
|
|
978
|
+
if getattr(view, "_global_grab_mode", False):
|
|
979
|
+
self._hover_resize_mode = "none"
|
|
980
|
+
self.unsetCursor()
|
|
981
|
+
self.update()
|
|
982
|
+
return
|
|
983
|
+
|
|
857
984
|
mode = self._hit_resize_zone(pos)
|
|
858
985
|
self._hover_resize_mode = mode
|
|
859
986
|
|
|
@@ -891,9 +1018,9 @@ class NodeItem(QGraphicsWidget):
|
|
|
891
1018
|
self.update()
|
|
892
1019
|
|
|
893
1020
|
def hoverMoveEvent(self, event):
|
|
894
|
-
"""While panning is active, suppress resize hints; otherwise update hover state."""
|
|
1021
|
+
"""While panning is active or global grab is enabled, suppress resize hints; otherwise update hover state."""
|
|
895
1022
|
view = self.editor.view
|
|
896
|
-
if getattr(view, "_panning", False):
|
|
1023
|
+
if getattr(view, "_panning", False) or getattr(view, "_global_grab_mode", False):
|
|
897
1024
|
self._hover_resize_mode = "none"
|
|
898
1025
|
self.unsetCursor()
|
|
899
1026
|
self.update()
|
|
@@ -912,6 +1039,17 @@ class NodeItem(QGraphicsWidget):
|
|
|
912
1039
|
|
|
913
1040
|
def mousePressEvent(self, event):
|
|
914
1041
|
"""Start resize when pressing the proper zone; otherwise begin move drag."""
|
|
1042
|
+
# Always bring clicked node to front (dynamic z-order)
|
|
1043
|
+
try:
|
|
1044
|
+
self.editor.raise_node_to_top(self)
|
|
1045
|
+
except Exception:
|
|
1046
|
+
pass
|
|
1047
|
+
|
|
1048
|
+
# If global grab is active, do not initiate node drag/resize (view handles panning)
|
|
1049
|
+
if getattr(self.editor.view, "_global_grab_mode", False) and event.button() == Qt.LeftButton:
|
|
1050
|
+
event.ignore()
|
|
1051
|
+
return
|
|
1052
|
+
|
|
915
1053
|
if event.button() == Qt.LeftButton:
|
|
916
1054
|
mode = self._hit_resize_zone(event.pos())
|
|
917
1055
|
if mode != "none":
|
|
@@ -971,7 +1109,7 @@ class NodeItem(QGraphicsWidget):
|
|
|
971
1109
|
return
|
|
972
1110
|
|
|
973
1111
|
if self._dragging and event.button() == Qt.LeftButton:
|
|
974
|
-
if self._overlaps:
|
|
1112
|
+
if bool(getattr(self.editor, "enable_collisions", True)) and self._overlaps:
|
|
975
1113
|
self.setPos(self._last_valid_pos)
|
|
976
1114
|
else:
|
|
977
1115
|
if self.pos() != self._start_pos:
|
|
@@ -985,6 +1123,77 @@ class NodeItem(QGraphicsWidget):
|
|
|
985
1123
|
"""Filter events on proxy/content to keep hover/ports/overlay in sync."""
|
|
986
1124
|
et = e.type()
|
|
987
1125
|
try:
|
|
1126
|
+
# Forward RMB on the proxy (covers all embedded editors) to node menu
|
|
1127
|
+
if obj is self._proxy:
|
|
1128
|
+
if et == QEvent.GraphicsSceneMousePress:
|
|
1129
|
+
try:
|
|
1130
|
+
if e.button() == Qt.RightButton:
|
|
1131
|
+
# Use screenPos if available; fallback to view mapping
|
|
1132
|
+
gp = e.screenPos() if hasattr(e, "screenPos") else None
|
|
1133
|
+
if gp is None:
|
|
1134
|
+
sp = e.scenePos()
|
|
1135
|
+
vp = self.editor.view.mapFromScene(sp)
|
|
1136
|
+
gp = self.editor.view.viewport().mapToGlobal(vp)
|
|
1137
|
+
# Reuse node menu logic here for consistency
|
|
1138
|
+
menu = QMenu(self.editor.window())
|
|
1139
|
+
ss = self.editor.window().styleSheet()
|
|
1140
|
+
if ss:
|
|
1141
|
+
menu.setStyleSheet(ss)
|
|
1142
|
+
act_rename = QAction(self.editor.config.node_context_rename(), menu)
|
|
1143
|
+
act_delete = QAction(self.editor.config.node_context_delete(), menu)
|
|
1144
|
+
menu.addAction(act_rename)
|
|
1145
|
+
menu.addSeparator()
|
|
1146
|
+
menu.addAction(act_delete)
|
|
1147
|
+
chosen = menu.exec(gp.toPoint() if hasattr(gp, "toPoint") else gp)
|
|
1148
|
+
if chosen == act_rename:
|
|
1149
|
+
from PySide6.QtWidgets import QInputDialog
|
|
1150
|
+
new_name, ok = QInputDialog.getText(
|
|
1151
|
+
self.editor.window(),
|
|
1152
|
+
self.editor.config.rename_dialog_title(),
|
|
1153
|
+
self.editor.config.rename_dialog_label(),
|
|
1154
|
+
text=self.node.name
|
|
1155
|
+
)
|
|
1156
|
+
if ok and new_name:
|
|
1157
|
+
self.node.name = new_name
|
|
1158
|
+
self.update()
|
|
1159
|
+
elif chosen == act_delete:
|
|
1160
|
+
self.editor._delete_node_item(self)
|
|
1161
|
+
e.accept()
|
|
1162
|
+
return True
|
|
1163
|
+
except Exception:
|
|
1164
|
+
pass
|
|
1165
|
+
|
|
1166
|
+
if et == QEvent.GraphicsSceneContextMenu:
|
|
1167
|
+
try:
|
|
1168
|
+
gp = e.screenPos() if hasattr(e, "screenPos") else None
|
|
1169
|
+
menu = QMenu(self.editor.window())
|
|
1170
|
+
ss = self.editor.window().styleSheet()
|
|
1171
|
+
if ss:
|
|
1172
|
+
menu.setStyleSheet(ss)
|
|
1173
|
+
act_rename = QAction(self.editor.config.node_context_rename(), menu)
|
|
1174
|
+
act_delete = QAction(self.editor.config.node_context_delete(), menu)
|
|
1175
|
+
menu.addAction(act_rename)
|
|
1176
|
+
menu.addSeparator()
|
|
1177
|
+
menu.addAction(act_delete)
|
|
1178
|
+
chosen = menu.exec(gp)
|
|
1179
|
+
if chosen == act_rename:
|
|
1180
|
+
from PySide6.QtWidgets import QInputDialog
|
|
1181
|
+
new_name, ok = QInputDialog.getText(
|
|
1182
|
+
self.editor.window(),
|
|
1183
|
+
self.editor.config.rename_dialog_title(),
|
|
1184
|
+
self.editor.config.rename_dialog_label(),
|
|
1185
|
+
text=self.node.name
|
|
1186
|
+
)
|
|
1187
|
+
if ok and new_name:
|
|
1188
|
+
self.node.name = new_name
|
|
1189
|
+
self.update()
|
|
1190
|
+
elif chosen == act_delete:
|
|
1191
|
+
self.editor._delete_node_item(self)
|
|
1192
|
+
e.accept()
|
|
1193
|
+
return True
|
|
1194
|
+
except Exception:
|
|
1195
|
+
pass
|
|
1196
|
+
|
|
988
1197
|
if obj is self._proxy and et in (QEvent.GraphicsSceneMouseMove, QEvent.GraphicsSceneHoverMove):
|
|
989
1198
|
local = self.mapFromScene(e.scenePos())
|
|
990
1199
|
self._apply_hover_from_pos(local)
|
|
@@ -1047,6 +1256,14 @@ class NodeItem(QGraphicsWidget):
|
|
|
1047
1256
|
if _qt_is_valid(self._overlay):
|
|
1048
1257
|
self._overlay.update()
|
|
1049
1258
|
return False
|
|
1259
|
+
|
|
1260
|
+
if obj is self._content and et == QEvent.MouseButtonPress:
|
|
1261
|
+
# Bring to front when clicking inside embedded editors (proxy widget area)
|
|
1262
|
+
try:
|
|
1263
|
+
self.editor.raise_node_to_top(self)
|
|
1264
|
+
except Exception:
|
|
1265
|
+
pass
|
|
1266
|
+
return False
|
|
1050
1267
|
except Exception:
|
|
1051
1268
|
pass
|
|
1052
1269
|
return False
|
|
@@ -1065,6 +1282,11 @@ class NodeItem(QGraphicsWidget):
|
|
|
1065
1282
|
if not getattr(self, "_dragging", False):
|
|
1066
1283
|
return value
|
|
1067
1284
|
|
|
1285
|
+
# When collisions are disabled, skip overlap checks entirely
|
|
1286
|
+
if not bool(getattr(self.editor, "enable_collisions", True)):
|
|
1287
|
+
self._overlaps = False
|
|
1288
|
+
return value
|
|
1289
|
+
|
|
1068
1290
|
sc = self.scene()
|
|
1069
1291
|
if sc is None or not _qt_is_valid(sc):
|
|
1070
1292
|
return value
|
|
@@ -1115,6 +1337,17 @@ class NodeItem(QGraphicsWidget):
|
|
|
1115
1337
|
pass
|
|
1116
1338
|
return super().itemChange(change, value)
|
|
1117
1339
|
|
|
1340
|
+
if change == QGraphicsItem.ItemSelectedHasChanged:
|
|
1341
|
+
# Bring newly selected node to front as well (e.g., rubber-band selection)
|
|
1342
|
+
try:
|
|
1343
|
+
ed = getattr(self, "editor", None)
|
|
1344
|
+
if ed and getattr(ed, "_alive", True) and not getattr(ed, "_closing", False):
|
|
1345
|
+
if self.isSelected():
|
|
1346
|
+
ed.raise_node_to_top(self)
|
|
1347
|
+
except Exception:
|
|
1348
|
+
pass
|
|
1349
|
+
return super().itemChange(change, value)
|
|
1350
|
+
|
|
1118
1351
|
return super().itemChange(change, value)
|
|
1119
1352
|
|
|
1120
1353
|
def contextMenuEvent(self, event):
|
|
@@ -1123,15 +1356,15 @@ class NodeItem(QGraphicsWidget):
|
|
|
1123
1356
|
ss = self.editor.window().styleSheet()
|
|
1124
1357
|
if ss:
|
|
1125
1358
|
menu.setStyleSheet(ss)
|
|
1126
|
-
act_rename = QAction(
|
|
1127
|
-
act_delete = QAction(
|
|
1359
|
+
act_rename = QAction(self.editor.config.node_context_rename(), menu)
|
|
1360
|
+
act_delete = QAction(self.editor.config.node_context_delete(), menu)
|
|
1128
1361
|
menu.addAction(act_rename)
|
|
1129
1362
|
menu.addSeparator()
|
|
1130
1363
|
menu.addAction(act_delete)
|
|
1131
1364
|
chosen = menu.exec(event.screenPos())
|
|
1132
1365
|
if chosen == act_rename:
|
|
1133
1366
|
from PySide6.QtWidgets import QInputDialog
|
|
1134
|
-
new_name, ok = QInputDialog.getText(self.editor.window(),
|
|
1367
|
+
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
1368
|
if ok and new_name:
|
|
1136
1369
|
self.node.name = new_name
|
|
1137
1370
|
self.update()
|