parent
							
								
									1cfa906201
								
							
						
					
					
						commit
						628e19ffb0
					
				
				 3 changed files with 188 additions and 275 deletions
			
			
		@ -1,282 +1,198 @@ | 
				
			|||||||
import uuid | 
					 | 
				
			||||||
import dearpygui.dearpygui as dpg | 
					import dearpygui.dearpygui as dpg | 
				
			||||||
from abc import ABC, abstractmethod | 
					 | 
				
			||||||
from openpilot.tools.jotpluggler.data import DataManager | 
					from openpilot.tools.jotpluggler.data import DataManager | 
				
			||||||
from openpilot.tools.jotpluggler.views import ViewPanel, TimeSeriesPanel | 
					from openpilot.tools.jotpluggler.views import TimeSeriesPanel | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LayoutNode(ABC): | 
					class PlotLayoutManager: | 
				
			||||||
  def __init__(self, node_id: str | None = None): | 
					  def __init__(self, data_manager: DataManager, playback_manager, scale: float = 1.0): | 
				
			||||||
    self.node_id = node_id or str(uuid.uuid4()) | 
					    self.data_manager = data_manager | 
				
			||||||
    self.tag: str | None = None | 
					    self.playback_manager = playback_manager | 
				
			||||||
 | 
					 | 
				
			||||||
  @abstractmethod | 
					 | 
				
			||||||
  def create_ui(self, parent_tag: str, width: int = -1, height: int = -1): | 
					 | 
				
			||||||
    pass | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @abstractmethod | 
					 | 
				
			||||||
  def destroy_ui(self): | 
					 | 
				
			||||||
    pass | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class LeafNode(LayoutNode): | 
					 | 
				
			||||||
  """Leaf node that contains a single ViewPanel with controls""" | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def __init__(self, panel: ViewPanel, layout_manager=None, scale: float = 1.0, node_id: str = None): | 
					 | 
				
			||||||
    super().__init__(node_id) | 
					 | 
				
			||||||
    self.panel = panel | 
					 | 
				
			||||||
    self.layout_manager = layout_manager | 
					 | 
				
			||||||
    self.scale = scale | 
					    self.scale = scale | 
				
			||||||
 | 
					    self.container_tag = "plot_layout_container" | 
				
			||||||
 | 
					    self.active_panels = [] | 
				
			||||||
 | 
					
 | 
				
			||||||
  def create_ui(self, parent_tag: str, width: int = -1, height: int = -1): | 
					    initial_panel = TimeSeriesPanel(data_manager, playback_manager) | 
				
			||||||
    """Create UI container with controls and panel""" | 
					    self.active_panels.append(initial_panel) | 
				
			||||||
    self.tag = f"leaf_{self.node_id}" | 
					    self.layout = {"type": "panel", "panel": initial_panel} | 
				
			||||||
 | 
					 | 
				
			||||||
    with dpg.child_window(tag=self.tag, parent=parent_tag, border=True, width=-1, height=-1, no_scrollbar=True): | 
					 | 
				
			||||||
      # Control bar | 
					 | 
				
			||||||
      with dpg.group(horizontal=True): | 
					 | 
				
			||||||
        dpg.add_input_text(tag=f"title_{self.node_id}", default_value=self.panel.title, width=int(100 * self.scale), callback=self._on_title_change) | 
					 | 
				
			||||||
        dpg.add_combo( | 
					 | 
				
			||||||
          items=["Time Series"],  # "Camera", "Text Log", "Map View"], | 
					 | 
				
			||||||
          tag=f"type_{self.node_id}", | 
					 | 
				
			||||||
          default_value="Time Series", | 
					 | 
				
			||||||
          width=int(100 * self.scale), | 
					 | 
				
			||||||
          callback=self._on_type_change, | 
					 | 
				
			||||||
        ) | 
					 | 
				
			||||||
        dpg.add_button(label="Clear", callback=self._clear, width=int(50 * self.scale)) | 
					 | 
				
			||||||
        dpg.add_button(label="Delete", callback=self._delete, width=int(50 * self.scale)) | 
					 | 
				
			||||||
        dpg.add_button(label="Split H", callback=lambda: self._split("horizontal"), width=int(50 * self.scale)) | 
					 | 
				
			||||||
        dpg.add_button(label="Split V", callback=lambda: self._split("vertical"), width=int(50 * self.scale)) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      dpg.add_separator() | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      # Panel content area | 
					 | 
				
			||||||
      panel_area_tag = f"panel_area_{self.node_id}" | 
					 | 
				
			||||||
      with dpg.child_window(tag=panel_area_tag, border=False, height=-1, width=-1, no_scrollbar=True): | 
					 | 
				
			||||||
        self.panel.create_ui(panel_area_tag) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def destroy_ui(self): | 
					 | 
				
			||||||
    if self.panel: | 
					 | 
				
			||||||
      self.panel.destroy_ui() | 
					 | 
				
			||||||
    if self.tag and dpg.does_item_exist(self.tag): | 
					 | 
				
			||||||
      dpg.delete_item(self.tag) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def _on_title_change(self, sender, app_data): | 
					 | 
				
			||||||
    self.panel.title = app_data | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def _on_type_change(self, sender, app_data): | 
					 | 
				
			||||||
    print(f"Panel type change requested: {app_data}") | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def _split(self, orientation: str): | 
					  def create_ui(self, parent_tag: str): | 
				
			||||||
    if self.layout_manager: | 
					    if dpg.does_item_exist(self.container_tag): | 
				
			||||||
      self.layout_manager.split_node(self, orientation) | 
					      dpg.delete_item(self.container_tag) | 
				
			||||||
 | 
					
 | 
				
			||||||
  def _clear(self): | 
					    with dpg.child_window(tag=self.container_tag, parent=parent_tag, border=False, width=-1, height=-1, no_scrollbar=True): | 
				
			||||||
    if hasattr(self.panel, 'clear_all_series'): | 
					      container_width, container_height = dpg.get_item_rect_size(self.container_tag) | 
				
			||||||
      self.panel.clear_all_series() | 
					      self._create_ui_recursive(self.layout, self.container_tag, [], container_width, container_height) | 
				
			||||||
 | 
					
 | 
				
			||||||
  def _delete(self): | 
					  def on_viewport_resize(self): | 
				
			||||||
    if self.layout_manager: | 
					    self._resize_splits_recursive(self.layout, []) | 
				
			||||||
      self.layout_manager.delete_node(self) | 
					
 | 
				
			||||||
 | 
					  def split_panel(self, panel_path: list[int], orientation: str): | 
				
			||||||
 | 
					    current_layout = self._get_layout_at_path(panel_path) | 
				
			||||||
 | 
					    existing_panel = current_layout["panel"] | 
				
			||||||
 | 
					    new_panel = TimeSeriesPanel(self.data_manager, self.playback_manager) | 
				
			||||||
 | 
					    self.active_panels.append(new_panel) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    parent, child_index = self._get_parent_and_index(panel_path) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if parent is None:  # Root split | 
				
			||||||
 | 
					      self.layout = { | 
				
			||||||
 | 
					        "type": "split", | 
				
			||||||
 | 
					        "orientation": orientation, | 
				
			||||||
 | 
					        "children": [{"type": "panel", "panel": existing_panel}, {"type": "panel", "panel": new_panel}], | 
				
			||||||
 | 
					        "proportions": [0.5, 0.5], | 
				
			||||||
 | 
					      } | 
				
			||||||
 | 
					      self._rebuild_ui_at_path([]) | 
				
			||||||
 | 
					    elif parent["type"] == "split" and parent["orientation"] == orientation: | 
				
			||||||
 | 
					      parent["children"].insert(child_index + 1, {"type": "panel", "panel": new_panel}) | 
				
			||||||
 | 
					      parent["proportions"] = [1.0 / len(parent["children"])] * len(parent["children"]) | 
				
			||||||
 | 
					      self._rebuild_ui_at_path(panel_path[:-1]) | 
				
			||||||
 | 
					    else: | 
				
			||||||
 | 
					      new_split = {"type": "split", "orientation": orientation, "children": [current_layout, {"type": "panel", "panel": new_panel}], "proportions": [0.5, 0.5]} | 
				
			||||||
 | 
					      self._replace_layout_at_path(panel_path, new_split) | 
				
			||||||
 | 
					      self._rebuild_ui_at_path(panel_path) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def delete_panel(self, panel_path: list[int]): | 
				
			||||||
 | 
					    if not panel_path:  # Root deletion | 
				
			||||||
 | 
					      old_panel = self.layout["panel"] | 
				
			||||||
 | 
					      old_panel.destroy_ui() | 
				
			||||||
 | 
					      self.active_panels.remove(old_panel) | 
				
			||||||
 | 
					      new_panel = TimeSeriesPanel(self.data_manager, self.playback_manager) | 
				
			||||||
 | 
					      self.active_panels.append(new_panel) | 
				
			||||||
 | 
					      self.layout = {"type": "panel", "panel": new_panel} | 
				
			||||||
 | 
					      self._rebuild_ui_at_path([]) | 
				
			||||||
 | 
					      return | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    parent, child_index = self._get_parent_and_index(panel_path) | 
				
			||||||
 | 
					    layout_to_delete = parent["children"][child_index] | 
				
			||||||
 | 
					    self._cleanup_ui_recursive(layout_to_delete) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    parent["children"].pop(child_index) | 
				
			||||||
 | 
					    parent["proportions"].pop(child_index) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if len(parent["children"]) == 1: # remove parent and collapse | 
				
			||||||
 | 
					      remaining_child = parent["children"][0] | 
				
			||||||
 | 
					      if len(panel_path) == 1: # parent is at root level - promote remaining child to root | 
				
			||||||
 | 
					        self.layout = remaining_child | 
				
			||||||
 | 
					        self._rebuild_ui_at_path([]) | 
				
			||||||
 | 
					      else: # replace parent with remaining child in grandparent | 
				
			||||||
 | 
					        grandparent_path = panel_path[:-2] | 
				
			||||||
 | 
					        parent_index = panel_path[-2] | 
				
			||||||
 | 
					        self._replace_layout_at_path(grandparent_path + [parent_index], remaining_child) | 
				
			||||||
 | 
					        self._rebuild_ui_at_path(grandparent_path + [parent_index]) | 
				
			||||||
 | 
					    else: # redistribute proportions | 
				
			||||||
 | 
					      equal_prop = 1.0 / len(parent["children"]) | 
				
			||||||
 | 
					      parent["proportions"] = [equal_prop] * len(parent["children"]) | 
				
			||||||
 | 
					      self._rebuild_ui_at_path(panel_path[:-1]) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def update_all_panels(self): | 
				
			||||||
 | 
					    for panel in self.active_panels: | 
				
			||||||
 | 
					      panel.update() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def _get_layout_at_path(self, path: list[int]) -> dict: | 
				
			||||||
 | 
					    current = self.layout | 
				
			||||||
 | 
					    for index in path: | 
				
			||||||
 | 
					      current = current["children"][index] | 
				
			||||||
 | 
					    return current | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def _get_parent_and_index(self, path: list[int]) -> tuple: | 
				
			||||||
 | 
					    return (None, -1) if not path else (self._get_layout_at_path(path[:-1]), path[-1]) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def _replace_layout_at_path(self, path: list[int], new_layout: dict): | 
				
			||||||
 | 
					    if not path: | 
				
			||||||
 | 
					      self.layout = new_layout | 
				
			||||||
 | 
					    else: | 
				
			||||||
 | 
					      parent, index = self._get_parent_and_index(path) | 
				
			||||||
 | 
					      parent["children"][index] = new_layout | 
				
			||||||
 | 
					
 | 
				
			||||||
