rewrite layout

jotpluggler
Quantizr (Jimmy) 2 weeks ago
parent 1cfa906201
commit 628e19ffb0
  1. 426
      tools/jotpluggler/layout.py
  2. 13
      tools/jotpluggler/pluggle.py
  3. 10
      tools/jotpluggler/views.py

@ -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()

@ -9,7 +9,7 @@ import numpy as np
from openpilot.common.basedir import BASEDIR from openpilot.common.basedir import BASEDIR
from openpilot.tools.jotpluggler.data import DataManager from openpilot.tools.jotpluggler.data import DataManager
from openpilot.tools.jotpluggler.views import DataTreeView from openpilot.tools.jotpluggler.views import DataTreeView
from openpilot.tools.jotpluggler.layout import PlotLayoutManager, SplitterNode, LeafNode from openpilot.tools.jotpluggler.layout import PlotLayoutManager
DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19" DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19"
@ -207,16 +207,7 @@ class MainController:
dpg.set_value(value_tag, formatted_value) dpg.set_value(value_tag, formatted_value)
def _update_timeline_indicators(self, current_time_s: float): def _update_timeline_indicators(self, current_time_s: float):
def update_node_recursive(node): self.plot_layout_manager.update_all_panels()
if isinstance(node, LeafNode):
if hasattr(node.panel, 'update_timeline_indicator'):
node.panel.update_timeline_indicator(current_time_s)
elif isinstance(node, SplitterNode):
for child in node.children:
update_node_recursive(child)
if self.plot_layout_manager.root_node:
update_node_recursive(self.plot_layout_manager.root_node)
def main(route_to_load=None): def main(route_to_load=None):

@ -28,6 +28,9 @@ class ViewPanel(ABC):
def get_panel_type(self) -> str: def get_panel_type(self) -> str:
pass pass
def update(self):
pass
class TimeSeriesPanel(ViewPanel): class TimeSeriesPanel(ViewPanel):
def __init__(self, data_manager: DataManager, playback_manager, panel_id: str | None = None): def __init__(self, data_manager: DataManager, playback_manager, panel_id: str | None = None):
@ -61,6 +64,10 @@ class TimeSeriesPanel(ViewPanel):
self._ui_created = True self._ui_created = True
def update(self):
if self._ui_created:
self.update_timeline_indicator(self.playback_manager.current_time_s)
def update_timeline_indicator(self, current_time_s: float): def update_timeline_indicator(self, current_time_s: float):
if not self._ui_created or not dpg.does_item_exist(self.timeline_indicator_tag): if not self._ui_created or not dpg.does_item_exist(self.timeline_indicator_tag):
return return
@ -176,7 +183,6 @@ class DataTreeView:
for child in sorted(self.data_tree.children.values(), key=self._natural_sort_key): for child in sorted(self.data_tree.children.values(), key=self._natural_sort_key):
self.ui_render_queue.append((child, "data_tree_container", search_term, child.is_leaf)) self.ui_render_queue.append((child, "data_tree_container", search_term, child.is_leaf))
def _add_paths_to_tree(self, paths, incremental=False): def _add_paths_to_tree(self, paths, incremental=False):
search_term = self.current_search.strip().lower() search_term = self.current_search.strip().lower()
filtered_paths = [path for path in paths if self._should_show_path(path, search_term)] filtered_paths = [path for path in paths if self._should_show_path(path, search_term)]
@ -269,7 +275,7 @@ class DataTreeView:
node.ui_tag = node_tag node.ui_tag = node_tag
label = f"{node.name} ({node.child_count} fields)" label = f"{node.name} ({node.child_count} fields)"
should_open = (bool(search_term) and len(search_term) > 1 and any(search_term in path for path in self._get_descendant_paths(node))) should_open = bool(search_term) and len(search_term) > 1 and any(search_term in path for path in self._get_descendant_paths(node))
with dpg.tree_node(label=label, parent=parent_tag, tag=node_tag, default_open=should_open, open_on_arrow=True, open_on_double_click=True) as tree_node: with dpg.tree_node(label=label, parent=parent_tag, tag=node_tag, default_open=should_open, open_on_arrow=True, open_on_double_click=True) as tree_node:
with dpg.item_handler_registry() as handler: with dpg.item_handler_registry() as handler:

Loading…
Cancel
Save