pygpt-net 2.6.61__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 +7 -0
- pygpt_net/__init__.py +1 -1
- pygpt_net/controller/chat/response.py +8 -2
- 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 +2 -2
- pygpt_net/controller/theme/theme.py +2 -1
- pygpt_net/controller/ui/ui.py +31 -3
- pygpt_net/core/agents/custom/llama_index/runner.py +18 -3
- pygpt_net/core/agents/custom/runner.py +10 -5
- pygpt_net/core/agents/runners/llama_workflow.py +65 -5
- pygpt_net/core/agents/runners/openai_workflow.py +2 -1
- pygpt_net/core/node_editor/types.py +13 -1
- pygpt_net/core/render/web/renderer.py +76 -11
- pygpt_net/data/config/config.json +2 -2
- pygpt_net/data/config/models.json +2 -2
- 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 +2 -0
- pygpt_net/data/locale/locale.en.ini +2 -0
- pygpt_net/data/locale/locale.es.ini +2 -0
- pygpt_net/data/locale/locale.fr.ini +2 -0
- pygpt_net/data/locale/locale.it.ini +2 -0
- pygpt_net/data/locale/locale.pl.ini +3 -1
- pygpt_net/data/locale/locale.uk.ini +2 -0
- pygpt_net/data/locale/locale.zh.ini +2 -0
- pygpt_net/item/ctx.py +23 -1
- 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 +8 -0
- pygpt_net/tools/agent_builder/tool.py +6 -0
- pygpt_net/tools/agent_builder/ui/dialogs.py +0 -41
- pygpt_net/ui/layout/toolbox/presets.py +14 -2
- pygpt_net/ui/main.py +2 -2
- pygpt_net/ui/widget/dialog/confirm.py +27 -3
- pygpt_net/ui/widget/draw/painter.py +90 -1
- pygpt_net/ui/widget/lists/preset.py +289 -25
- pygpt_net/ui/widget/node_editor/editor.py +53 -15
- pygpt_net/ui/widget/node_editor/node.py +82 -104
- pygpt_net/ui/widget/node_editor/view.py +4 -5
- pygpt_net/ui/widget/textarea/input.py +155 -21
- {pygpt_net-2.6.61.dist-info → pygpt_net-2.6.62.dist-info}/METADATA +17 -8
- {pygpt_net-2.6.61.dist-info → pygpt_net-2.6.62.dist-info}/RECORD +58 -58
- {pygpt_net-2.6.61.dist-info → pygpt_net-2.6.62.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.61.dist-info → pygpt_net-2.6.62.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.61.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.26
|
|
9
|
+
# Updated Date: 2025.09.26 12:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtCore import QPoint, QItemSelectionModel, Qt, QEventLoop, QTimer, QMimeData
|
|
@@ -65,6 +65,7 @@ class PresetList(BaseList):
|
|
|
65
65
|
self.setDragDropMode(QAbstractItemView.NoDragDrop) # switched dynamically
|
|
66
66
|
self.setDefaultDropAction(Qt.MoveAction)
|
|
67
67
|
self.setDragDropOverwriteMode(False)
|
|
68
|
+
# We use our own visual indicator for drop position
|
|
68
69
|
self.setDropIndicatorShown(False)
|
|
69
70
|
|
|
70
71
|
self._press_pos = None
|
|
@@ -92,6 +93,18 @@ class PresetList(BaseList):
|
|
|
92
93
|
# One-shot forced selection after refresh (list of ROLE_ID)
|
|
93
94
|
self._selection_override_ids = None
|
|
94
95
|
|
|
96
|
+
# Custom drop indicator (visual only)
|
|
97
|
+
self._drop_indicator_active = False
|
|
98
|
+
# seam row for indicator (row under which the line is drawn)
|
|
99
|
+
self._drop_indicator_to_row = -1
|
|
100
|
+
self._drop_indicator_padding = 6 # visual left/right padding
|
|
101
|
+
|
|
102
|
+
# Short-lived scroll freeze to prevent jumps during click-triggered model refresh
|
|
103
|
+
self._scroll_freeze_depth = 0
|
|
104
|
+
self._scroll_freeze_timer = None
|
|
105
|
+
self._pending_scroll_value = None
|
|
106
|
+
self._pending_refocus_role_id = None
|
|
107
|
+
|
|
95
108
|
# -------- Public helpers to protect updates --------
|
|
96
109
|
|
|
97
110
|
def begin_model_update(self):
|
|
@@ -103,26 +116,135 @@ class PresetList(BaseList):
|
|
|
103
116
|
"""Re-enable interaction after model/view rebuild is complete."""
|
|
104
117
|
self.setEnabled(True)
|
|
105
118
|
self._model_updating = False
|
|
119
|
+
# If there is a pending scroll/selection stabilization, apply it right after update
|
|
120
|
+
self._apply_pending_scroll()
|
|
121
|
+
self._apply_pending_refocus()
|
|
122
|
+
QTimer.singleShot(0, self._apply_pending_scroll)
|
|
123
|
+
QTimer.singleShot(0, self._apply_pending_refocus)
|
|
124
|
+
# Unfreeze shortly after everything settled in the event loop
|
|
125
|
+
QTimer.singleShot(50, self._unfreeze_scroll)
|
|
106
126
|
|
|
107
127
|
# ---------------------------------------------------
|
|
108
128
|
|
|
129
|
+
# -------- Scroll freeze helpers (prevent accidental jumps on click) --------
|
|
130
|
+
|
|
131
|
+
def _freeze_scroll(self, ms: int = 250):
|
|
132
|
+
"""
|
|
133
|
+
Freeze scrollTo() effects for a very short time and keep current scroll value.
|
|
134
|
+
This avoids jumps caused by programmatic scroll during selection/refresh.
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
sb = self.verticalScrollBar()
|
|
138
|
+
except Exception:
|
|
139
|
+
sb = None
|
|
140
|
+
if sb is not None:
|
|
141
|
+
self._pending_scroll_value = sb.value()
|
|
142
|
+
self._scroll_freeze_depth += 1
|
|
143
|
+
|
|
144
|
+
# Apply stabilization now and on next frame(s)
|
|
145
|
+
QTimer.singleShot(0, self._apply_pending_scroll)
|
|
146
|
+
QTimer.singleShot(16, self._apply_pending_scroll)
|
|
147
|
+
|
|
148
|
+
# Auto-unfreeze after given duration
|
|
149
|
+
if self._scroll_freeze_timer:
|
|
150
|
+
try:
|
|
151
|
+
self._scroll_freeze_timer.stop()
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
self._scroll_freeze_timer = QTimer(self)
|
|
155
|
+
self._scroll_freeze_timer.setSingleShot(True)
|
|
156
|
+
self._scroll_freeze_timer.timeout.connect(self._unfreeze_scroll)
|
|
157
|
+
self._scroll_freeze_timer.start(max(50, int(ms)))
|
|
158
|
+
|
|
159
|
+
def _apply_pending_scroll(self):
|
|
160
|
+
"""Re-apply saved scroll position when frozen."""
|
|
161
|
+
if self._pending_scroll_value is None:
|
|
162
|
+
return
|
|
163
|
+
try:
|
|
164
|
+
sb = self.verticalScrollBar()
|
|
165
|
+
except Exception:
|
|
166
|
+
sb = None
|
|
167
|
+
if sb is not None:
|
|
168
|
+
sb.setValue(self._pending_scroll_value)
|
|
169
|
+
|
|
170
|
+
def _unfreeze_scroll(self):
|
|
171
|
+
"""Release the temporary scroll freeze."""
|
|
172
|
+
if self._scroll_freeze_depth > 0:
|
|
173
|
+
self._scroll_freeze_depth -= 1
|
|
174
|
+
if self._scroll_freeze_depth <= 0:
|
|
175
|
+
self._scroll_freeze_depth = 0
|
|
176
|
+
self._pending_scroll_value = None
|
|
177
|
+
|
|
178
|
+
def scrollTo(self, index, hint=QAbstractItemView.EnsureVisible):
|
|
179
|
+
"""
|
|
180
|
+
Temporarily suppress automatic scrolling while frozen.
|
|
181
|
+
This prevents list jumping when selection triggers scrollTo during refresh.
|
|
182
|
+
"""
|
|
183
|
+
if self._scroll_freeze_depth > 0:
|
|
184
|
+
self._apply_pending_scroll()
|
|
185
|
+
return
|
|
186
|
+
return super().scrollTo(index, hint)
|
|
187
|
+
|
|
188
|
+
def _apply_pending_refocus(self):
|
|
189
|
+
"""
|
|
190
|
+
Ensure selection stays on the intended item (by ROLE_ID) after a model refresh.
|
|
191
|
+
Does not force scrolling when scroll is frozen.
|
|
192
|
+
"""
|
|
193
|
+
pid = self._pending_refocus_role_id
|
|
194
|
+
if not pid:
|
|
195
|
+
return
|
|
196
|
+
model = self.model()
|
|
197
|
+
if model is None:
|
|
198
|
+
return
|
|
199
|
+
target_idx = None
|
|
200
|
+
try:
|
|
201
|
+
for r in range(model.rowCount()):
|
|
202
|
+
ix = model.index(r, 0)
|
|
203
|
+
if ix.data(self.ROLE_ID) == pid:
|
|
204
|
+
target_idx = ix
|
|
205
|
+
break
|
|
206
|
+
except Exception:
|
|
207
|
+
target_idx = None
|
|
208
|
+
|
|
209
|
+
if target_idx is not None and target_idx.isValid():
|
|
210
|
+
try:
|
|
211
|
+
sel_model = self.selectionModel()
|
|
212
|
+
if sel_model:
|
|
213
|
+
prev_unlocked = getattr(self, "unlocked", True)
|
|
214
|
+
self.unlocked = True
|
|
215
|
+
try:
|
|
216
|
+
sel_model.clearSelection()
|
|
217
|
+
sel_model.select(target_idx, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
|
|
218
|
+
self.setCurrentIndex(target_idx)
|
|
219
|
+
finally:
|
|
220
|
+
self.unlocked = prev_unlocked
|
|
221
|
+
# If refocus succeeded, clear the pending marker
|
|
222
|
+
self._pending_refocus_role_id = None
|
|
223
|
+
except Exception:
|
|
224
|
+
# Keep pending id for next attempt if apply failed
|
|
225
|
+
pass
|
|
226
|
+
|
|
227
|
+
# --------------------------------------------------------------------------
|
|
228
|
+
|
|
109
229
|
def set_dnd_enabled(self, enabled: bool):
|
|
110
230
|
"""
|
|
111
231
|
Toggle DnD behaviour at runtime.
|
|
112
232
|
Using DragDrop (not InternalMove) to avoid implicit Qt reordering.
|
|
233
|
+
We also disable the native drop indicator and render our own line.
|
|
113
234
|
"""
|
|
114
235
|
self._dnd_enabled = bool(enabled)
|
|
115
236
|
if self._dnd_enabled:
|
|
116
237
|
self.setDragEnabled(True)
|
|
117
238
|
self.setAcceptDrops(True)
|
|
118
239
|
self.setDragDropMode(QAbstractItemView.DragDrop)
|
|
119
|
-
self.setDropIndicatorShown(
|
|
240
|
+
self.setDropIndicatorShown(False) # use custom indicator
|
|
120
241
|
else:
|
|
121
242
|
self.setDragEnabled(False)
|
|
122
243
|
self.setAcceptDrops(False)
|
|
123
244
|
self.setDragDropMode(QAbstractItemView.NoDragDrop)
|
|
124
245
|
self.setDropIndicatorShown(False)
|
|
125
246
|
self.unsetCursor()
|
|
247
|
+
self._clear_drop_indicator() # ensure clean state
|
|
126
248
|
|
|
127
249
|
def backup_selection(self):
|
|
128
250
|
"""
|
|
@@ -186,12 +308,21 @@ class PresetList(BaseList):
|
|
|
186
308
|
return
|
|
187
309
|
preset_id = index.data(self.ROLE_ID)
|
|
188
310
|
if preset_id:
|
|
311
|
+
# Freeze scroll and remember the intended selection to re-apply after any refresh
|
|
312
|
+
self._freeze_scroll(300)
|
|
313
|
+
self._pending_refocus_role_id = preset_id
|
|
189
314
|
self.window.controller.presets.select_by_id(preset_id)
|
|
315
|
+
# Re-apply selection in next ticks to win races with late refresh
|
|
316
|
+
QTimer.singleShot(0, self._apply_pending_refocus)
|
|
317
|
+
QTimer.singleShot(50, self._apply_pending_refocus)
|
|
190
318
|
self.selection = self.selectionModel().selection()
|
|
191
319
|
return
|
|
192
320
|
row = index.row()
|
|
193
321
|
if row >= 0:
|
|
322
|
+
self._freeze_scroll(300)
|
|
194
323
|
self.window.controller.presets.select(row)
|
|
324
|
+
QTimer.singleShot(0, self._apply_pending_refocus)
|
|
325
|
+
QTimer.singleShot(50, self._apply_pending_refocus)
|
|
195
326
|
self.selection = self.selectionModel().selection()
|
|
196
327
|
|
|
197
328
|
def dblclick(self, val):
|
|
@@ -504,16 +635,13 @@ class PresetList(BaseList):
|
|
|
504
635
|
|
|
505
636
|
self.store_scroll_position()
|
|
506
637
|
|
|
507
|
-
|
|
508
|
-
self.setDropIndicatorShown(False)
|
|
638
|
+
# Use custom indicator only; do not re-enable native one here
|
|
509
639
|
self.setUpdatesEnabled(False)
|
|
510
640
|
try:
|
|
511
641
|
self.window.controller.presets.update_list()
|
|
512
642
|
self.restore_scroll_position()
|
|
513
643
|
finally:
|
|
514
644
|
self.setUpdatesEnabled(True)
|
|
515
|
-
if di_prev and self._dnd_enabled:
|
|
516
|
-
self.setDropIndicatorShown(True)
|
|
517
645
|
|
|
518
646
|
# Clear helpers for context menu (layout will consume _selection_override_ids)
|
|
519
647
|
self._ctx_menu_original_ids = None
|
|
@@ -556,6 +684,136 @@ class PresetList(BaseList):
|
|
|
556
684
|
moved_id = self._reorder_and_persist(from_row, to_row)
|
|
557
685
|
self._refresh_after_order_change(moved_id, follow_selection=False)
|
|
558
686
|
|
|
687
|
+
# --- Custom drop indicator helpers ---
|
|
688
|
+
|
|
689
|
+
def _compute_drop_locations(self, pos: QPoint) -> tuple[int, int]:
|
|
690
|
+
"""
|
|
691
|
+
Compute both:
|
|
692
|
+
- to_row_drop: final insertion row used for reordering (after 'moving-down' adjustment),
|
|
693
|
+
- seam_row: row under which the visual indicator line should be drawn
|
|
694
|
+
in the current (pre-drop) view geometry.
|
|
695
|
+
|
|
696
|
+
This keeps visuals and the final insertion point perfectly aligned.
|
|
697
|
+
|
|
698
|
+
Returns: (to_row_drop, seam_row)
|
|
699
|
+
"""
|
|
700
|
+
model = self.model()
|
|
701
|
+
if model is None:
|
|
702
|
+
return -1, -1
|
|
703
|
+
|
|
704
|
+
idx = self.indexAt(pos)
|
|
705
|
+
|
|
706
|
+
beyond_last = False
|
|
707
|
+
if not idx.isValid():
|
|
708
|
+
to_row_raw = model.rowCount() # append at the end
|
|
709
|
+
if model.rowCount() > 0:
|
|
710
|
+
last_idx = model.index(model.rowCount() - 1, 0)
|
|
711
|
+
last_rect = self.visualRect(last_idx)
|
|
712
|
+
if last_rect.isValid() and pos.y() > last_rect.bottom():
|
|
713
|
+
beyond_last = True
|
|
714
|
+
else:
|
|
715
|
+
rect = self.visualRect(idx)
|
|
716
|
+
to_row_raw = idx.row() + (1 if pos.y() > rect.center().y() else 0)
|
|
717
|
+
|
|
718
|
+
# Keep first row pinned (cannot insert above row 1)
|
|
719
|
+
if to_row_raw <= 1:
|
|
720
|
+
to_row_raw = 1
|
|
721
|
+
|
|
722
|
+
# seam row is always the boundary under the row at (to_row_raw - 1),
|
|
723
|
+
# except in explicit "beyond last" zone where we draw under the last row.
|
|
724
|
+
if model.rowCount() > 0:
|
|
725
|
+
if beyond_last:
|
|
726
|
+
seam_row = model.rowCount() - 1
|
|
727
|
+
else:
|
|
728
|
+
seam_row = max(0, min(model.rowCount() - 1, to_row_raw - 1))
|
|
729
|
+
else:
|
|
730
|
+
seam_row = -1
|
|
731
|
+
|
|
732
|
+
# Apply 'moving down' adjustment only to the logical insertion row,
|
|
733
|
+
# never to the visual seam (otherwise the line jumps one row up).
|
|
734
|
+
from_row = self._press_index.row() if (self._press_index and self._press_index.isValid()) else -1
|
|
735
|
+
to_row_drop = to_row_raw
|
|
736
|
+
if from_row >= 0 and to_row_raw > from_row and not beyond_last:
|
|
737
|
+
to_row_drop -= 1
|
|
738
|
+
|
|
739
|
+
# Clamp to valid ranges
|
|
740
|
+
to_row_drop = max(1, min(model.rowCount(), to_row_drop))
|
|
741
|
+
if seam_row >= 0:
|
|
742
|
+
seam_row = max(0, min(model.rowCount() - 1, seam_row))
|
|
743
|
+
|
|
744
|
+
return to_row_drop, seam_row
|
|
745
|
+
|
|
746
|
+
def _update_drop_indicator_from_pos(self, pos: QPoint):
|
|
747
|
+
"""
|
|
748
|
+
Update custom drop indicator state based on cursor position.
|
|
749
|
+
Draws a single horizontal line under the row where the item will land.
|
|
750
|
+
"""
|
|
751
|
+
if not self._dnd_enabled or self._model_updating:
|
|
752
|
+
self._clear_drop_indicator()
|
|
753
|
+
return
|
|
754
|
+
|
|
755
|
+
model = self.model()
|
|
756
|
+
if model is None or model.rowCount() <= 0:
|
|
757
|
+
self._clear_drop_indicator()
|
|
758
|
+
return
|
|
759
|
+
|
|
760
|
+
_, seam_row = self._compute_drop_locations(pos)
|
|
761
|
+
if seam_row < 0:
|
|
762
|
+
self._clear_drop_indicator()
|
|
763
|
+
return
|
|
764
|
+
|
|
765
|
+
if not self._drop_indicator_active or self._drop_indicator_to_row != seam_row:
|
|
766
|
+
self._drop_indicator_active = True
|
|
767
|
+
self._drop_indicator_to_row = seam_row
|
|
768
|
+
self.viewport().update()
|
|
769
|
+
|
|
770
|
+
def _clear_drop_indicator(self):
|
|
771
|
+
"""Hide custom drop indicator."""
|
|
772
|
+
if self._drop_indicator_active or self._drop_indicator_to_row != -1:
|
|
773
|
+
self._drop_indicator_active = False
|
|
774
|
+
self._drop_indicator_to_row = -1
|
|
775
|
+
if self.viewport():
|
|
776
|
+
self.viewport().update()
|
|
777
|
+
|
|
778
|
+
def paintEvent(self, event):
|
|
779
|
+
"""
|
|
780
|
+
Standard paint + overlay a clear drop indicator line at the computed insertion position.
|
|
781
|
+
"""
|
|
782
|
+
super().paintEvent(event)
|
|
783
|
+
|
|
784
|
+
if not self._drop_indicator_active or not self._dnd_enabled:
|
|
785
|
+
return
|
|
786
|
+
|
|
787
|
+
model = self.model()
|
|
788
|
+
if model is None or model.rowCount() <= 0:
|
|
789
|
+
return
|
|
790
|
+
|
|
791
|
+
seam_row = self._drop_indicator_to_row
|
|
792
|
+
if seam_row < 0 or seam_row >= model.rowCount():
|
|
793
|
+
return
|
|
794
|
+
|
|
795
|
+
idx = model.index(seam_row, 0)
|
|
796
|
+
rect = self.visualRect(idx)
|
|
797
|
+
if not rect.isValid() or rect.height() <= 0:
|
|
798
|
+
return
|
|
799
|
+
|
|
800
|
+
# Line under the seam row
|
|
801
|
+
y = rect.bottom()
|
|
802
|
+
x1 = self._drop_indicator_padding
|
|
803
|
+
x2 = self.viewport().width() - self._drop_indicator_padding
|
|
804
|
+
|
|
805
|
+
painter = QPainter(self.viewport())
|
|
806
|
+
try:
|
|
807
|
+
# Use highlight color with good contrast; 1px thickness
|
|
808
|
+
color = self.palette().highlight().color()
|
|
809
|
+
color.setAlpha(220)
|
|
810
|
+
pen = QPen(color, 1)
|
|
811
|
+
pen.setCapStyle(Qt.RoundCap)
|
|
812
|
+
painter.setPen(pen)
|
|
813
|
+
painter.drawLine(x1, y, x2, y)
|
|
814
|
+
finally:
|
|
815
|
+
painter.end()
|
|
816
|
+
|
|
559
817
|
# ----------------------------
|
|
560
818
|
# Mouse / DnD events
|
|
561
819
|
# ----------------------------
|
|
@@ -580,6 +838,8 @@ class PresetList(BaseList):
|
|
|
580
838
|
index = self.indexAt(self._mouse_event_point(event))
|
|
581
839
|
if not index.isValid():
|
|
582
840
|
return
|
|
841
|
+
# Freeze scroll for a moment to prevent jumps caused by selection-triggered refresh
|
|
842
|
+
self._freeze_scroll(250)
|
|
583
843
|
if self._dnd_enabled:
|
|
584
844
|
sel_model = self.selectionModel()
|
|
585
845
|
self._press_backup_selection = list(sel_model.selectedIndexes())
|
|
@@ -657,6 +917,7 @@ class PresetList(BaseList):
|
|
|
657
917
|
|
|
658
918
|
self._dragging = True
|
|
659
919
|
self.setCursor(QCursor(Qt.ClosedHandCursor))
|
|
920
|
+
# Let base class proceed; it will trigger startDrag when needed.
|
|
660
921
|
super().mouseMoveEvent(event)
|
|
661
922
|
|
|
662
923
|
def mouseReleaseEvent(self, event):
|
|
@@ -666,12 +927,18 @@ class PresetList(BaseList):
|
|
|
666
927
|
try:
|
|
667
928
|
if self._dnd_enabled and event.button() == Qt.LeftButton:
|
|
668
929
|
self.unsetCursor()
|
|
930
|
+
self._clear_drop_indicator()
|
|
669
931
|
if not self._dragging:
|
|
670
932
|
idx = self.indexAt(self._mouse_event_point(event))
|
|
671
933
|
if idx.isValid():
|
|
672
934
|
pid = idx.data(self.ROLE_ID)
|
|
673
935
|
if pid:
|
|
936
|
+
# Keep scroll stable also for this late selection path
|
|
937
|
+
self._freeze_scroll(300)
|
|
938
|
+
self._pending_refocus_role_id = pid
|
|
674
939
|
self.window.controller.presets.select_by_id(pid)
|
|
940
|
+
QTimer.singleShot(0, self._apply_pending_refocus)
|
|
941
|
+
QTimer.singleShot(50, self._apply_pending_refocus)
|
|
675
942
|
else:
|
|
676
943
|
self.setCurrentIndex(idx)
|
|
677
944
|
self.window.controller.presets.select(idx.row())
|
|
@@ -693,12 +960,16 @@ class PresetList(BaseList):
|
|
|
693
960
|
return
|
|
694
961
|
event.setDropAction(Qt.MoveAction)
|
|
695
962
|
event.acceptProposedAction()
|
|
963
|
+
super().dragEnterEvent(event)
|
|
964
|
+
# Show indicator immediately on enter
|
|
965
|
+
self._update_drop_indicator_from_pos(self._mouse_event_point(event))
|
|
696
966
|
|
|
697
967
|
def dragLeaveEvent(self, event):
|
|
698
968
|
if self._model_updating:
|
|
699
969
|
event.ignore()
|
|
700
970
|
return
|
|
701
971
|
self.unsetCursor()
|
|
972
|
+
self._clear_drop_indicator()
|
|
702
973
|
super().dragLeaveEvent(event)
|
|
703
974
|
|
|
704
975
|
def dragMoveEvent(self, event):
|
|
@@ -707,7 +978,6 @@ class PresetList(BaseList):
|
|
|
707
978
|
return
|
|
708
979
|
if not self._dnd_enabled:
|
|
709
980
|
return
|
|
710
|
-
event.setDropAction(Qt.MoveAction)
|
|
711
981
|
|
|
712
982
|
pos = self._mouse_event_point(event)
|
|
713
983
|
idx = self.indexAt(pos)
|
|
@@ -715,9 +985,17 @@ class PresetList(BaseList):
|
|
|
715
985
|
if idx.isValid() and idx.row() == 0:
|
|
716
986
|
rect = self.visualRect(idx)
|
|
717
987
|
if pos.y() <= rect.center().y():
|
|
988
|
+
self._clear_drop_indicator()
|
|
718
989
|
event.ignore()
|
|
719
990
|
return
|
|
991
|
+
|
|
992
|
+
# Let base class process autoscroll and internal geometry first
|
|
993
|
+
event.setDropAction(Qt.MoveAction)
|
|
720
994
|
event.acceptProposedAction()
|
|
995
|
+
super().dragMoveEvent(event)
|
|
996
|
+
|
|
997
|
+
# Update custom indicator based on current cursor and updated viewport
|
|
998
|
+
self._update_drop_indicator_from_pos(pos)
|
|
721
999
|
|
|
722
1000
|
def dropEvent(self, event):
|
|
723
1001
|
"""
|
|
@@ -746,26 +1024,11 @@ class PresetList(BaseList):
|
|
|
746
1024
|
event.ignore()
|
|
747
1025
|
self.unsetCursor()
|
|
748
1026
|
self._drag_selection_applied = False
|
|
1027
|
+
self._clear_drop_indicator()
|
|
749
1028
|
return
|
|
750
1029
|
|
|
751
|
-
# Target row
|
|
752
|
-
|
|
753
|
-
idx = self.indexAt(pos)
|
|
754
|
-
if not idx.isValid():
|
|
755
|
-
to_row = model.rowCount() # append
|
|
756
|
-
else:
|
|
757
|
-
rect = self.visualRect(idx)
|
|
758
|
-
to_row = idx.row()
|
|
759
|
-
if pos.y() > rect.center().y():
|
|
760
|
-
to_row += 1
|
|
761
|
-
|
|
762
|
-
# Keep first row pinned
|
|
763
|
-
if to_row <= 1:
|
|
764
|
-
to_row = 1
|
|
765
|
-
|
|
766
|
-
# Adjust when moving down (Qt inserts before position)
|
|
767
|
-
if to_row > from_row:
|
|
768
|
-
to_row -= 1
|
|
1030
|
+
# Target row computed exactly the same way as the indicator (but with 'moving down' adjustment)
|
|
1031
|
+
to_row, _ = self._compute_drop_locations(self._mouse_event_point(event))
|
|
769
1032
|
|
|
770
1033
|
moved_id = self._reorder_and_persist(from_row, to_row)
|
|
771
1034
|
|
|
@@ -778,6 +1041,7 @@ class PresetList(BaseList):
|
|
|
778
1041
|
event.acceptProposedAction()
|
|
779
1042
|
self.unsetCursor()
|
|
780
1043
|
self._drag_selection_applied = False
|
|
1044
|
+
self._clear_drop_indicator()
|
|
781
1045
|
|
|
782
1046
|
# ----------------------------
|
|
783
1047
|
# Legacy helper (not used in new path)
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2025.09.
|
|
9
|
+
# Updated Date: 2025.09.26 12:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
@@ -143,18 +143,6 @@ class NodeEditor(QWidget):
|
|
|
143
143
|
# Centralized strings
|
|
144
144
|
self.config: EditorConfig = config if isinstance(config, EditorConfig) else EditorConfig()
|
|
145
145
|
|
|
146
|
-
# Theme and palette
|
|
147
|
-
"""
|
|
148
|
-
if parent and hasattr(parent, "window"):
|
|
149
|
-
theme = parent.window.core.config.get("theme")
|
|
150
|
-
if theme.startswith("light"):
|
|
151
|
-
print("[NodeEditor] Detected light theme from parent")
|
|
152
|
-
self._grid_back_color = QColor(255, 255, 255)
|
|
153
|
-
self._grid_pen_color = QColor(230, 230, 230)
|
|
154
|
-
self.gridBackColor = Property(QColor, lambda self: self._grid_back_color, lambda self, v: self._q_set("_grid_back_color", v))
|
|
155
|
-
self.gridPenColor = Property(QColor, lambda self: self._grid_pen_color, lambda self, v: self._q_set("_grid_pen_color", v))
|
|
156
|
-
"""
|
|
157
|
-
|
|
158
146
|
self.graph = NodeGraph(registry)
|
|
159
147
|
self.scene = NodeGraphicsScene(self)
|
|
160
148
|
self.view = NodeGraphicsView(self.scene, self)
|
|
@@ -422,7 +410,12 @@ class NodeEditor(QWidget):
|
|
|
422
410
|
|
|
423
411
|
def add_node(self, type_name: str, scene_pos: QPointF):
|
|
424
412
|
"""Add a new node of the given type at scene_pos (undoable)."""
|
|
413
|
+
# Enforce optional per-type limit configured in registry.
|
|
414
|
+
if not self._can_add_node_of_type(type_name):
|
|
415
|
+
self._dbg(f"add_node blocked: type='{type_name}' reached its max per-layout limit")
|
|
416
|
+
return False
|
|
425
417
|
self._undo.push(AddNodeCommand(self, type_name, scene_pos))
|
|
418
|
+
return True
|
|
426
419
|
|
|
427
420
|
def clear(self, ask_user: bool = True):
|
|
428
421
|
"""Clear the entire editor (undoable), optionally asking the user for confirmation."""
|
|
@@ -438,6 +431,7 @@ class NodeEditor(QWidget):
|
|
|
438
431
|
if reply != QMessageBox.Yes:
|
|
439
432
|
return False
|
|
440
433
|
self._undo.push(ClearGraphCommand(self))
|
|
434
|
+
self._update_status_label()
|
|
441
435
|
return True
|
|
442
436
|
|
|
443
437
|
def undo(self):
|
|
@@ -668,6 +662,35 @@ class NodeEditor(QWidget):
|
|
|
668
662
|
vp_pt = self.view.mapFromScene(scene_pos)
|
|
669
663
|
return self.view.viewport().mapToGlobal(vp_pt)
|
|
670
664
|
|
|
665
|
+
# ---------- Per-type limit helpers ----------
|
|
666
|
+
|
|
667
|
+
def _type_limit(self, type_name: str) -> Optional[int]:
|
|
668
|
+
"""Return configured max_num for type or None if unlimited."""
|
|
669
|
+
spec = self.graph.registry.get(type_name) if self.graph and self.graph.registry else None
|
|
670
|
+
if not spec:
|
|
671
|
+
return None
|
|
672
|
+
try:
|
|
673
|
+
limit = getattr(spec, "max_num", None)
|
|
674
|
+
if isinstance(limit, int) and limit <= 0:
|
|
675
|
+
return None
|
|
676
|
+
return limit if isinstance(limit, int) else None
|
|
677
|
+
except Exception:
|
|
678
|
+
return None
|
|
679
|
+
|
|
680
|
+
def _count_nodes_of_type(self, type_name: str) -> int:
|
|
681
|
+
"""Count current nodes of a given type in the live graph."""
|
|
682
|
+
try:
|
|
683
|
+
return sum(1 for n in self.graph.nodes.values() if getattr(n, "type", None) == type_name)
|
|
684
|
+
except Exception:
|
|
685
|
+
return 0
|
|
686
|
+
|
|
687
|
+
def _can_add_node_of_type(self, type_name: str) -> bool:
|
|
688
|
+
"""Check whether adding another node of given type is allowed by max_num."""
|
|
689
|
+
limit = self._type_limit(type_name)
|
|
690
|
+
if limit is None:
|
|
691
|
+
return True
|
|
692
|
+
return self._count_nodes_of_type(type_name) < int(limit)
|
|
693
|
+
|
|
671
694
|
def _on_scene_context_menu(self, scene_pos: QPointF):
|
|
672
695
|
"""Show context menu for adding nodes and undo/redo/clear at empty scene position."""
|
|
673
696
|
try:
|
|
@@ -685,7 +708,21 @@ class NodeEditor(QWidget):
|
|
|
685
708
|
add_menu = menu.addMenu(self.config.menu_add())
|
|
686
709
|
action_by_type: Dict[QAction, str] = {}
|
|
687
710
|
for tname in self.graph.registry.types():
|
|
688
|
-
|
|
711
|
+
# Prefer UI-only display name; fallback to identifier (type_name)
|
|
712
|
+
try:
|
|
713
|
+
label = self.graph.registry.display_name(tname)
|
|
714
|
+
except Exception:
|
|
715
|
+
label = tname
|
|
716
|
+
act = add_menu.addAction(label)
|
|
717
|
+
# Show the underlying identifier in tooltip to make UI/ID relation explicit
|
|
718
|
+
if label != tname:
|
|
719
|
+
try:
|
|
720
|
+
act.setToolTip(tname)
|
|
721
|
+
except Exception:
|
|
722
|
+
pass
|
|
723
|
+
# Disable when limit reached (evaluated in real-time, per layout)
|
|
724
|
+
if not self._can_add_node_of_type(tname):
|
|
725
|
+
act.setEnabled(False)
|
|
689
726
|
action_by_type[act] = tname
|
|
690
727
|
|
|
691
728
|
menu.addSeparator()
|
|
@@ -711,7 +748,8 @@ class NodeEditor(QWidget):
|
|
|
711
748
|
self.clear(ask_user=True)
|
|
712
749
|
elif chosen in action_by_type:
|
|
713
750
|
type_name = action_by_type[chosen]
|
|
714
|
-
|
|
751
|
+
# Route through editor.add_node() to enforce limit guards
|
|
752
|
+
self.add_node(type_name, scene_pos)
|
|
715
753
|
|
|
716
754
|
# ---------- Z-order helpers ----------
|
|
717
755
|
|