DynamicAbstractionSystem/ui/inspector_tree.py
2025-11-08 22:11:03 -06:00

267 lines
8.6 KiB
Python

"""
Tree node classes for the hierarchical inspector view.
Provides extensible tree structure for entity inspection.
"""
from abc import ABC, abstractmethod
from typing import List, Optional, Dict, Any
import pygame
class TreeNode(ABC):
"""Base class for tree nodes in the inspector."""
def __init__(self, label: str, parent: Optional['TreeNode'] = None):
self.label = label
self.parent = parent
self.children: List['TreeNode'] = []
self.is_expanded = False
self.is_selected = False
self.depth = 0 if parent is None else parent.depth + 1
self.rect = pygame.Rect(0, 0, 0, 20) # Will be updated during layout
def add_child(self, child: 'TreeNode') -> None:
"""Add a child node to this node."""
child.parent = self
child.depth = self.depth + 1
self.children.append(child)
def remove_child(self, child: 'TreeNode') -> None:
"""Remove a child node from this node."""
if child in self.children:
self.children.remove(child)
child.parent = None
def toggle_expand(self) -> None:
"""Toggle the expanded state of this node."""
if self.children:
self.is_expanded = not self.is_expanded
def expand(self) -> None:
"""Expand this node."""
if self.children:
self.is_expanded = True
def collapse(self) -> None:
"""Collapse this node."""
self.is_expanded = False
def get_visible_children(self) -> List['TreeNode']:
"""Get children that are currently visible."""
if not self.is_expanded:
return []
return self.children
def get_all_visible_descendants(self) -> List['TreeNode']:
"""Get all visible descendants of this node."""
visible = []
if self.is_expanded:
for child in self.children:
visible.append(child)
visible.extend(child.get_all_visible_descendants())
return visible
def is_leaf(self) -> bool:
"""Check if this node has no children."""
return len(self.children) == 0
@abstractmethod
def get_display_text(self) -> str:
"""Get the display text for this node."""
pass
@abstractmethod
def can_expand(self) -> bool:
"""Check if this node can be expanded."""
pass
def get_indent(self) -> int:
"""Get the indentation width for this node."""
return self.depth * 20
class SimulationNode(TreeNode):
"""Root node representing the entire simulation."""
def __init__(self, world):
super().__init__("Simulation")
self.world = world
self.is_expanded = True # Always expanded
def get_display_text(self) -> str:
return f"Simulation"
def can_expand(self) -> bool:
return True
def update_entity_counts(self) -> None:
"""Update entity type nodes with current world state."""
entity_types = {}
# Count entities by type
for entity in self.world.get_objects():
entity_type = type(entity).__name__
if entity_type not in entity_types:
entity_types[entity_type] = []
entity_types[entity_type].append(entity)
# Update children to match current entity types
existing_types = {child.label: child for child in self.children}
# Remove types that no longer exist
for type_name in existing_types:
if type_name not in entity_types:
self.remove_child(existing_types[type_name])
# Add or update type nodes
for type_name, entities in entity_types.items():
if type_name in existing_types:
# Update existing node
type_node = existing_types[type_name]
type_node.update_entities(entities)
else:
# Create new type node
type_node = EntityTypeNode(type_name, entities)
self.add_child(type_node)
class EntityTypeNode(TreeNode):
"""Node representing a category of entities."""
def __init__(self, entity_type: str, entities: List[Any]):
super().__init__(entity_type)
self.entities = entities
self._update_children()
def _update_children(self) -> None:
"""Update child entity nodes to match current entities."""
existing_ids = {child.entity_id: child for child in self.children}
current_ids = {id(entity): entity for entity in self.entities}
# Remove entities that no longer exist
for entity_id in existing_ids:
if entity_id not in current_ids:
self.remove_child(existing_ids[entity_id])
# Add or update entity nodes
for entity_id, entity in current_ids.items():
if entity_id in existing_ids:
# Update existing node
entity_node = existing_ids[entity_id]
entity_node.entity = entity
else:
# Create new entity node
entity_node = EntityNode(entity)
self.add_child(entity_node)
def update_entities(self, entities: List[Any]) -> None:
"""Update the entities for this type."""
self.entities = entities
self._update_children()
def get_display_text(self) -> str:
count = len(self.entities)
return f"{self.label} ({count})"
def can_expand(self) -> bool:
return len(self.entities) > 0
class EntityNode(TreeNode):
"""Node representing an individual entity."""
def __init__(self, entity: Any):
# Use entity ID as the initial label, will be updated in get_display_text
super().__init__("", None)
self.entity = entity
self.entity_id = id(entity)
self.is_leaf = True # Entity nodes can't have children
def get_display_text(self) -> str:
# Start with just ID, designed for extensibility
return f"Entity {self.entity_id}"
def can_expand(self) -> bool:
return False
def is_leaf(self) -> bool:
return True
def get_entity_info(self) -> Dict[str, Any]:
"""Get entity information for display purposes."""
# Base implementation - can be extended later
info = {
'id': self.entity_id,
'type': type(self.entity).__name__
}
# Add position if available
if hasattr(self.entity, 'position'):
info['position'] = {
'x': getattr(self.entity.position, 'x', 0),
'y': getattr(self.entity.position, 'y', 0)
}
return info
class TreeSelectionManager:
"""Manages selection state for tree nodes."""
def __init__(self):
self.selected_nodes: List[TreeNode] = []
self.last_selected_node: Optional[TreeNode] = None
def select_node(self, node: TreeNode, multi_select: bool = False) -> None:
"""Select a node, optionally with multi-select."""
if not multi_select:
# Clear existing selection
for selected_node in self.selected_nodes:
selected_node.is_selected = False
self.selected_nodes.clear()
# Always select the node (remove toggle behavior for better UX)
if not node.is_selected:
node.is_selected = True
self.selected_nodes.append(node)
self.last_selected_node = node
def select_range(self, from_node: TreeNode, to_node: TreeNode, all_nodes: List[TreeNode]) -> None:
"""Select a range of nodes between from_node and to_node."""
if from_node not in all_nodes or to_node not in all_nodes:
return
# Clear existing selection
for selected_node in self.selected_nodes:
selected_node.is_selected = False
self.selected_nodes.clear()
# Find indices
from_index = all_nodes.index(from_node)
to_index = all_nodes.index(to_node)
# Select range
start = min(from_index, to_index)
end = max(from_index, to_index)
for i in range(start, end + 1):
node = all_nodes[i]
node.is_selected = True
self.selected_nodes.append(node)
self.last_selected_node = to_node
def clear_selection(self) -> None:
"""Clear all selections."""
for node in self.selected_nodes:
node.is_selected = False
self.selected_nodes.clear()
self.last_selected_node = None
def get_selected_entities(self) -> List[Any]:
"""Get the actual entities for selected entity nodes."""
entities = []
for node in self.selected_nodes:
if isinstance(node, EntityNode):
entities.append(node.entity)
return entities