more changes to config editor + funny bug
This commit is contained in:
@@ -3168,7 +3168,10 @@ class PuzzleBlock(QGraphicsPathItem):
|
||||
self.width = 160
|
||||
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")
|
||||
if getattr(self, '_processing_release', False):
|
||||
return
|
||||
|
||||
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()
|
||||
self._processing_release = True
|
||||
# ---------------------------
|
||||
|
||||
if is_over_library:
|
||||
print("Action: Trashing selected items")
|
||||
for item in self.scene.selectedItems():
|
||||
if isinstance(item, PuzzleBlock):
|
||||
self.scene.removeItem(item)
|
||||
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()
|
||||
|
||||
|
||||
finally:
|
||||
# Always reset the guard so the next real click works
|
||||
self._processing_release = False
|
||||
|
||||
|
||||
|
||||
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
|
||||
|
||||
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.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)
|
||||
|
||||
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
|
||||
return False
|
||||
# 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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user