config editor changes
This commit is contained in:
@@ -34,11 +34,11 @@ from pose_worker import run_pose_analysis
|
||||
from batch_processing import BatchProcessorDialog
|
||||
|
||||
import PySide6
|
||||
from PySide6.QtWidgets import (QApplication, QDoubleSpinBox, QGraphicsItem, QLineEdit, QListWidget, QListWidgetItem, QMainWindow, QProgressDialog, QStyleOptionGraphicsItem, QTabBar, QWidget, QVBoxLayout, QGraphicsView, QGraphicsScene,
|
||||
QHBoxLayout, QSplitter, QLabel, QPushButton, QComboBox, QInputDialog,
|
||||
from PySide6.QtWidgets import (QApplication, QDoubleSpinBox, QFormLayout, QGraphicsItem, QGraphicsProxyWidget, QGraphicsTextItem, QLineEdit, QListWidget, QListWidgetItem, QMainWindow, QProgressDialog, QSizePolicy, QStyleOptionGraphicsItem, QTabBar, QWidget, QVBoxLayout, QGraphicsView, QGraphicsScene,
|
||||
QHBoxLayout, QSplitter, QLabel, QPushButton, QComboBox, QInputDialog, QGraphicsRectItem,
|
||||
QFileDialog, QScrollArea, QMessageBox, QSlider, QTextEdit, QGroupBox, QGridLayout, QCheckBox, QTabWidget, QProgressBar)
|
||||
from PySide6.QtCore import QEvent, Qt, QThread, Signal, QUrl, QRectF, QPointF, QRect, QSizeF, QTimer
|
||||
from PySide6.QtGui import QCursor, QGuiApplication, QPainter, QColor, QFont, QPen, QBrush, QAction, QKeySequence, QIcon, QTextOption, QImage, QPixmap
|
||||
from PySide6.QtGui import QCursor, QDoubleValidator, QGuiApplication, QPainter, QColor, QFont, QPen, QBrush, QAction, QKeySequence, QIcon, QTextOption, QImage, QPixmap, QTransform
|
||||
from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput
|
||||
from PySide6.QtMultimediaWidgets import QGraphicsVideoItem
|
||||
|
||||
@@ -2008,13 +2008,16 @@ class PremiereWindow(QMainWindow):
|
||||
train_act = QAction("Train Model", self)
|
||||
train_act.triggered.connect(self.open_train_model_dialog)
|
||||
|
||||
config_act = QAction("Configuration Editor", self)
|
||||
config_act.triggered.connect(self.open_model_configuration_tab)
|
||||
config_act = QAction("Configuration (Ind.)", self)
|
||||
config_act.triggered.connect(lambda: self.open_model_configuration_tab("individual"))
|
||||
|
||||
config_act2 = QAction("Configuration (Group)", self)
|
||||
config_act2.triggered.connect(lambda: self.open_model_configuration_tab("group"))
|
||||
|
||||
menu.addAction(load_act)
|
||||
menu.addAction(train_act)
|
||||
menu.addAction(config_act)
|
||||
menu.addAction(config_act2)
|
||||
|
||||
# Show the menu right under the mouse cursor
|
||||
menu.exec(QCursor.pos())
|
||||
@@ -2090,8 +2093,8 @@ class PremiereWindow(QMainWindow):
|
||||
self.train_window.btn_train.clicked.connect(self.handle_start_training)
|
||||
self.train_window.show()
|
||||
|
||||
def open_model_configuration_tab(self):
|
||||
self.handle_model_configuration_session()
|
||||
def open_model_configuration_tab(self, stringy):
|
||||
self.handle_model_configuration_session(stringy)
|
||||
|
||||
def open_export_data_dialog(self):
|
||||
if self.export_window is None or not self.export_window.isVisible():
|
||||
@@ -2169,11 +2172,11 @@ class PremiereWindow(QMainWindow):
|
||||
self.tabs.setCurrentIndex(new_idx)
|
||||
|
||||
|
||||
def handle_model_configuration_session(self):
|
||||
new_tab = ModelParameterConfigurationTab()
|
||||
def handle_model_configuration_session(self, stringy):
|
||||
new_tab = ModelParameterConfigurationTab(stringy)
|
||||
|
||||
# 4. Handle Tab Placement (Keep '+' at the end)
|
||||
tab_name = "Configuration Editor"
|
||||
tab_name = f"Configuration Editor - {stringy}"
|
||||
plus_idx = self.tabs.count() - 1
|
||||
new_idx = self.tabs.insertTab(plus_idx, new_tab, tab_name)
|
||||
|
||||
@@ -3161,7 +3164,7 @@ from PySide6.QtWidgets import QGraphicsItem, QGraphicsSimpleTextItem, QInputDial
|
||||
|
||||
|
||||
class PuzzleBlock(QGraphicsPathItem):
|
||||
def __init__(self, b_type, label, parent_item=None):
|
||||
def __init__(self, b_type, label, parent_item=None, fields=None):
|
||||
super().__init__(parent_item)
|
||||
self.b_type = b_type # "begin", "middle", "end"
|
||||
self.label_text = label
|
||||
@@ -3178,19 +3181,108 @@ class PuzzleBlock(QGraphicsPathItem):
|
||||
QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
||||
)
|
||||
|
||||
self.update_path()
|
||||
self.setData(0, "p_block")
|
||||
|
||||
self.inputs = {}
|
||||
self.original_inputs = {}
|
||||
|
||||
print(f"[DEBUG] - {fields}")
|
||||
|
||||
if fields:
|
||||
self.container = QWidget()
|
||||
self.container.setObjectName("blockContainer")
|
||||
self.container.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
if self.b_type == "logic":
|
||||
self.container.setFixedSize(130, 20)
|
||||
self.container.setStyleSheet("background: transparent; border: none;")
|
||||
else:
|
||||
self.container.setFixedWidth(self.width)
|
||||
self.container.setStyleSheet("background: transparent; color: white;")
|
||||
|
||||
# Use FormLayout for Side-by-Side placement
|
||||
layout = QFormLayout(self.container)
|
||||
layout.setContentsMargins(10, 5, 10, 5)
|
||||
layout.setSpacing(5)
|
||||
layout.setLabelAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
|
||||
|
||||
for f in fields:
|
||||
if f['type'] in ["number", "string"]:
|
||||
# Create the label
|
||||
field_label = QLabel(f.get('label', 'Field:'))
|
||||
field_label.setStyleSheet("font-size: 9px; font-weight: bold;")
|
||||
|
||||
# Create the input
|
||||
edit = QLineEdit()
|
||||
edit.setFixedHeight(18) # Keep it slim
|
||||
edit.setStyleSheet("background: rgba(255, 255, 255, 0.2); border: 1px solid #aaa; color: white;")
|
||||
|
||||
if f['type'] == "number":
|
||||
edit.setValidator(QDoubleValidator())
|
||||
|
||||
# Add to the form: Label is left, Edit is right
|
||||
layout.addRow(field_label, edit)
|
||||
self.inputs[f.get('label')] = edit
|
||||
|
||||
elif f['type'] == "slot":
|
||||
slot_label = QLabel("")
|
||||
slot_label.setStyleSheet("""
|
||||
color: #FFA500;
|
||||
font-size: 10px;
|
||||
border: 1px dashed #FFA500;
|
||||
padding: 2px;
|
||||
border-radius: 2px;
|
||||
min-height: 20px; /* CRITICAL: Gives the mouse something to hit */
|
||||
background: rgba(255, 165, 0, 0.05); /* Slight tint helps visual hit-testing */
|
||||
""")
|
||||
# Force it to expand so it's not a tiny dot in the corner
|
||||
slot_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
|
||||
# Add as a full-width row
|
||||
layout.addRow(slot_label)
|
||||
# Make sure it's in the inputs dict so perform_snap can find the key
|
||||
key = f.get('label', 'slot')
|
||||
self.inputs[key] = slot_label
|
||||
self.original_inputs[key] = slot_label
|
||||
|
||||
self.proxy = QGraphicsProxyWidget(self)
|
||||
self.proxy.setZValue(10)
|
||||
self.proxy.setWidget(self.container)
|
||||
# Center the 160px container inside your (potentially) 180px block
|
||||
# Or set self.width = 160 to match exactly
|
||||
self.width = 160
|
||||
self.proxy.setPos(0, 35)
|
||||
|
||||
# Calculate height based on the form's hint
|
||||
self.height = 50 + layout.sizeHint().height()
|
||||
|
||||
# Color Coding
|
||||
colors = {"begin": "#2e7d32", "middle": "#1565c0", "end": "#c62828"}
|
||||
self.update_path()
|
||||
|
||||
colors = {"begin": "#2e7d32", "middle": "#1565c0", "end": "#c62828", "logic": "#ef6c00"}
|
||||
self.setBrush(QBrush(QColor(colors.get(b_type, "#555"))))
|
||||
self.setPen(QPen(QColor("#ffffff"), 1))
|
||||
|
||||
self.label_item = QGraphicsSimpleTextItem(self.label_text, self)
|
||||
self.label_item.setBrush(QColor("white"))
|
||||
self.label_item.setPos(25, 15)
|
||||
|
||||
if self.b_type == "logic":
|
||||
self.label_item.setPos(2, 1)
|
||||
else:
|
||||
self.label_item.setPos(25, 15)
|
||||
|
||||
|
||||
def update_path(self):
|
||||
"""This changes how they visually appear. Likely no logic broken here."""
|
||||
self.prepareGeometryChange()
|
||||
|
||||
if self.b_type == "logic":
|
||||
display_width = 130 # Slimmer profile
|
||||
display_height = 20 # Shorter default height
|
||||
else:
|
||||
display_width = self.width
|
||||
display_height = self.height
|
||||
|
||||
path = QPainterPath()
|
||||
path.moveTo(0, 0)
|
||||
|
||||
@@ -3200,18 +3292,18 @@ class PuzzleBlock(QGraphicsPathItem):
|
||||
path.lineTo(30, 0)
|
||||
# Nub (Male) - Sweep is negative to curve OUTWARD
|
||||
path.arcTo(30, -self.tab_size/2, self.tab_size, self.tab_size, 180, -180)
|
||||
path.lineTo(self.width, 0)
|
||||
path.lineTo(display_width, 0)
|
||||
|
||||
# 2. RIGHT EDGE
|
||||
path.lineTo(self.width, self.height)
|
||||
path.lineTo(display_width, display_height)
|
||||
|
||||
# 3. BOTTOM EDGE
|
||||
# Begin and Middle blocks now have a "Hole" (Female) on bottom
|
||||
if self.b_type in ["begin", "middle"]:
|
||||
path.lineTo(30 + self.tab_size, self.height)
|
||||
path.lineTo(30 + self.tab_size, display_height)
|
||||
# Socket (Female) - Sweep is positive to curve INWARD
|
||||
path.arcTo(30, self.height - self.tab_size/2, self.tab_size, self.tab_size, 0, 180)
|
||||
path.lineTo(0, self.height)
|
||||
path.arcTo(30, display_height - self.tab_size/2, self.tab_size, self.tab_size, 0, 180)
|
||||
path.lineTo(0, display_height)
|
||||
|
||||
# 4. LEFT EDGE
|
||||
path.lineTo(0, 0)
|
||||
@@ -3219,7 +3311,9 @@ class PuzzleBlock(QGraphicsPathItem):
|
||||
path.closeSubpath()
|
||||
self.setPath(path)
|
||||
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
'''Simply to edit the name of a start box.'''
|
||||
if self.b_type == "begin":
|
||||
new_name, ok = QInputDialog.getText(None, "Rename Step", "Enter name:", text=self.label_text)
|
||||
if ok and new_name:
|
||||
@@ -3227,63 +3321,94 @@ class PuzzleBlock(QGraphicsPathItem):
|
||||
self.label_item.setText(new_name)
|
||||
super().mouseDoubleClickEvent(event)
|
||||
|
||||
def detach_from_neighbors(self):
|
||||
"""Sever logical connections safely so we don't leave ghost references."""
|
||||
if self.parent_block:
|
||||
self.parent_block.child_block = None
|
||||
self.parent_block = None
|
||||
if self.child_block:
|
||||
self.child_block.parent_block = None
|
||||
self.child_block = None
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
# Only handle visual layering here. Do NOT detach links.
|
||||
self.setZValue(100)
|
||||
super().mousePressEvent(event)
|
||||
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
# Drop it back down to standard Z-level
|
||||
self.setZValue(0)
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
|
||||
def itemChange(self, change, value):
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
||||
# If this block is moving, check if its neighbors are NOT moving with it.
|
||||
# If a neighbor is stationary while we move, the link must break.
|
||||
|
||||
# 1. Check Parent Link
|
||||
if self.parent_block and not self.parent_block.isSelected():
|
||||
print(f"DEBUG: Tearing away from stationary parent: {self.parent_block.label_text}")
|
||||
self.parent_block.child_block = None
|
||||
self.parent_block = None
|
||||
|
||||
# 2. Check Child Link
|
||||
if self.child_block and not self.child_block.isSelected():
|
||||
print(f"DEBUG: Tearing away from stationary child: {self.child_block.label_text}")
|
||||
self.child_block.parent_block = None
|
||||
self.child_block = None
|
||||
if self.scene() and self.scene().mouseGrabberItem() is self:
|
||||
|
||||
# 1. Sever Parent Link (Both ways)
|
||||
if self.parent_block:
|
||||
print(f"DEBUG: Tearing away from parent: {self.parent_block.label_text}")
|
||||
self.parent_block.child_block = None # Tell parent I'm gone
|
||||
self.parent_block = None # Forget my parent
|
||||
|
||||
# 2. Sever Child Link (Both ways)
|
||||
if self.child_block and not self.child_block.isSelected():
|
||||
print(f"DEBUG: Tearing away from stationary child: {self.child_block.label_text}")
|
||||
self.child_block.parent_block = None # Tell child I'm gone
|
||||
self.child_block = None # Forget my child
|
||||
|
||||
return super().itemChange(change, value)
|
||||
|
||||
|
||||
|
||||
|
||||
class BlockLibrary(QListWidget):
|
||||
def __init__(self, parent_tab):
|
||||
def __init__(self, parent_tab, stringy):
|
||||
super().__init__()
|
||||
self.setDragEnabled(True)
|
||||
self.add_item("Begin Event", "begin")
|
||||
self.add_item("Process Step", "middle")
|
||||
self.add_item("Finish/Export", "end")
|
||||
|
||||
self.mode = stringy
|
||||
|
||||
self.parent_tab = parent_tab # Reference to main tab to access canvas
|
||||
|
||||
def add_item(self, label, block_type):
|
||||
self.load_blocks_from_json("blocks.json")
|
||||
|
||||
|
||||
def load_blocks_from_json(self, file_path):
|
||||
if not os.path.exists(file_path):
|
||||
print(f"Error: {file_path} not found.")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Pull the list assigned to the current mode (e.g., "individual")
|
||||
blocks = data.get(self.mode, [])
|
||||
|
||||
for block in blocks:
|
||||
# NEW: Get fields if they exist, otherwise empty list
|
||||
fields = block.get('fields', [])
|
||||
self.add_item(block['name'], block['type'], fields)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to load block library: {e}")
|
||||
|
||||
|
||||
def add_item(self, label, block_type, fields):
|
||||
item = QListWidgetItem(label)
|
||||
item.setData(Qt.UserRole, block_type)
|
||||
# NEW: Store fields list in the item
|
||||
item.setData(Qt.UserRole + 1, fields)
|
||||
self.addItem(item)
|
||||
|
||||
|
||||
def startDrag(self, supportedActions):
|
||||
item = self.currentItem()
|
||||
if not item: return
|
||||
|
||||
mime_data = QMimeData()
|
||||
# Pass the type and label as string
|
||||
mime_data.setText(f"{item.data(Qt.UserRole)}|{item.text()}")
|
||||
|
||||
# NEW: We must include the fields in the drag data!
|
||||
# Format: type | label | fields_as_json_string
|
||||
b_type = item.data(Qt.UserRole)
|
||||
label = item.text()
|
||||
fields = item.data(Qt.UserRole + 1)
|
||||
fields_str = json.dumps(fields) # Convert list to string for transfer
|
||||
|
||||
mime_data.setText(f"{b_type}|{label}|{fields_str}")
|
||||
|
||||
drag = QDrag(self)
|
||||
drag.setMimeData(mime_data)
|
||||
@@ -3293,9 +3418,13 @@ class BlockLibrary(QListWidget):
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
from PySide6.QtWidgets import QGraphicsLineItem, QGraphicsRectItem, QGraphicsSimpleTextItem
|
||||
from PySide6.QtGui import QPen, QColor, QBrush
|
||||
|
||||
|
||||
class TopologyCanvas(QGraphicsView):
|
||||
def __init__(self):
|
||||
self.scene = QGraphicsScene()
|
||||
@@ -3303,58 +3432,97 @@ class TopologyCanvas(QGraphicsView):
|
||||
self.setAcceptDrops(True)
|
||||
self.scene.setSceneRect(0, 0, 2000, 2000)
|
||||
|
||||
self.chain = [] # Track items if needed
|
||||
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
item = self.itemAt(event.position().toPoint())
|
||||
# Toggle drag mode based on whether an item was clicked
|
||||
mode = QGraphicsView.DragMode.NoDrag if item else QGraphicsView.DragMode.RubberBandDrag
|
||||
self.setDragMode(mode)
|
||||
super().mousePressEvent(event)
|
||||
|
||||
#needed?
|
||||
self.chain = []
|
||||
self._processing_release = False
|
||||
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
# 1. Native release
|
||||
# Let the native C++ event finish first
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
if getattr(self, '_processing_release', False):
|
||||
|
||||
if self._processing_release:
|
||||
return
|
||||
|
||||
# Capture the data we need
|
||||
global_pos = event.globalPosition().toPoint()
|
||||
scene_pos = self.mapToScene(event.position().toPoint())
|
||||
|
||||
# Defer the logic to the next event loop cycle (stops the DLL crash)
|
||||
QTimer.singleShot(0, lambda: self.safe_process_release(global_pos, scene_pos))
|
||||
|
||||
def safe_process_release(self, global_pos, scene_pos):
|
||||
self._processing_release = True
|
||||
# ---------------------------
|
||||
|
||||
try:
|
||||
|
||||
# 2. Sidebar Trash Logic
|
||||
global_pos = event.globalPosition().toPoint()
|
||||
widget_under_mouse = QApplication.widgetAt(global_pos)
|
||||
|
||||
print("huh")
|
||||
|
||||
is_over_library = False
|
||||
current_widget = widget_under_mouse
|
||||
while current_widget:
|
||||
if isinstance(current_widget, BlockLibrary):
|
||||
is_over_library = True
|
||||
break
|
||||
current_widget = current_widget.parentWidget()
|
||||
|
||||
if is_over_library:
|
||||
print("Action: Trashing selected items")
|
||||
# 1. Cleaner Trash Check (Assume you have a ref to your sidebar)
|
||||
# Replacing the 'while widget' loop with a geometry check is much safer
|
||||
if hasattr(self, 'library_ref') and self.library_ref.rect().contains(self.library_ref.mapFromGlobal(global_pos)):
|
||||
print("Action: Trashing items")
|
||||
for item in self.scene.selectedItems():
|
||||
if isinstance(item, PuzzleBlock):
|
||||
self.scene.removeItem(item)
|
||||
self.scene.removeItem(item)
|
||||
return
|
||||
|
||||
item_under_mouse = self.itemAt(event.position().toPoint())
|
||||
|
||||
if isinstance(item_under_mouse, PuzzleBlock):
|
||||
print(f"DEBUG: Snapping initiated via {item_under_mouse.label_text}")
|
||||
mouse_scene_pos = self.mapToScene(event.position().toPoint())
|
||||
self.perform_snap(item_under_mouse, mouse_scene_pos)
|
||||
else:
|
||||
print("DEBUG: Drop in empty space. Maintaining current stack integrity.")
|
||||
|
||||
self.validate_topology()
|
||||
|
||||
# 2. Logic for Snapping
|
||||
items_at_pos = self.scene.items(scene_pos)
|
||||
driver = next((i for i in items_at_pos if isinstance(i, PuzzleBlock) and i.isSelected()), None)
|
||||
target = next((i for i in items_at_pos if isinstance(i, PuzzleBlock) and not i.isSelected()), None)
|
||||
|
||||
if driver and target:
|
||||
self.perform_snap(driver, scene_pos)
|
||||
|
||||
finally:
|
||||
# Always reset the guard so the next real click works
|
||||
self._processing_release = False
|
||||
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
if event.mimeData().hasText():
|
||||
event.acceptProposedAction()
|
||||
|
||||
def dragMoveEvent(self, event):
|
||||
if event.mimeData().hasText():
|
||||
event.acceptProposedAction()
|
||||
|
||||
|
||||
def dropEvent(self, event):
|
||||
raw_data = event.mimeData().text()
|
||||
parts = raw_data.split("|")
|
||||
if len(parts) < 2: return # Safety check
|
||||
|
||||
block_type, label = parts[0], parts[1]
|
||||
fields = []
|
||||
|
||||
# Safe JSON parsing
|
||||
if len(parts) > 2:
|
||||
try:
|
||||
fields = json.loads(parts[2])
|
||||
except Exception as e:
|
||||
print(f"DEBUG: Failed to parse: {e}")
|
||||
|
||||
# Spawn selection
|
||||
if block_type == "value":
|
||||
# new_block = ValueBlock(value="0")
|
||||
print("Spawned ValueBlock (Placeholder)")
|
||||
return # Exit early if not implemented to avoid setPos error
|
||||
else:
|
||||
new_block = PuzzleBlock(block_type, label, fields=fields)
|
||||
|
||||
self.scene.addItem(new_block)
|
||||
|
||||
# Try snapping, otherwise place at drop location
|
||||
if not self.perform_snap(new_block, self.mapToScene(event.position().toPoint())):
|
||||
new_block.setPos(self.mapToScene(event.position().toPoint()))
|
||||
|
||||
event.acceptProposedAction()
|
||||
|
||||
|
||||
def get_stack_extremity(self, block, direction):
|
||||
curr = block
|
||||
@@ -3369,49 +3537,126 @@ class TopologyCanvas(QGraphicsView):
|
||||
return curr
|
||||
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.key() == Qt.Key.Key_F7:
|
||||
self.diagnostic_dump()
|
||||
super().keyPressEvent(event)
|
||||
|
||||
|
||||
def diagnostic_dump(self):
|
||||
print(f"\n{'-'*20} LOGICAL STATE DUMP {'-'*20}")
|
||||
blocks = [item for item in self.scene.items() if hasattr(item, 'data') and item.data(0) == "p_block"]
|
||||
|
||||
if not blocks:
|
||||
print("No blocks found in scene.")
|
||||
return
|
||||
|
||||
for i, block in enumerate(blocks):
|
||||
parent_name = block.parent_block.label_text if block.parent_block else "None"
|
||||
child_name = block.child_block.label_text if block.child_block else "None"
|
||||
|
||||
# Check for 'Half-Broken' links (Asymmetry)
|
||||
warning = ""
|
||||
if block.parent_block and block.parent_block.child_block is not block:
|
||||
warning = " [!] ASYMMETRY: Parent doesn't recognize this child"
|
||||
if block.child_block and block.child_block.parent_block is not block:
|
||||
warning = " [!] ASYMMETRY: Child doesn't recognize this parent"
|
||||
|
||||
print(f"[{i}] BLOCK: {block.label_text}")
|
||||
print(f" - Parent: {parent_name}")
|
||||
print(f" - Child: {child_name}{warning}")
|
||||
print(f" - Selected: {block.isSelected()}")
|
||||
print(f"{'-'*60}\n")
|
||||
|
||||
|
||||
|
||||
def perform_snap(self, dragged_item, mouse_pos):
|
||||
print(f"\n{'='*60}\nSNAP ATTEMPT: {dragged_item.label_text} ({dragged_item.b_type})")
|
||||
print(f"\n{'='*60}\n[DEBUG] STARTING SNAP: {dragged_item.label_text}")
|
||||
print(f"[DEBUG] Mouse Release Pos: {mouse_pos}")
|
||||
|
||||
potential_targets = self.scene.items(mouse_pos)
|
||||
target_block = None
|
||||
|
||||
# 1. Identify target block on canvas
|
||||
search_rect = QRectF(mouse_pos.x() - 10, mouse_pos.y() - 10, 20, 20)
|
||||
items = self.scene.items(search_rect)
|
||||
|
||||
entry_target = None
|
||||
for item in items:
|
||||
if isinstance(item, PuzzleBlock) and not item.isSelected():
|
||||
entry_target = item
|
||||
for item in potential_targets:
|
||||
# Ignore the item we are dragging
|
||||
if item is dragged_item:
|
||||
continue
|
||||
|
||||
# PROXY CHECK: If it's a proxy, we want the block it belongs to
|
||||
# Instead of walking up, check if the item itself IS the block
|
||||
if item.data(0) == "p_block":
|
||||
target_block = item
|
||||
break
|
||||
|
||||
# If it's a child of a block (like a label), get the parent safely
|
||||
p = item.parentItem()
|
||||
if p and p.data(0) == "p_block":
|
||||
target_block = p
|
||||
break
|
||||
|
||||
if not target_block:
|
||||
return False
|
||||
|
||||
if not entry_target:
|
||||
print(f"DEBUG: No valid target under mouse at {mouse_pos}. Snap failed.")
|
||||
# 1. Find the ultimate roots of both stacks
|
||||
dragged_root = self.get_stack_extremity(dragged_item, "up")
|
||||
dragged_family = set()
|
||||
curr = dragged_root
|
||||
while curr:
|
||||
dragged_family.add(curr)
|
||||
curr = curr.child_block
|
||||
if curr == dragged_root: break
|
||||
|
||||
# 2. Find a target that is NOT in that family
|
||||
potential_targets = self.scene.items(mouse_pos)
|
||||
target_block = None
|
||||
|
||||
for item in potential_targets:
|
||||
# Safely get the block object
|
||||
candidate = None
|
||||
if item.data(0) == "p_block":
|
||||
candidate = item
|
||||
else:
|
||||
p = item.parentItem()
|
||||
if p and p.data(0) == "p_block":
|
||||
candidate = p
|
||||
|
||||
# CRITICAL: Target cannot be part of the stack we are holding
|
||||
if candidate and candidate not in dragged_family:
|
||||
target_block = candidate
|
||||
break
|
||||
|
||||
if not target_block:
|
||||
return False
|
||||
|
||||
print(f"DEBUG: Entry target identified: {entry_target.label_text} ({entry_target.b_type}) at {entry_target.scenePos()}")
|
||||
# 3. Double-check logical roots (The "Atomic" check)
|
||||
target_root = self.get_stack_extremity(target_block, "up")
|
||||
if dragged_root == target_root:
|
||||
print(f"DEBUG: Still shares root {dragged_root.label_text}")
|
||||
return False
|
||||
|
||||
# 2. Find the head/tail of the targeted stack
|
||||
head = self.get_stack_extremity(entry_target, "up")
|
||||
tail = self.get_stack_extremity(entry_target, "down")
|
||||
print(f"DEBUG: Stack Extremities -> Head: {head.label_text} | Tail: {tail.label_text}")
|
||||
|
||||
|
||||
print(f"[DEBUG] Processing stack-to-stack connection: {dragged_item.label_text} -> {target_block.label_text}")
|
||||
|
||||
head = self.get_stack_extremity(target_block, "up")
|
||||
tail = self.get_stack_extremity(target_block, "down")
|
||||
print(f"[DEBUG] Stack Extremities -> Head: {head.label_text} | Tail: {tail.label_text}")
|
||||
|
||||
# CASE 1: Snap ABOVE the stack (Dragged item becomes PARENT)
|
||||
# Condition: Dragged item isn't a Finish, and the stack head is open.
|
||||
if dragged_item.b_type != "end" and head.parent_block is None:
|
||||
if head.b_type != "begin":
|
||||
print(f"DEBUG: Logic Trigger -> Snap {dragged_item.label_text} ABOVE {head.label_text}")
|
||||
if head.b_type != "begin":
|
||||
print(f"[DEBUG] Snap Trigger: {dragged_item.label_text} ABOVE {head.label_text}")
|
||||
return self.connect_blocks(dragged_item, head, mode="upward")
|
||||
else:
|
||||
print("DEBUG: Snap Rejected -> Cannot snap above a 'Begin' block.")
|
||||
print("[DEBUG] SNAP REJECTED: Cannot snap above a 'Begin' block.")
|
||||
|
||||
# CASE 2: Snap BELOW the stack (Dragged item becomes CHILD)
|
||||
# Condition: Dragged item isn't a Begin, and the stack tail is open.
|
||||
if dragged_item.b_type != "begin" and tail.child_block is None:
|
||||
if tail.b_type != "end":
|
||||
print(f"DEBUG: Logic Trigger -> Snap {dragged_item.label_text} BELOW {tail.label_text}")
|
||||
print(f"[DEBUG] Snap Trigger: {dragged_item.label_text} BELOW {tail.label_text}")
|
||||
return self.connect_blocks(tail, dragged_item, mode="downward")
|
||||
else:
|
||||
print("DEBUG: Snap Rejected -> Cannot snap below a 'Finish' block.")
|
||||
print("[DEBUG] SNAP REJECTED: Cannot snap below a 'Finish' block.")
|
||||
|
||||
print("DEBUG: No valid connection rules met for this pair.")
|
||||
print(f"[DEBUG] SNAP FAILED: {dragged_item.label_text} found no valid connection point on {target_block.label_text}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -3424,7 +3669,6 @@ class TopologyCanvas(QGraphicsView):
|
||||
return False
|
||||
|
||||
if mode == "upward":
|
||||
# The 'child' (already on canvas) is the anchor
|
||||
anchor_pos = child.scenePos()
|
||||
if anchor_pos.isNull():
|
||||
print(f"DEBUG: UPWARD ERROR -> {child.label_text} scenePos is Null. Aborting.")
|
||||
@@ -3435,7 +3679,6 @@ class TopologyCanvas(QGraphicsView):
|
||||
print(f"DEBUG: Physical Move -> {parent.label_text} set to {new_parent_pos} (Above {child.label_text})")
|
||||
|
||||
else:
|
||||
# The 'parent' (already on canvas) is the anchor
|
||||
anchor_pos = parent.scenePos()
|
||||
if anchor_pos.isNull():
|
||||
print(f"DEBUG: DOWNWARD ERROR -> {parent.label_text} scenePos is Null. Aborting.")
|
||||
@@ -3445,122 +3688,50 @@ class TopologyCanvas(QGraphicsView):
|
||||
child.setPos(new_child_pos)
|
||||
print(f"DEBUG: Physical Move -> {child.label_text} set to {new_child_pos} (Below {parent.label_text})")
|
||||
|
||||
# Logical Link
|
||||
parent.child_block = child
|
||||
child.parent_block = parent
|
||||
print(f"DEBUG: Logical Link Established -> {parent.label_text}.child = {child.label_text}")
|
||||
|
||||
# Chain Sync
|
||||
self.ripple_move(child)
|
||||
print(f"SUCCESS: {parent.label_text} and {child.label_text} are now connected.")
|
||||
return True
|
||||
|
||||
|
||||
|
||||
def ripple_move(self, start_block):
|
||||
"""Forces all downstream blocks to align with their parents."""
|
||||
curr = start_block
|
||||
visited = set() # Track to prevent infinite loops
|
||||
print(f"DEBUG: Starting Ripple Move from {start_block.label_text}")
|
||||
|
||||
while curr and curr.child_block:
|
||||
if curr in visited:
|
||||
print("CRITICAL: Circular link detected in ripple! Breaking.")
|
||||
break
|
||||
visited.add(curr)
|
||||
|
||||
next_block = curr.child_block
|
||||
expected_pos = curr.scenePos() + QPointF(0, curr.height)
|
||||
|
||||
if next_block.scenePos() != expected_pos:
|
||||
print(f"DEBUG: Ripple Sync -> Moving {next_block.label_text} to {expected_pos}")
|
||||
|
||||
next_block.setPos(expected_pos)
|
||||
|
||||
curr = next_block
|
||||
print("DEBUG: Ripple Move Complete.")
|
||||
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""Catches the F7 key to print current logical graphs."""
|
||||
if event.key() == Qt.Key_F7:
|
||||
self.print_logical_graphs()
|
||||
else:
|
||||
# Ensure normal key events (like Ctrl for multi-select) still work
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def print_logical_graphs(self):
|
||||
"""Crawls the logical links and prints all current stacks."""
|
||||
print(f"\n{'='*60}\nCURRENT LOGICAL GRAPHS (F7 Triggered)")
|
||||
|
||||
# Grab all puzzle blocks currently in the scene
|
||||
blocks = [item for item in self.scene.items() if hasattr(item, 'b_type')]
|
||||
|
||||
# Find the 'Heads' (blocks that have no parent)
|
||||
heads = [b for b in blocks if getattr(b, 'parent_block', None) is None]
|
||||
|
||||
if not heads:
|
||||
print("No standalone blocks or stacks found on canvas.")
|
||||
print(f"{'='*60}\n")
|
||||
return
|
||||
|
||||
# Trace down from every head to map the stacks
|
||||
for i, head in enumerate(heads):
|
||||
chain = []
|
||||
curr = head
|
||||
while curr:
|
||||
# Format: [Label (type)]
|
||||
chain.append(f"[{curr.label_text} ({curr.b_type})]")
|
||||
curr = getattr(curr, 'child_block', None)
|
||||
|
||||
print(f"Stack {i + 1}: " + " -> ".join(chain))
|
||||
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
def dropEvent(self, event):
|
||||
raw_data = event.mimeData().text()
|
||||
if "|" not in raw_data: return
|
||||
|
||||
if isinstance(event.source(), BlockLibrary):
|
||||
block_type, label = raw_data.split("|")
|
||||
drop_pos = self.mapToScene(event.position().toPoint())
|
||||
|
||||
new_block = PuzzleBlock(block_type, label)
|
||||
# Add to scene first (invisible/no pos set yet)
|
||||
self.scene.addItem(new_block)
|
||||
|
||||
# Attempt to snap it immediately
|
||||
snapped = self.perform_snap(new_block, drop_pos)
|
||||
|
||||
# If it didn't snap to a stack, place it exactly at the mouse
|
||||
if not snapped:
|
||||
new_block.setPos(drop_pos)
|
||||
|
||||
self.validate_topology()
|
||||
event.acceptProposedAction()
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
if event.mimeData().hasText():
|
||||
event.acceptProposedAction()
|
||||
|
||||
def dragMoveEvent(self, event):
|
||||
if event.mimeData().hasText():
|
||||
event.acceptProposedAction()
|
||||
|
||||
def validate_topology(self):
|
||||
all_items = [i for i in self.scene.items() if isinstance(i, PuzzleBlock)]
|
||||
has_begin = any(i.b_type == "begin" for i in all_items)
|
||||
has_end = any(i.b_type == "end" for i in all_items)
|
||||
|
||||
warnings = []
|
||||
|
||||
if not has_begin:
|
||||
warnings.append("⚠️ Missing 'Begin' block (No data source defined).")
|
||||
if not has_end:
|
||||
warnings.append("⚠️ Missing 'Finish' block (Data will not be saved).")
|
||||
|
||||
if warnings:
|
||||
status_text = "\n".join(warnings)
|
||||
#self.parent().info_label.setText(f"<font color='red'>{status_text}</font>")
|
||||
else:
|
||||
pass
|
||||
#self.parent().info_label.setText("<font color='#00ff00'>✅ Configuration Valid</font>")
|
||||
|
||||
|
||||
class ModelParameterConfigurationTab(QWidget):
|
||||
def __init__(self):
|
||||
def __init__(self, stringy):
|
||||
super().__init__()
|
||||
|
||||
self.mode = stringy
|
||||
|
||||
self.setStyleSheet("""
|
||||
QMainWindow, QWidget#centralWidget {
|
||||
@@ -3635,7 +3806,7 @@ class ModelParameterConfigurationTab(QWidget):
|
||||
sidebar_container = QWidget()
|
||||
sidebar_layout = QVBoxLayout(sidebar_container)
|
||||
|
||||
self.block_library = BlockLibrary(parent_tab=self)
|
||||
self.block_library = BlockLibrary(parent_tab=self, stringy=self.mode)
|
||||
|
||||
# Reuse your existing Inspector logic
|
||||
self.inspector_scroll = QScrollArea()
|
||||
|
||||
Reference in New Issue
Block a user