jotpluggler: add tabs to layout (#36146)

* queue syncs in main thread to avoid Glfw Error/segfault

* tabs
pull/36147/head
Jimmy 6 days ago committed by GitHub
parent 347b23055d
commit 1870d4905b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      tools/jotpluggler/assets/plus.png
  2. 149
      tools/jotpluggler/layout.py
  3. 12
      tools/jotpluggler/pluggle.py
  4. 27
      tools/jotpluggler/views.py

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:248b71eafd1b42b0861da92114da3d625221cd88121fff01e0514bf3d79ff3b1
size 1364

@ -5,15 +5,135 @@ from openpilot.tools.jotpluggler.views import TimeSeriesPanel
GRIP_SIZE = 4 GRIP_SIZE = 4
MIN_PANE_SIZE = 60 MIN_PANE_SIZE = 60
class LayoutManager:
def __init__(self, data_manager, playback_manager, worker_manager, scale: float = 1.0):
self.data_manager = data_manager
self.playback_manager = playback_manager
self.worker_manager = worker_manager
self.scale = scale
self.container_tag = "plot_layout_container"
self.tab_bar_tag = "tab_bar_container"
self.tab_content_tag = "tab_content_area"
self.active_tab = 0
initial_panel_layout = PanelLayoutManager(data_manager, playback_manager, worker_manager, scale)
self.tabs: dict = {0: {"name": "Tab 1", "panel_layout": initial_panel_layout}}
self._next_tab_id = self.active_tab + 1
self._create_tab_themes()
def _create_tab_themes(self):
for tag, color in (("active_tab_theme", (37, 37, 38, 255)), ("inactive_tab_theme", (70, 70, 75, 255))):
with dpg.theme(tag=tag):
for cmp, target in ((dpg.mvChildWindow, dpg.mvThemeCol_ChildBg), (dpg.mvInputText, dpg.mvThemeCol_FrameBg), (dpg.mvImageButton, dpg.mvThemeCol_Button)):
with dpg.theme_component(cmp):
dpg.add_theme_color(target, color)
with dpg.theme(tag="tab_bar_theme"):
with dpg.theme_component(dpg.mvChildWindow):
dpg.add_theme_color(dpg.mvThemeCol_ChildBg, (51, 51, 55, 255))
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, no_scroll_with_mouse=True):
self._create_tab_bar()
self._create_tab_content()
dpg.bind_item_theme(self.tab_bar_tag, "tab_bar_theme")
def _create_tab_bar(self):
text_size = int(13 * self.scale)
with dpg.child_window(tag=self.tab_bar_tag, parent=self.container_tag, height=(text_size + 8), border=False, horizontal_scrollbar=True):
with dpg.group(horizontal=True, tag="tab_bar_group"):
for tab_id, tab_data in self.tabs.items():
self._create_tab_ui(tab_id, tab_data["name"])
dpg.add_image_button(texture_tag="plus_texture", callback=self.add_tab, width=text_size, height=text_size, tag="add_tab_button")
dpg.bind_item_theme("add_tab_button", "inactive_tab_theme")
def _create_tab_ui(self, tab_id: int, tab_name: str):
text_size = int(13 * self.scale)
tab_width = int(120 * self.scale)
with dpg.child_window(width=tab_width, height=-1, border=False, no_scrollbar=True, tag=f"tab_window_{tab_id}", parent="tab_bar_group"):
with dpg.group(horizontal=True, tag=f"tab_group_{tab_id}"):
dpg.add_input_text(
default_value=tab_name, width=tab_width - text_size - 16, callback=lambda s, v, u: self.rename_tab(u, v), user_data=tab_id, tag=f"tab_input_{tab_id}"
)
dpg.add_image_button(
texture_tag="x_texture", callback=lambda s, a, u: self.close_tab(u), user_data=tab_id, width=text_size, height=text_size, tag=f"tab_close_{tab_id}"
)
with dpg.item_handler_registry(tag=f"tab_handler_{tab_id}"):
dpg.add_item_clicked_handler(callback=lambda s, a, u: self.switch_tab(u), user_data=tab_id)
dpg.bind_item_handler_registry(f"tab_group_{tab_id}", f"tab_handler_{tab_id}")
theme_tag = "active_tab_theme" if tab_id == self.active_tab else "inactive_tab_theme"
dpg.bind_item_theme(f"tab_window_{tab_id}", theme_tag)
def _create_tab_content(self):
with dpg.child_window(tag=self.tab_content_tag, parent=self.container_tag, border=False, width=-1, height=-1, no_scrollbar=True, no_scroll_with_mouse=True):
active_panel_layout = self.tabs[self.active_tab]["panel_layout"]
active_panel_layout.create_ui()
def add_tab(self):
new_panel_layout = PanelLayoutManager(self.data_manager, self.playback_manager, self.worker_manager, self.scale)
new_tab = {"name": f"Tab {self._next_tab_id + 1}", "panel_layout": new_panel_layout}
self.tabs[ self._next_tab_id] = new_tab
self._create_tab_ui( self._next_tab_id, new_tab["name"])
dpg.move_item("add_tab_button", parent="tab_bar_group") # move plus button to end
self.switch_tab( self._next_tab_id)
self._next_tab_id += 1
def close_tab(self, tab_id: int):
if len(self.tabs) <= 1:
return # don't allow closing the last tab
tab_to_close = self.tabs[tab_id]
tab_to_close["panel_layout"].destroy_ui()
for suffix in ["window", "group", "input", "close", "handler"]:
tag = f"tab_{suffix}_{tab_id}"
if dpg.does_item_exist(tag):
dpg.delete_item(tag)
del self.tabs[tab_id]
if self.active_tab == tab_id: # switch to another tab if we closed the active one
self.active_tab = next(iter(self.tabs.keys()))
self._switch_tab_content()
dpg.bind_item_theme(f"tab_window_{self.active_tab}", "active_tab_theme")
def switch_tab(self, tab_id: int):
if tab_id == self.active_tab or tab_id not in self.tabs:
return
class PlotLayoutManager: current_panel_layout = self.tabs[self.active_tab]["panel_layout"]
current_panel_layout.destroy_ui()
dpg.bind_item_theme(f"tab_window_{self.active_tab}", "inactive_tab_theme") # deactivate old tab
self.active_tab = tab_id
dpg.bind_item_theme(f"tab_window_{tab_id}", "active_tab_theme") # activate new tab
self._switch_tab_content()
def _switch_tab_content(self):
dpg.delete_item(self.tab_content_tag, children_only=True)
active_panel_layout = self.tabs[self.active_tab]["panel_layout"]
active_panel_layout.create_ui()
active_panel_layout.update_all_panels()
def rename_tab(self, tab_id: int, new_name: str):
if tab_id in self.tabs:
self.tabs[tab_id]["name"] = new_name
def update_all_panels(self):
self.tabs[self.active_tab]["panel_layout"].update_all_panels()
def on_viewport_resize(self):
self.tabs[self.active_tab]["panel_layout"].on_viewport_resize()
class PanelLayoutManager:
def __init__(self, data_manager: DataManager, playback_manager, worker_manager, scale: float = 1.0): def __init__(self, data_manager: DataManager, playback_manager, worker_manager, scale: float = 1.0):
self.data_manager = data_manager self.data_manager = data_manager
self.playback_manager = playback_manager self.playback_manager = playback_manager
self.worker_manager = worker_manager self.worker_manager = worker_manager
self.scale = scale self.scale = scale
self.container_tag = "plot_layout_container"
self.active_panels: list = [] self.active_panels: list = []
self.parent_tag = "tab_content_area"
self.grip_size = int(GRIP_SIZE * self.scale) self.grip_size = int(GRIP_SIZE * self.scale)
self.min_pane_size = int(MIN_PANE_SIZE * self.scale) self.min_pane_size = int(MIN_PANE_SIZE * self.scale)
@ -21,13 +141,18 @@ class PlotLayoutManager:
initial_panel = TimeSeriesPanel(data_manager, playback_manager, worker_manager) initial_panel = TimeSeriesPanel(data_manager, playback_manager, worker_manager)
self.layout: dict = {"type": "panel", "panel": initial_panel} self.layout: dict = {"type": "panel", "panel": initial_panel}
def create_ui(self, parent_tag: str): def create_ui(self):
if dpg.does_item_exist(self.container_tag): self.active_panels.clear()
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, no_scroll_with_mouse=True): if dpg.does_item_exist(self.parent_tag):
container_width, container_height = dpg.get_item_rect_size(self.container_tag) dpg.delete_item(self.parent_tag, children_only=True)
self._create_ui_recursive(self.layout, self.container_tag, [], container_width, container_height)
container_width, container_height = dpg.get_item_rect_size(self.parent_tag)
self._create_ui_recursive(self.layout, self.parent_tag, [], container_width, container_height)
def destroy_ui(self):
self._cleanup_ui_recursive(self.layout, [])
self.active_panels.clear()
def _create_ui_recursive(self, layout: dict, parent_tag: str, path: list[int], width: int, height: int): def _create_ui_recursive(self, layout: dict, parent_tag: str, path: list[int], width: int, height: int):
if layout["type"] == "panel": if layout["type"] == "panel":
@ -40,9 +165,9 @@ class PlotLayoutManager:
panel = layout["panel"] panel = layout["panel"]
self.active_panels.append(panel) self.active_panels.append(panel)
text_size = int(13 * self.scale) text_size = int(13 * self.scale)
bar_height = (text_size+24) if width < int(279 * self.scale + 80) else (text_size+8) # adjust height to allow for scrollbar bar_height = (text_size + 24) if width < int(279 * self.scale + 64) else (text_size + 8) # adjust height to allow for scrollbar
with dpg.child_window(parent=parent_tag, border=True, width=-1, height=-1, no_scrollbar=True): with dpg.child_window(parent=parent_tag, border=False, width=-1, height=-1, no_scrollbar=True):
with dpg.group(horizontal=True): with dpg.group(horizontal=True):
with dpg.child_window(tag=panel_tag, width=-(text_size + 16), height=bar_height, horizontal_scrollbar=True, no_scroll_with_mouse=True, border=False): with dpg.child_window(tag=panel_tag, width=-(text_size + 16), height=bar_height, horizontal_scrollbar=True, no_scroll_with_mouse=True, border=False):
with dpg.group(horizontal=True): with dpg.group(horizontal=True):
@ -137,7 +262,7 @@ class PlotLayoutManager:
if path: if path:
container_tag = self._path_to_tag(path, "container") container_tag = self._path_to_tag(path, "container")
else: # Root update else: # Root update
container_tag = self.container_tag container_tag = self.parent_tag
self._cleanup_ui_recursive(layout, path) self._cleanup_ui_recursive(layout, path)
dpg.delete_item(container_tag, children_only=True) dpg.delete_item(container_tag, children_only=True)
@ -183,7 +308,7 @@ class PlotLayoutManager:
self._resize_splits_recursive(child_layout, child_path, child_width, child_height) self._resize_splits_recursive(child_layout, child_path, child_width, child_height)
else: # leaf node/panel - adjust bar height to allow for scrollbar else: # leaf node/panel - adjust bar height to allow for scrollbar
panel_tag = self._path_to_tag(path, "panel") panel_tag = self._path_to_tag(path, "panel")
if width is not None and width < int(279 * self.scale + 80): # scaled widths of the elements in top bar + fixed 8 padding on left and right of each item if width is not None and width < int(279 * self.scale + 64): # scaled widths of the elements in top bar + fixed 8 padding on left and right of each item
dpg.configure_item(panel_tag, height=(int(13 * self.scale) + 24)) dpg.configure_item(panel_tag, height=(int(13 * self.scale) + 24))
else: else:
dpg.configure_item(panel_tag, height=(int(13 * self.scale) + 8)) dpg.configure_item(panel_tag, height=(int(13 * self.scale) + 8))

