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.
Files changed (87) hide show
  1. pygpt_net/CHANGELOG.txt +14 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/chat/common.py +115 -6
  4. pygpt_net/controller/chat/input.py +4 -1
  5. pygpt_net/controller/chat/response.py +8 -2
  6. pygpt_net/controller/presets/presets.py +121 -6
  7. pygpt_net/controller/settings/editor.py +0 -15
  8. pygpt_net/controller/settings/profile.py +16 -4
  9. pygpt_net/controller/settings/workdir.py +30 -5
  10. pygpt_net/controller/theme/common.py +4 -2
  11. pygpt_net/controller/theme/markdown.py +4 -7
  12. pygpt_net/controller/theme/theme.py +2 -1
  13. pygpt_net/controller/ui/ui.py +32 -7
  14. pygpt_net/core/agents/custom/__init__.py +7 -1
  15. pygpt_net/core/agents/custom/llama_index/factory.py +17 -6
  16. pygpt_net/core/agents/custom/llama_index/runner.py +52 -4
  17. pygpt_net/core/agents/custom/llama_index/utils.py +12 -1
  18. pygpt_net/core/agents/custom/router.py +45 -6
  19. pygpt_net/core/agents/custom/runner.py +11 -5
  20. pygpt_net/core/agents/custom/schema.py +3 -1
  21. pygpt_net/core/agents/custom/utils.py +13 -1
  22. pygpt_net/core/agents/runners/llama_workflow.py +65 -5
  23. pygpt_net/core/agents/runners/openai_workflow.py +2 -1
  24. pygpt_net/core/db/viewer.py +11 -5
  25. pygpt_net/core/node_editor/graph.py +18 -9
  26. pygpt_net/core/node_editor/models.py +9 -2
  27. pygpt_net/core/node_editor/types.py +15 -1
  28. pygpt_net/core/presets/presets.py +216 -29
  29. pygpt_net/core/render/markdown/parser.py +0 -2
  30. pygpt_net/core/render/web/renderer.py +76 -11
  31. pygpt_net/data/config/config.json +5 -6
  32. pygpt_net/data/config/models.json +3 -3
  33. pygpt_net/data/config/settings.json +2 -38
  34. pygpt_net/data/css/style.dark.css +18 -0
  35. pygpt_net/data/css/style.light.css +20 -1
  36. pygpt_net/data/locale/locale.de.ini +66 -1
  37. pygpt_net/data/locale/locale.en.ini +64 -3
  38. pygpt_net/data/locale/locale.es.ini +66 -1
  39. pygpt_net/data/locale/locale.fr.ini +66 -1
  40. pygpt_net/data/locale/locale.it.ini +66 -1
  41. pygpt_net/data/locale/locale.pl.ini +67 -2
  42. pygpt_net/data/locale/locale.uk.ini +66 -1
  43. pygpt_net/data/locale/locale.zh.ini +66 -1
  44. pygpt_net/data/locale/plugin.cmd_system.en.ini +62 -66
  45. pygpt_net/item/ctx.py +23 -1
  46. pygpt_net/provider/agents/llama_index/flow_from_schema.py +2 -2
  47. pygpt_net/provider/agents/llama_index/workflow/codeact.py +9 -6
  48. pygpt_net/provider/agents/llama_index/workflow/openai.py +38 -11
  49. pygpt_net/provider/agents/llama_index/workflow/planner.py +36 -16
  50. pygpt_net/provider/agents/llama_index/workflow/supervisor.py +60 -10
  51. pygpt_net/provider/agents/openai/agent.py +3 -1
  52. pygpt_net/provider/agents/openai/agent_b2b.py +13 -9
  53. pygpt_net/provider/agents/openai/agent_planner.py +6 -2
  54. pygpt_net/provider/agents/openai/agent_with_experts.py +4 -1
  55. pygpt_net/provider/agents/openai/agent_with_experts_feedback.py +4 -2
  56. pygpt_net/provider/agents/openai/agent_with_feedback.py +4 -2
  57. pygpt_net/provider/agents/openai/evolve.py +6 -2
  58. pygpt_net/provider/agents/openai/supervisor.py +3 -1
  59. pygpt_net/provider/api/openai/agents/response.py +1 -0
  60. pygpt_net/provider/core/config/patch.py +18 -1
  61. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +0 -6
  62. pygpt_net/tools/agent_builder/tool.py +48 -26
  63. pygpt_net/tools/agent_builder/ui/dialogs.py +36 -28
  64. pygpt_net/ui/__init__.py +2 -4
  65. pygpt_net/ui/dialog/about.py +58 -38
  66. pygpt_net/ui/dialog/db.py +142 -3
  67. pygpt_net/ui/dialog/preset.py +47 -8
  68. pygpt_net/ui/layout/toolbox/presets.py +64 -16
  69. pygpt_net/ui/main.py +2 -2
  70. pygpt_net/ui/widget/dialog/confirm.py +27 -3
  71. pygpt_net/ui/widget/dialog/db.py +0 -0
  72. pygpt_net/ui/widget/draw/painter.py +90 -1
  73. pygpt_net/ui/widget/lists/preset.py +908 -60
  74. pygpt_net/ui/widget/node_editor/command.py +10 -10
  75. pygpt_net/ui/widget/node_editor/config.py +157 -0
  76. pygpt_net/ui/widget/node_editor/editor.py +223 -153
  77. pygpt_net/ui/widget/node_editor/item.py +12 -11
  78. pygpt_net/ui/widget/node_editor/node.py +246 -13
  79. pygpt_net/ui/widget/node_editor/view.py +179 -63
  80. pygpt_net/ui/widget/tabs/output.py +1 -1
  81. pygpt_net/ui/widget/textarea/input.py +157 -23
  82. pygpt_net/utils.py +114 -2
  83. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.62.dist-info}/METADATA +26 -100
  84. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.62.dist-info}/RECORD +86 -85
  85. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.62.dist-info}/LICENSE +0 -0
  86. {pygpt_net-2.6.60.dist-info → pygpt_net-2.6.62.dist-info}/WHEEL +0 -0
  87. {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.24 00:00:00 #
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 = "unlimited ()"
146
+ cap_str = cfg.cap_unlimited()
146
147
  else:
147
148
  cap_str = str(cap_val)
148
149
  else:
149
- cap_str = "n/a"
150
- tip = (
151
- f"Node: {node_name}\n"
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("Delete connection", menu)
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.24 00:00:00 #
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
- w.setToolTip(f"Allowed connections (IN/OUT): {cap_text}")
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 = "ID"
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", 3.0) or 0.0)
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("Rename", menu)
1127
- act_delete = QAction("Delete", menu)
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(), "Rename Node", "Name:", text=self.node.name)
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()