class SplitterNode(LayoutNode): | 
					  def _path_to_tag(self, path: list[int], prefix: str = "") -> str: | 
				
			||||||
  def __init__(self, children: list[LayoutNode], orientation: str = "horizontal", node_id: str | None = None): | 
					    path_str = "_".join(map(str, path)) if path else "root" | 
				
			||||||
    super().__init__(node_id) | 
					    return f"{prefix}_{path_str}" if prefix else path_str | 
				
			||||||
    self.children = children if children else [] | 
					 | 
				
			||||||
    self.orientation = orientation | 
					 | 
				
			||||||
    self.child_proportions = [1.0 / len(self.children) for _ in self.children] if self.children else [] | 
					 | 
				
			||||||
    self.child_container_tags: list[str] = []  # Track container tags for resizing | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def add_child(self, child: LayoutNode, index: int = None): | 
					  def _create_ui_recursive(self, layout: dict, parent_tag: str, path: list[int], width: int, height: int): | 
				
			||||||
    if index is None: | 
					    if layout["type"] == "panel": | 
				
			||||||
      self.children.append(child) | 
					      self._create_panel_ui(layout, parent_tag, path) | 
				
			||||||
      self.child_proportions.append(0.0) | 
					 | 
				
			||||||
    else: | 
					    else: | 
				
			||||||
      self.children.insert(index, child) | 
					      self._create_split_ui(layout, parent_tag, path, width, height) | 
				
			||||||
      self.child_proportions.insert(index, 0.0) | 
					 | 
				
			||||||
    self._redistribute_proportions() | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def remove_child(self, child: LayoutNode): | 
					 | 
				
			||||||
    if child in self.children: | 
					 | 
				
			||||||
      index = self.children.index(child) | 
					 | 
				
			||||||
      self.children.remove(child) | 
					 | 
				
			||||||
      self.child_proportions.pop(index) | 
					 | 
				
			||||||
      child.destroy_ui() | 
					 | 
				
			||||||
      if self.children: | 
					 | 
				
			||||||
        self._redistribute_proportions() | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def replace_child(self, old_child: LayoutNode, new_child: LayoutNode): | 
					 | 
				
			||||||
    try: | 
					 | 
				
			||||||
      index = self.children.index(old_child) | 
					 | 
				
			||||||
      self.children[index] = new_child | 
					 | 
				
			||||||
      return index | 
					 | 
				
			||||||
    except ValueError: | 
					 | 
				
			||||||
      return None | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def _redistribute_proportions(self): | 
					 | 
				
			||||||
    if self.children: | 
					 | 
				
			||||||
      equal_proportion = 1.0 / len(self.children) | 
					 | 
				
			||||||
      self.child_proportions = [equal_proportion for _ in self.children] | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def resize_children(self): | 
					 | 
				
			||||||
    if not self.tag or not dpg.does_item_exist(self.tag): | 
					 | 
				
			||||||
      return | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    available_width, available_height = dpg.get_item_rect_size(dpg.get_item_parent(self.tag)) | 
					  def _create_panel_ui(self, layout: dict, parent_tag: str, path: list[int]): | 
				
			||||||
 | 
					    panel_tag = self._path_to_tag(path, "panel") | 
				
			||||||
 | 
					
 | 
				
			||||||
    for i, container_tag in enumerate(self.child_container_tags): | 
					    with dpg.child_window(tag=panel_tag, parent=parent_tag, border=True, width=-1, height=-1, no_scrollbar=True): | 
				
			||||||
      if not dpg.does_item_exist(container_tag): | 
					      with dpg.group(horizontal=True): | 
				
			||||||
        continue | 
					        dpg.add_input_text(default_value=layout["panel"].title, width=int(100 * self.scale), callback=lambda s, v: setattr(layout["panel"], "title", v)) | 
				
			||||||
 | 
					        dpg.add_combo(items=["Time Series"], default_value="Time Series", width=int(100 * self.scale)) | 
				
			||||||
 | 
					        dpg.add_button(label="Clear", callback=lambda: self._clear_panel(layout["panel"]), width=int(50 * self.scale)) | 
				
			||||||
 | 
					        dpg.add_button(label="Delete", callback=lambda: self.delete_panel(path), width=int(50 * self.scale)) | 
				
			||||||
 | 
					        dpg.add_button(label="Split H", callback=lambda: self.split_panel(path, "horizontal"), width=int(50 * self.scale)) | 
				
			||||||
 | 
					        dpg.add_button(label="Split V", callback=lambda: self.split_panel(path, "vertical"), width=int(50 * self.scale)) | 
				
			||||||
 | 
					
 | 
				
			||||||
      proportion = self.child_proportions[i] if i < len(self.child_proportions) else (1.0 / len(self.children)) | 
					      dpg.add_separator() | 
				
			||||||
 | 
					
 | 
				
			||||||
      if self.orientation == "horizontal": | 
					      content_tag = self._path_to_tag(path, "content") | 
				
			||||||
        new_width = max(100, int(available_width * proportion)) | 
					      with dpg.child_window(tag=content_tag, border=False, height=-1, width=-1, no_scrollbar=True): | 
				
			||||||
        dpg.configure_item(container_tag, width=new_width) | 
					        layout["panel"].create_ui(content_tag) | 
				
			||||||
      else: | 
					 | 
				
			||||||
        new_height = max(100, int(available_height * proportion)) | 
					 | 
				
			||||||
        dpg.configure_item(container_tag, height=new_height) | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      child = self.children[i] if i < len(self.children) else None | 
					  def _create_split_ui(self, layout: dict, parent_tag: str, path: list[int], width: int, height: int): | 
				
			||||||
      if child and isinstance(child, SplitterNode): | 
					    split_tag = self._path_to_tag(path, "split") | 
				
			||||||
        child.resize_children() | 
					    is_horizontal = layout["orientation"] == "horizontal" | 
				
			||||||
 | 
					
 | 
				
			||||||
  def create_ui(self, parent_tag: str, width: int = -1, height: int = -1): | 
					    with dpg.group(tag=split_tag, parent=parent_tag, horizontal=is_horizontal): | 
				
			||||||
    self.tag = f"splitter_{self.node_id}" | 
					      for i, (child_layout, proportion) in enumerate(zip(layout["children"], layout["proportions"], strict=True)): | 
				
			||||||
    self.child_container_tags = [] | 
					        child_path = path + [i] | 
				
			||||||
 | 
					        container_tag = self._path_to_tag(child_path, "container") | 
				
			||||||
 | 
					
 | 
				
			||||||
    if self.orientation == "horizontal": | 
					        if is_horizontal: | 
				
			||||||
      with dpg.group(tag=self.tag, parent=parent_tag, horizontal=True): | 
					 | 
				
			||||||
        for i, child in enumerate(self.children): | 
					 | 
				
			||||||
          proportion = self.child_proportions[i] | 
					 | 
				
			||||||
          child_width = max(100, int(width * proportion)) | 
					          child_width = max(100, int(width * proportion)) | 
				
			||||||
          container_tag = f"child_container_{self.node_id}_{i}" | 
					          with dpg.child_window(tag=container_tag, width=child_width, height=-1, border=False, no_scrollbar=True): | 
				
			||||||
          self.child_container_tags.append(container_tag) | 
					            self._create_ui_recursive(child_layout, container_tag, child_path, child_width, height) | 
				
			||||||
 | 
					 | 
				
			||||||
          with dpg.child_window(tag=container_tag, width=child_width, height=-1, border=False, no_scrollbar=True, resizable_x=False): | 
					 | 
				
			||||||
            child.create_ui(container_tag, child_width, height) | 
					 | 
				
			||||||
        else: | 
					        else: | 
				
			||||||
      with dpg.group(tag=self.tag, parent=parent_tag): | 
					 | 
				
			||||||
        for i, child in enumerate(self.children): | 
					 | 
				
			||||||
          proportion = self.child_proportions[i] | 
					 | 
				
			||||||
          child_height = max(100, int(height * proportion)) | 
					          child_height = max(100, int(height * proportion)) | 
				
			||||||
          container_tag = f"child_container_{self.node_id}_{i}" | 
					          with dpg.child_window(tag=container_tag, width=-1, height=child_height, border=False, no_scrollbar=True): | 
				
			||||||
          self.child_container_tags.append(container_tag) | 
					            self._create_ui_recursive(child_layout, container_tag, child_path, width, child_height) | 
				
			||||||
 | 
					 | 
				
			||||||
          with dpg.child_window(tag=container_tag, width=-1, height=child_height, border=False, no_scrollbar=True, resizable_y=False): | 
					 | 
				
			||||||
            child.create_ui(container_tag, width, child_height) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def destroy_ui(self): | 
					 | 
				
			||||||
    for child in self.children: | 
					 | 
				
			||||||
      if child: | 
					 | 
				
			||||||
        child.destroy_ui() | 
					 | 
				
			||||||
    if self.tag and dpg.does_item_exist(self.tag): | 
					 | 
				
			||||||
      dpg.delete_item(self.tag) | 
					 | 
				
			||||||
    self.child_container_tags.clear() | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def _rebuild_ui_at_path(self, path: list[int]): | 
				
			||||||
 | 
					    layout = self._get_layout_at_path(path) | 
				
			||||||
 | 
					
 | 
				
			||||||