@ -10,7 +10,7 @@ import signal
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.datatree import DataTree from openpilot.tools.jotpluggler.datatree import DataTree
from openpilot.tools.jotpluggler.layout import PlotLayoutManager from openpilot.tools.jotpluggler.layout import LayoutManager
DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19" DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19"
@ -127,7 +127,7 @@ class MainController:
self.worker_manager = WorkerManager() self.worker_manager = WorkerManager()
self._create_global_themes() self._create_global_themes()
self.data_tree = DataTree(self.data_manager, self.playback_manager) self.data_tree = DataTree(self.data_manager, self.playback_manager)
self.plot_layout_manager = PlotLayoutManager(self.data_manager, self.playback_manager, self.worker_manager, scale=self.scale) self.layout_manager = LayoutManager(self.data_manager, self.playback_manager, self.worker_manager, scale=self.scale)
self.data_manager.add_observer(self.on_data_loaded) self.data_manager.add_observer(self.on_data_loaded)
def _create_global_themes(self): def _create_global_themes(self):
@ -168,7 +168,7 @@ class MainController:
def setup_ui(self): def setup_ui(self):
with dpg.texture_registry(): with dpg.texture_registry():
script_dir = os.path.dirname(os.path.realpath(__file__)) script_dir = os.path.dirname(os.path.realpath(__file__))
for image in ["play", "pause", "x", "split_h", "split_v"]: for image in ["play", "pause", "x", "split_h", "split_v", "plus"]:
texture = dpg.load_image(os.path.join(script_dir, "assets", f"{image}.png")) texture = dpg.load_image(os.path.join(script_dir, "assets", f"{image}.png"))
dpg.add_static_texture(width=texture[0], height=texture[1], default_value=texture[3], tag=f"{image}_texture") dpg.add_static_texture(width=texture[0], height=texture[1], default_value=texture[3], tag=f"{image}_texture")
@ -186,7 +186,7 @@ class MainController:
# Right panel - Plots and timeline # Right panel - Plots and timeline
with dpg.group(tag="right_panel"): with dpg.group(tag="right_panel"):
with dpg.child_window(label="Plot Window", border=True, height=-(32 + 13 * self.scale), tag="main_plot_area"): with dpg.child_window(label="Plot Window", border=True, height=-(32 + 13 * self.scale), tag="main_plot_area"):
self.plot_layout_manager.create_ui("main_plot_area") self.layout_manager.create_ui("main_plot_area")
with dpg.child_window(label="Timeline", border=True): with dpg.child_window(label="Timeline", border=True):
with dpg.table(header_row=False, borders_innerH=False, borders_innerV=False, borders_outerH=False, borders_outerV=False): with dpg.table(header_row=False, borders_innerH=False, borders_innerV=False, borders_outerH=False, borders_outerV=False):
@ -205,7 +205,7 @@ class MainController:
dpg.set_primary_window("Primary Window", True) dpg.set_primary_window("Primary Window", True)
def on_plot_resize(self, sender, app_data, user_data): def on_plot_resize(self, sender, app_data, user_data):
self.plot_layout_manager.on_viewport_resize() self.layout_manager.on_viewport_resize()
def load_route(self): def load_route(self):
route_name = dpg.get_value("route_input").strip() route_name = dpg.get_value("route_input").strip()
@ -227,7 +227,7 @@ class MainController:
if not dpg.is_item_active("timeline_slider"): if not dpg.is_item_active("timeline_slider"):
dpg.set_value("timeline_slider", new_time) dpg.set_value("timeline_slider", new_time)
self.plot_layout_manager.update_all_panels() self.layout_manager.update_all_panels()
dpg.set_value("fps_counter", f"{dpg.get_frame_rate():.1f} FPS") dpg.set_value("fps_counter", f"{dpg.get_frame_rate():.1f} FPS")

