You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
198 lines
9.1 KiB
198 lines
9.1 KiB
import dearpygui.dearpygui as dpg
|
|
from openpilot.tools.jotpluggler.data import DataManager
|
|
from openpilot.tools.jotpluggler.views import TimeSeriesPanel
|
|
|
|
|
|
class PlotLayoutManager:
|
|
def __init__(self, data_manager: DataManager, playback_manager, scale: float = 1.0):
|
|
self.data_manager = data_manager
|
|
self.playback_manager = playback_manager
|
|
self.scale = scale
|
|
self.container_tag = "plot_layout_container"
|
|
self.active_panels = []
|
|
|
|
initial_panel = TimeSeriesPanel(data_manager, playback_manager)
|
|
self.active_panels.append(initial_panel)
|
|
self.layout = {"type": "panel", "panel": initial_panel}
|
|
|
|
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)
|
|
self._create_ui_recursive(self.layout, self.container_tag, [], container_width, container_height)
|
|
|
|
def on_viewport_resize(self):
|
|
self._resize_splits_recursive(self.layout, [])
|
|
|
|
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
|
|
|
|
def _path_to_tag(self, path: list[int], prefix: str = "") -> str:
|
|
path_str = "_".join(map(str, path)) if path else "root"
|
|
return f"{prefix}_{path_str}" if prefix else path_str
|
|
|
|
def _create_ui_recursive(self, layout: dict, parent_tag: str, path: list[int], width: int, height: int):
|
|
if layout["type"] == "panel":
|
|
self._create_panel_ui(layout, parent_tag, path)
|
|
else:
|
|
self._create_split_ui(layout, parent_tag, path, width, height)
|
|
|
|
def _create_panel_ui(self, layout: dict, parent_tag: str, path: list[int]):
|
|
panel_tag = self._path_to_tag(path, "panel")
|
|
|
|
with dpg.child_window(tag=panel_tag, parent=parent_tag, border=True, width=-1, height=-1, no_scrollbar=True):
|
|
with dpg.group(horizontal=True):
|
|
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))
|
|
|
|
dpg.add_separator()
|
|
|
|
content_tag = self._path_to_tag(path, "content")
|
|
with dpg.child_window(tag=content_tag, border=False, height=-1, width=-1, no_scrollbar=True):
|
|
layout["panel"].create_ui(content_tag)
|
|
|
|
def _create_split_ui(self, layout: dict, parent_tag: str, path: list[int], width: int, height: int):
|
|
split_tag = self._path_to_tag(path, "split")
|
|
is_horizontal = layout["orientation"] == "horizontal"
|
|
|
|
with dpg.group(tag=split_tag, parent=parent_tag, horizontal=is_horizontal):
|
|
for i, (child_layout, proportion) in enumerate(zip(layout["children"], layout["proportions"], strict=True)):
|
|
child_path = path + [i]
|
|
container_tag = self._path_to_tag(child_path, "container")
|
|
|
|
if is_horizontal:
|
|
child_width = max(100, int(width * proportion))
|
|
with dpg.child_window(tag=container_tag, width=child_width, height=-1, border=False, no_scrollbar=True):
|
|
self._create_ui_recursive(child_layout, container_tag, child_path, child_width, height)
|
|
else:
|
|
child_height = max(100, int(height * proportion))
|
|
with dpg.child_window(tag=container_tag, width=-1, height=child_height, border=False, no_scrollbar=True):
|
|
self._create_ui_recursive(child_layout, container_tag, child_path, width, child_height)
|
|
|
|
def _rebuild_ui_at_path(self, path: list[int]):
|
|
layout = self._get_layout_at_path(path)
|
|
|
|
if not path: # Root update
|
|
dpg.delete_item(self.container_tag, children_only=True)
|
|
container_width, container_height = dpg.get_item_rect_size(self.container_tag)
|
|
self._create_ui_recursive(layout, self.container_tag, path, container_width, container_height)
|
|
else:
|
|
container_tag = self._path_to_tag(path, "container")
|
|
if dpg.does_item_exist(container_tag):
|
|
self._cleanup_ui_recursive(layout)
|
|
dpg.delete_item(container_tag, children_only=True)
|
|
width, height = dpg.get_item_rect_size(container_tag)
|
|
self._create_ui_recursive(layout, container_tag, path, width, height)
|
|
|
|
def _cleanup_ui_recursive(self, layout: dict):
|
|
if layout["type"] == "panel":
|
|
panel = layout["panel"]
|
|
panel.destroy_ui()
|
|
if panel in self.active_panels:
|
|
self.active_panels.remove(panel)
|
|
else:
|
|
for child in layout["children"]:
|
|
self._cleanup_ui_recursive(child)
|
|
|
|
def _resize_splits_recursive(self, layout: dict, path: list[int]):
|
|
if layout["type"] == "split":
|
|
split_tag = self._path_to_tag(path, "split")
|
|
if dpg.does_item_exist(split_tag):
|
|
parent_tag = dpg.get_item_parent(split_tag)
|
|
available_width, available_height = dpg.get_item_rect_size(parent_tag)
|
|
|
|
for i, proportion in enumerate(layout["proportions"]):
|
|
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()
|
|
|