more changes to config editor + funny bug

This commit is contained in:
2026-05-01 15:04:33 -07:00
parent 3f4c4163f0
commit 6c1c4596df
+238 -61
View File
@@ -3169,6 +3169,9 @@ class PuzzleBlock(QGraphicsPathItem):
self.height = 50
self.tab_size = 15
self.parent_block = None
self.child_block = None
self.setFlags(
QGraphicsItem.GraphicsItemFlag.ItemIsMovable |
QGraphicsItem.GraphicsItemFlag.ItemIsSelectable |
@@ -3224,6 +3227,43 @@ 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
return super().itemChange(change, value)
class BlockLibrary(QListWidget):
def __init__(self, parent_tab):
@@ -3251,21 +3291,6 @@ class BlockLibrary(QListWidget):
print(f"DEBUG: Drag finished with result: {result}")
if result != Qt.IgnoreAction:
self.finalize_drop_snapping()
def finalize_drop_snapping(self):
# Grab all blocks and sort by their index in the scene (newest is usually last)
all_items = self.parent_tab.canvas.scene.items()
puzzle_blocks = [i for i in all_items if isinstance(i, PuzzleBlock)]
if puzzle_blocks:
# We want the block that was just dropped
# Usually the first in scene.items() is the top-most/newest
newest_block = puzzle_blocks[0]
# Call the method that was missing
self.parent_tab.canvas.perform_snap(newest_block, newest_block.scenePos())
from PySide6.QtWidgets import QGraphicsLineItem, QGraphicsRectItem, QGraphicsSimpleTextItem
@@ -3284,72 +3309,224 @@ class TopologyCanvas(QGraphicsView):
# 1. Native release
super().mouseReleaseEvent(event)
# 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")
for item in self.scene.selectedItems():
if isinstance(item, PuzzleBlock):
self.scene.removeItem(item)
if getattr(self, '_processing_release', False):
return
selected = [i for i in self.scene.selectedItems() if isinstance(i, PuzzleBlock)]
if selected:
dragged_item = selected[0]
# Use scenePos() for the internal coordinate math
self.perform_snap(dragged_item, dragged_item.scenePos())
self._processing_release = True
# ---------------------------
self.validate_topology()
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")
for item in self.scene.selectedItems():
if isinstance(item, PuzzleBlock):
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()
def perform_snap(self, dragged_item, pos):
""" This is the method the library is looking for. """
overlap = 0
# Create a search area around the block's current position
snap_rect = QRectF(pos.x() - 25, pos.y() - 25, 200, 100)
finally:
# Always reset the guard so the next real click works
self._processing_release = False
items_nearby = self.scene.items(snap_rect)
for target in items_nearby:
if isinstance(target, PuzzleBlock) and target != dragged_item:
# SNAP BELOW: Dragged Item is BELOW the target
if dragged_item.b_type in ["middle", "end"] and target.b_type in ["begin", "middle"]:
dragged_item.setPos(target.scenePos() + QPointF(0, target.height - overlap))
print(f"DEBUG: {dragged_item.label_text} snapped to BOTTOM of {target.label_text}")
return True
# SNAP ABOVE: Dragged Item is ABOVE the target
elif dragged_item.b_type in ["begin", "middle"] and target.b_type in ["middle", "end"]:
dragged_item.setPos(target.scenePos() - QPointF(0, dragged_item.height - overlap))
print(f"DEBUG: {dragged_item.label_text} snapped to TOP of {target.label_text}")
return True
def get_stack_extremity(self, block, direction):
curr = block
visited = {curr}
for _ in range(50):
next_node = curr.parent_block if direction == "up" else curr.child_block
if next_node and next_node not in visited:
curr = next_node
visited.add(curr)
else:
break
return curr
def perform_snap(self, dragged_item, mouse_pos):
print(f"\n{'='*60}\nSNAP ATTEMPT: {dragged_item.label_text} ({dragged_item.b_type})")
# 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
break
if not entry_target:
print(f"DEBUG: No valid target under mouse at {mouse_pos}. Snap failed.")
return False
print(f"DEBUG: Entry target identified: {entry_target.label_text} ({entry_target.b_type}) at {entry_target.scenePos()}")
# 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}")
# 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}")
return self.connect_blocks(dragged_item, head, mode="upward")
else:
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}")
return self.connect_blocks(tail, dragged_item, mode="downward")
else:
print("DEBUG: Snap Rejected -> Cannot snap below a 'Finish' block.")
print("DEBUG: No valid connection rules met for this pair.")
return False
def connect_blocks(self, parent, child, mode="downward"):
"""Links blocks and forces physical alignment based on a stable anchor."""
print(f"DEBUG: Entering connect_blocks | Parent: {parent.label_text} | Child: {child.label_text} | Mode: {mode}")
if not parent or not child:
print("DEBUG: Connection Aborted -> Missing reference to parent or child.")
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.")
return False
new_parent_pos = anchor_pos - QPointF(0, parent.height)
parent.setPos(new_parent_pos)
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.")
return False
new_child_pos = anchor_pos + QPointF(0, parent.height)
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
print(f"DEBUG: Starting Ripple Move from {start_block.label_text}")
while curr and curr.child_block:
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
# Only process drops from the Library (New blocks)
if isinstance(event.source(), BlockLibrary):
block_type, label = raw_data.split("|")
pos = self.mapToScene(event.position().toPoint())
drop_pos = self.mapToScene(event.position().toPoint())
new_block = PuzzleBlock(block_type, label)
new_block.setPos(pos)
# 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()