@ -52,6 +52,8 @@ class TimeSeriesPanel(ViewPanel):
self._results_deque: deque[tuple[str, list, list]] = deque() self._results_deque: deque[tuple[str, list, list]] = deque()
self._new_data = False self._new_data = False
self._last_x_limits = (0.0, 0.0) self._last_x_limits = (0.0, 0.0)
self._queued_x_sync: tuple | None = None
self._queued_reallow_x_zoom = False
def create_ui(self, parent_tag: str): def create_ui(self, parent_tag: str):
self.data_manager.add_observer(self.on_data_loaded) self.data_manager.add_observer(self.on_data_loaded)
@ -63,8 +65,7 @@ class TimeSeriesPanel(ViewPanel):
timeline_series_tag = dpg.add_inf_line_series(x=[0], label="Timeline", parent=self.y_axis_tag, tag=self.timeline_indicator_tag) timeline_series_tag = dpg.add_inf_line_series(x=[0], label="Timeline", parent=self.y_axis_tag, tag=self.timeline_indicator_tag)
dpg.bind_item_theme(timeline_series_tag, "global_timeline_theme") dpg.bind_item_theme(timeline_series_tag, "global_timeline_theme")
for series_path in list(self._series_data.keys()): self._new_data = True
self.add_series(series_path)
self._ui_created = True self._ui_created = True
def update(self): def update(self):
@ -72,6 +73,19 @@ class TimeSeriesPanel(ViewPanel):
if not self._ui_created: if not self._ui_created:
return return
if self._queued_x_sync:
min_time, max_time = self._queued_x_sync
self._queued_x_sync = None
dpg.set_axis_limits(self.x_axis_tag, min_time, max_time)
self._last_x_limits = (min_time, max_time)
self._fit_y_axis(min_time, max_time)
self._queued_reallow_x_zoom = True # must wait a frame before allowing user changes so that axis limits take effect
return
if self._queued_reallow_x_zoom:
self._queued_reallow_x_zoom = False
dpg.set_axis_limits_auto(self.x_axis_tag)
current_limits = dpg.get_axis_limits(self.x_axis_tag) current_limits = dpg.get_axis_limits(self.x_axis_tag)
# downsample if plot zoom changed significantly # downsample if plot zoom changed significantly
plot_duration = current_limits[1] - current_limits[0] plot_duration = current_limits[1] - current_limits[0]
@ -112,13 +126,8 @@ class TimeSeriesPanel(ViewPanel):
def _on_x_axis_sync(self, min_time: float, max_time: float, source_panel): def _on_x_axis_sync(self, min_time: float, max_time: float, source_panel):
with self._update_lock: with self._update_lock:
if source_panel == self or not self._ui_created: if source_panel != self:
return self._queued_x_sync = (min_time, max_time)
dpg.set_axis_limits(self.x_axis_tag, min_time, max_time)
dpg.render_dearpygui_frame()
dpg.set_axis_limits_auto(self.x_axis_tag)
self._last_x_limits = (min_time, max_time)
self._fit_y_axis(min_time, max_time)
def _fit_y_axis(self, x_min: float, x_max: float): def _fit_y_axis(self, x_min: float, x_max: float):
if not self._series_data: if not self._series_data:

Loading…
Cancel
Save