class PlotLayoutManager: | 
					    if not path:  # Root update | 
				
			||||||
  def __init__(self, data_manager: DataManager, playback_manager, scale: float = 1.0): | 
					      dpg.delete_item(self.container_tag, children_only=True) | 
				
			||||||
    self.data_manager = data_manager | 
					 | 
				
			||||||
    self.playback_manager = playback_manager | 
					 | 
				
			||||||
    self.scale = scale | 
					 | 
				
			||||||
    self.container_tag = "plot_layout_container" | 
					 | 
				
			||||||
    self._initialize_default_layout() | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def _initialize_default_layout(self): | 
					 | 
				
			||||||
    panel = TimeSeriesPanel(self.data_manager, self.playback_manager) | 
					 | 
				
			||||||
    self.root_node = LeafNode(panel, layout_manager=self, scale=self.scale) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def create_ui(self, parent_tag: str): | 
					 | 
				
			||||||
    if dpg.does_item_exist(self.container_tag): | 
					 | 
				
			||||||
      dpg.delete_item(self.container_tag) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    with dpg.child_window(tag=self.container_tag, parent=parent_tag, border=False, width=-1, height=-1, no_scrollbar=True): | 
					 | 
				
			||||||
      container_width, container_height = dpg.get_item_rect_size(self.container_tag) | 
					      container_width, container_height = dpg.get_item_rect_size(self.container_tag) | 
				
			||||||
      self.root_node.create_ui(self.container_tag, container_width, container_height) | 
					      self._create_ui_recursive(layout, self.container_tag, path, container_width, container_height) | 
				
			||||||
 | 
					    else: | 
				
			||||||
  def on_viewport_resize(self): | 
					      container_tag = self._path_to_tag(path, "container") | 
				
			||||||
    if isinstance(self.root_node, SplitterNode): | 
					      if dpg.does_item_exist(container_tag): | 
				
			||||||
      self.root_node.resize_children() | 
					        self._cleanup_ui_recursive(layout) | 
				
			||||||
 | 
					 | 
				
			||||||
  def split_node(self, node: LeafNode, orientation: str): | 
					 | 
				
			||||||
    # create new panel for the split | 
					 | 
				
			||||||
    new_panel = TimeSeriesPanel(self.data_manager, self.playback_manager)  # TODO: create same type of panel as the split | 
					 | 
				
			||||||
    new_leaf = LeafNode(new_panel, layout_manager=self, scale=self.scale) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    parent_node, child_index = self._find_parent_and_index(node) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if parent_node is None:  # root node - create new splitter as root | 
					 | 
				
			||||||
      self.root_node = SplitterNode([node, new_leaf], orientation) | 
					 | 
				
			||||||
      self._update_ui_for_node(self.root_node, self.container_tag) | 
					 | 
				
			||||||
    elif isinstance(parent_node, SplitterNode) and parent_node.orientation == orientation:  # same orientation - add to existing splitter | 
					 | 
				
			||||||
      parent_node.add_child(new_leaf, child_index + 1) | 
					 | 
				
			||||||
      self._update_ui_for_node(parent_node) | 
					 | 
				
			||||||
    else:  # different orientation - replace node with new splitter | 
					 | 
				
			||||||
      new_splitter = SplitterNode([node, new_leaf], orientation) | 
					 | 
				
			||||||
      self._replace_child_in_parent(parent_node, node, new_splitter) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def delete_node(self, node: LeafNode):  # TODO: actually delete the node, not just the ui for the node | 
					 | 
				
			||||||
    parent_node, child_index = self._find_parent_and_index(node) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if parent_node is None:  # root deletion - replace with new default | 
					 | 
				
			||||||
      node.destroy_ui() | 
					 | 
				
			||||||
      self._initialize_default_layout() | 
					 | 
				
			||||||
      self._update_ui_for_node(self.root_node, self.container_tag) | 
					 | 
				
			||||||
    elif isinstance(parent_node, SplitterNode): | 
					 | 
				
			||||||
      parent_node.remove_child(node) | 
					 | 
				
			||||||
      if len(parent_node.children) == 1:  # collapse splitter --> leaf to just leaf | 
					 | 
				
			||||||
        remaining_child = parent_node.children[0] | 
					 | 
				
			||||||
        grandparent_node, parent_index = self._find_parent_and_index(parent_node) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if grandparent_node is None:  # promote remaining child to root | 
					 | 
				
			||||||
          parent_node.children.remove(remaining_child) | 
					 | 
				
			||||||
          self.root_node = remaining_child | 
					 | 
				
			||||||
          parent_node.destroy_ui() | 
					 | 
				
			||||||
          self._update_ui_for_node(self.root_node, self.container_tag) | 
					 | 
				
			||||||
        else:  # replace splitter with remaining child in grandparent node | 
					 | 
				
			||||||
          self._replace_child_in_parent(grandparent_node, parent_node, remaining_child) | 
					 | 
				
			||||||
      else:  # update splpitter contents | 
					 | 
				
			||||||
        self._update_ui_for_node(parent_node) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def _replace_child_in_parent(self, parent_node: SplitterNode, old_child: LayoutNode, new_child: LayoutNode): | 
					 | 
				
			||||||
    child_index = parent_node.children.index(old_child) | 
					 | 
				
			||||||
    child_container_tag = f"child_container_{parent_node.node_id}_{child_index}" | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    parent_node.replace_child(old_child, new_child) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Clean up old child if it's being replaced (not just moved) | 
					 | 
				
			||||||
    if old_child != new_child: | 
					 | 
				
			||||||
      old_child.destroy_ui() | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if dpg.does_item_exist(child_container_tag): | 
					 | 
				
			||||||
      dpg.delete_item(child_container_tag, children_only=True) | 
					 | 
				
			||||||
      container_width, container_height = dpg.get_item_rect_size(child_container_tag) | 
					 | 
				
			||||||
      new_child.create_ui(child_container_tag, container_width, container_height) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def _update_ui_for_node(self, node: LayoutNode, container_tag: str = None): | 
					 | 
				
			||||||
    if container_tag:  #  update node in a specific container (usually root) | 
					 | 
				
			||||||
        dpg.delete_item(container_tag, children_only=True) | 
					        dpg.delete_item(container_tag, children_only=True) | 
				
			||||||
      container_width, container_height = dpg.get_item_rect_size(container_tag) | 
					        width, height = dpg.get_item_rect_size(container_tag) | 
				
			||||||
      node.create_ui(container_tag, container_width, container_height) | 
					        self._create_ui_recursive(layout, container_tag, path, width, height) | 
				
			||||||
    else:  # update node in its current location (splitter updates) | 
					
 | 
				
			||||||
      if node.tag and dpg.does_item_exist(node.tag): | 
					  def _cleanup_ui_recursive(self, layout: dict): | 
				
			||||||
        parent_container = dpg.get_item_parent(node.tag) | 
					    if layout["type"] == "panel": | 
				
			||||||
        node.destroy_ui() | 
					      panel = layout["panel"] | 
				
			||||||
        if parent_container and dpg.does_item_exist(parent_container): | 
					      panel.destroy_ui() | 
				
			||||||
          parent_width, parent_height = dpg.get_item_rect_size(parent_container) | 
					      if panel in self.active_panels: | 
				
			||||||
          node.create_ui(parent_container, parent_width, parent_height) | 
					        self.active_panels.remove(panel) | 
				
			||||||
 | 
					    else: | 
				
			||||||
  def _find_parent_and_index(self, target_node: LayoutNode):  # TODO: probably can be stored in child | 
					      for child in layout["children"]: | 
				
			||||||
    def search_recursive(node: LayoutNode | None, parent: LayoutNode | None = None, index: int = 0): | 
					        self._cleanup_ui_recursive(child) | 
				
			||||||
      if node == target_node: | 
					
 | 
				
			||||||
        return parent, index | 
					  def _resize_splits_recursive(self, layout: dict, path: list[int]): | 
				
			||||||
      if isinstance(node, SplitterNode): | 
					    if layout["type"] == "split": | 
				
			||||||
        for i, child in enumerate(node.children): | 
					      split_tag = self._path_to_tag(path, "split") | 
				
			||||||
          result = search_recursive(child, node, i) | 
					      if dpg.does_item_exist(split_tag): | 
				
			||||||
          if result[0] is not None: | 
					        parent_tag = dpg.get_item_parent(split_tag) | 
				
			||||||
            return result | 
					        available_width, available_height = dpg.get_item_rect_size(parent_tag) | 
				
			||||||
      return None, None | 
					
 | 
				
			||||||
 | 
					        for i, proportion in enumerate(layout["proportions"]): | 
				
			||||||
    return search_recursive(self.root_node) | 
					          child_path = path + [i] | 
				
			||||||
 | 
					          container_tag = self._path_to_tag(child_path, "container") | 
				
			||||||
 | 
					          if dpg.does_item_exist(container_tag): | 
				
			||||||
 | 
					            if layout["orientation"] == "horizontal": | 
				
			||||||
 | 
					              dpg.configure_item(container_tag, width=max(100, int(available_width * proportion))) | 
				
			||||||
 | 
					            else: | 
				
			||||||
 | 
					              dpg.configure_item(container_tag, height=max(100, int(available_height * proportion))) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          self._resize_splits_recursive(layout["children"][i], child_path) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def _clear_panel(self, panel): | 
				
			||||||
 | 
					    if hasattr(panel, 'clear_all_series'): | 
				
			||||||
 | 
					      panel.clear_all_series() | 
				
			||||||
 | 
				
			|||||||
					Loading…
					
					
				
		Reference in new issue