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) |
||||||
|
else: |
||||||
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: |
|
||||||
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): |
def _rebuild_ui_at_path(self, path: list[int]): |
||||||
for child in self.children: |
layout = self._get_layout_at_path(path) |
||||||
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() |
|
||||||
|
|
||||||
|
if not path: # Root update |
||||||
class PlotLayoutManager: |
dpg.delete_item(self.container_tag, children_only=True) |
||||||
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._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) |
||||||
|
dpg.delete_item(container_tag, children_only=True) |
||||||
def split_node(self, node: LeafNode, orientation: str): |
width, height = dpg.get_item_rect_size(container_tag) |
||||||
# create new panel for the split |
self._create_ui_recursive(layout, container_tag, path, width, height) |
||||||
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) |
def _cleanup_ui_recursive(self, layout: dict): |
||||||
|
if layout["type"] == "panel": |
||||||
parent_node, child_index = self._find_parent_and_index(node) |
panel = layout["panel"] |
||||||
|
panel.destroy_ui() |
||||||
if parent_node is None: # root node - create new splitter as root |
if panel in self.active_panels: |
||||||
self.root_node = SplitterNode([node, new_leaf], orientation) |
self.active_panels.remove(panel) |
||||||
self._update_ui_for_node(self.root_node, self.container_tag) |
else: |
||||||
elif isinstance(parent_node, SplitterNode) and parent_node.orientation == orientation: # same orientation - add to existing splitter |
for child in layout["children"]: |
||||||
parent_node.add_child(new_leaf, child_index + 1) |
self._cleanup_ui_recursive(child) |
||||||
self._update_ui_for_node(parent_node) |
|
||||||
else: # different orientation - replace node with new splitter |
def _resize_splits_recursive(self, layout: dict, path: list[int]): |
||||||
new_splitter = SplitterNode([node, new_leaf], orientation) |
if layout["type"] == "split": |
||||||
self._replace_child_in_parent(parent_node, node, new_splitter) |
split_tag = self._path_to_tag(path, "split") |
||||||
|
if dpg.does_item_exist(split_tag): |
||||||
def delete_node(self, node: LeafNode): # TODO: actually delete the node, not just the ui for the node |
parent_tag = dpg.get_item_parent(split_tag) |
||||||
parent_node, child_index = self._find_parent_and_index(node) |
available_width, available_height = dpg.get_item_rect_size(parent_tag) |
||||||
|
|
||||||
if parent_node is None: # root deletion - replace with new default |
for i, proportion in enumerate(layout["proportions"]): |
||||||
node.destroy_ui() |
child_path = path + [i] |
||||||
self._initialize_default_layout() |
container_tag = self._path_to_tag(child_path, "container") |
||||||
self._update_ui_for_node(self.root_node, self.container_tag) |
if dpg.does_item_exist(container_tag): |
||||||
elif isinstance(parent_node, SplitterNode): |
if layout["orientation"] == "horizontal": |
||||||
parent_node.remove_child(node) |
dpg.configure_item(container_tag, width=max(100, int(available_width * proportion))) |
||||||
if len(parent_node.children) == 1: # collapse splitter --> leaf to just leaf |
else: |
||||||
remaining_child = parent_node.children[0] |
dpg.configure_item(container_tag, height=max(100, int(available_height * proportion))) |
||||||
grandparent_node, parent_index = self._find_parent_and_index(parent_node) |
|
||||||
|
self._resize_splits_recursive(layout["children"][i], child_path) |
||||||
if grandparent_node is None: # promote remaining child to root |
|
||||||
parent_node.children.remove(remaining_child) |
def _clear_panel(self, panel): |
||||||
self.root_node = remaining_child |
if hasattr(panel, 'clear_all_series'): |
||||||
parent_node.destroy_ui() |
panel.clear_all_series() |
||||||
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) |
|
||||||
container_width, container_height = dpg.get_item_rect_size(container_tag) |
|
||||||
node.create_ui(container_tag, container_width, container_height) |
|
||||||
else: # update node in its current location (splitter updates) |
|
||||||
if node.tag and dpg.does_item_exist(node.tag): |
|
||||||
parent_container = dpg.get_item_parent(node.tag) |
|
||||||
node.destroy_ui() |
|
||||||
if parent_container and dpg.does_item_exist(parent_container): |
|
||||||
parent_width, parent_height = dpg.get_item_rect_size(parent_container) |
|
||||||
node.create_ui(parent_container, parent_width, parent_height) |
|
||||||
|
|
||||||
def _find_parent_and_index(self, target_node: LayoutNode): # TODO: probably can be stored in child |
|
||||||
def search_recursive(node: LayoutNode | None, parent: LayoutNode | None = None, index: int = 0): |
|
||||||
if node == target_node: |
|
||||||
return parent, index |
|
||||||
if isinstance(node, SplitterNode): |
|
||||||
for i, child in enumerate(node.children): |
|
||||||
result = search_recursive(child, node, i) |
|
||||||
if result[0] is not None: |
|
||||||
return result |
|
||||||
return None, None |
|
||||||
|
|
||||||
return search_recursive(self.root_node) |
|
||||||
|
Loading…
Reference in new issue