jotpluggler: add icons, use monospace font, and fix ui quirks (#36141)
* use play/pause icons * use monospace font * x button for delete * add icons for splitting * many scaling + scrollbar fixes and niceties * simplify texture loading code
This commit is contained in:
BIN
tools/jotpluggler/assets/pause.png
LFS
Normal file
BIN
tools/jotpluggler/assets/pause.png
LFS
Normal file
Binary file not shown.
BIN
tools/jotpluggler/assets/play.png
LFS
Normal file
BIN
tools/jotpluggler/assets/play.png
LFS
Normal file
Binary file not shown.
BIN
tools/jotpluggler/assets/split_h.png
LFS
Normal file
BIN
tools/jotpluggler/assets/split_h.png
LFS
Normal file
Binary file not shown.
BIN
tools/jotpluggler/assets/split_v.png
LFS
Normal file
BIN
tools/jotpluggler/assets/split_v.png
LFS
Normal file
Binary file not shown.
BIN
tools/jotpluggler/assets/x.png
LFS
Normal file
BIN
tools/jotpluggler/assets/x.png
LFS
Normal file
Binary file not shown.
@@ -34,7 +34,7 @@ class DataTree:
|
||||
self._path_to_node: dict[str, DataTreeNode] = {} # full_path -> node
|
||||
self._expanded_tags: set[str] = set()
|
||||
self._item_handlers: dict[str, str] = {} # ui_tag -> handler_tag
|
||||
self._avg_char_width = None
|
||||
self._char_width = None
|
||||
self._queued_search = None
|
||||
self._new_data = False
|
||||
self._ui_lock = threading.RLock()
|
||||
@@ -43,12 +43,13 @@ class DataTree:
|
||||
|
||||
def create_ui(self, parent_tag: str):
|
||||
with dpg.child_window(parent=parent_tag, border=False, width=-1, height=-1):
|
||||
dpg.add_text("Available Data")
|
||||
dpg.add_text("Timeseries List")
|
||||
dpg.add_separator()
|
||||
dpg.add_input_text(tag="search_input", width=-1, hint="Search fields...", callback=self.search_data)
|
||||
dpg.add_separator()
|
||||
with dpg.group(tag="data_tree_container"):
|
||||
pass
|
||||
with dpg.child_window(border=False, width=-1, height=-1):
|
||||
with dpg.group(tag="data_tree_container"):
|
||||
pass
|
||||
|
||||
def _on_data_loaded(self, data: dict):
|
||||
with self._ui_lock:
|
||||
@@ -64,8 +65,9 @@ class DataTree:
|
||||
self._handlers_to_delete.clear()
|
||||
|
||||
with self._ui_lock:
|
||||
if self._avg_char_width is None and dpg.is_dearpygui_running():
|
||||
self._avg_char_width = self.calculate_avg_char_width(font)
|
||||
if self._char_width is None:
|
||||
if size := dpg.get_text_size(" ", font=font):
|
||||
self._char_width = size[0]
|
||||
|
||||
if self._new_data:
|
||||
self._process_path_change()
|
||||
@@ -256,10 +258,10 @@ class DataTree:
|
||||
value_tag = f"value_{path}"
|
||||
if not dpg.does_item_exist(value_tag):
|
||||
return
|
||||
value_column_width = dpg.get_item_rect_size("sidebar_window")[0] // 2
|
||||
value_column_width = dpg.get_item_rect_size(f"leaf_{path}")[0] // 2
|
||||
value = self.data_manager.get_value_at(path, self.playback_manager.current_time_s)
|
||||
if value is not None:
|
||||
formatted_value = self.format_and_truncate(value, value_column_width, self._avg_char_width)
|
||||
formatted_value = self.format_and_truncate(value, value_column_width, self._char_width)
|
||||
dpg.set_value(value_tag, formatted_value)
|
||||
else:
|
||||
dpg.set_value(value_tag, "N/A")
|
||||
@@ -305,16 +307,9 @@ class DataTree:
|
||||
yield f"{child_name_lower}/{path}"
|
||||
|
||||
@staticmethod
|
||||
def calculate_avg_char_width(font):
|
||||
sample_text = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
if size := dpg.get_text_size(sample_text, font=font):
|
||||
return size[0] / len(sample_text)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def format_and_truncate(value, available_width: float, avg_char_width: float) -> str:
|
||||
def format_and_truncate(value, available_width: float, char_width: float) -> str:
|
||||
s = f"{value:.5f}" if np.issubdtype(type(value), np.floating) else str(value)
|
||||
max_chars = int(available_width / avg_char_width) - 3
|
||||
max_chars = int(available_width / char_width)
|
||||
if len(s) > max_chars:
|
||||
return s[: max(0, max_chars)] + "..."
|
||||
return s[: max(0, max_chars - 3)] + "..."
|
||||
return s
|
||||
|
||||
@@ -25,29 +25,33 @@ class PlotLayoutManager:
|
||||
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):
|
||||
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):
|
||||
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 _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)
|
||||
self._create_panel_ui(layout, parent_tag, path, width, height)
|
||||
else:
|
||||
self._create_split_ui(layout, parent_tag, path, width, height)
|
||||
|
||||
def _create_panel_ui(self, layout: dict, parent_tag: str, path: list[int]):
|
||||
def _create_panel_ui(self, layout: dict, parent_tag: str, path: list[int], width: int, height:int):
|
||||
panel_tag = self._path_to_tag(path, "panel")
|
||||
panel = layout["panel"]
|
||||
self.active_panels.append(panel)
|
||||
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
|
||||
|
||||
with dpg.child_window(tag=panel_tag, parent=parent_tag, border=True, width=-1, height=-1, no_scrollbar=True):
|
||||
with dpg.child_window(parent=parent_tag, border=True, width=-1, height=-1, no_scrollbar=True):
|
||||
with dpg.group(horizontal=True):
|
||||
dpg.add_input_text(default_value=panel.title, width=int(100 * self.scale), callback=lambda s, v: setattr(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(panel), width=int(40 * self.scale))
|
||||
dpg.add_button(label="Delete", callback=lambda: self.delete_panel(path), width=int(40 * self.scale))
|
||||
dpg.add_button(label="Split H", callback=lambda: self.split_panel(path, 0), width=int(40 * self.scale))
|
||||
dpg.add_button(label="Split V", callback=lambda: self.split_panel(path, 1), width=int(40 * self.scale))
|
||||
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):
|
||||
dpg.add_input_text(default_value=panel.title, width=int(100 * self.scale), callback=lambda s, v: setattr(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(panel), width=int(40 * self.scale))
|
||||
dpg.add_image_button(texture_tag="split_h_texture", callback=lambda: self.split_panel(path, 0), width=text_size, height=text_size)
|
||||
dpg.add_image_button(texture_tag="split_v_texture", callback=lambda: self.split_panel(path, 1), width=text_size, height=text_size)
|
||||
dpg.add_image_button(texture_tag="x_texture", callback=lambda: self.delete_panel(path), width=text_size, height=text_size)
|
||||
|
||||
dpg.add_separator()
|
||||
|
||||
@@ -177,11 +181,17 @@ class PlotLayoutManager:
|
||||
dpg.configure_item(container_tag, **{size_properties[orientation]: pane_sizes[i]})
|
||||
child_width, child_height = [(pane_sizes[i], available_sizes[1]), (available_sizes[0], pane_sizes[i])][orientation]
|
||||
self._resize_splits_recursive(child_layout, child_path, child_width, child_height)
|
||||
else: # leaf node/panel - adjust bar height to allow for scrollbar
|
||||
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
|
||||
dpg.configure_item(panel_tag, height=(int(13*self.scale) + 24))
|
||||
else:
|
||||
dpg.configure_item(panel_tag, height=(int(13*self.scale) + 8))
|
||||
|
||||
def _get_split_geometry(self, layout: dict, available_size: tuple[int, int]) -> tuple[int, int, list[int]]:
|
||||
orientation = layout["orientation"]
|
||||
num_grips = len(layout["children"]) - 1
|
||||
usable_size = max(self.min_pane_size, available_size[orientation] - (num_grips * self.grip_size))
|
||||
usable_size = max(self.min_pane_size, available_size[orientation] - (num_grips * (self.grip_size + 8 * (2-orientation)))) # approximate, scaling is weird
|
||||
pane_sizes = [max(self.min_pane_size, int(usable_size * prop)) for prop in layout["proportions"]]
|
||||
return orientation, usable_size, pane_sizes
|
||||
|
||||
|
||||
@@ -73,9 +73,10 @@ class PlaybackManager:
|
||||
if not self.is_playing and self.current_time_s >= self.duration_s:
|
||||
self.seek(0.0)
|
||||
self.is_playing = not self.is_playing
|
||||
texture_tag = "pause_texture" if self.is_playing else "play_texture"
|
||||
dpg.configure_item("play_pause_button", texture_tag=texture_tag)
|
||||
|
||||
def seek(self, time_s: float):
|
||||
self.is_playing = False
|
||||
self.current_time_s = max(0.0, min(time_s, self.duration_s))
|
||||
|
||||
def update_time(self, delta_t: float):
|
||||
@@ -83,6 +84,7 @@ class PlaybackManager:
|
||||
self.current_time_s = min(self.current_time_s + delta_t, self.duration_s)
|
||||
if self.current_time_s >= self.duration_s:
|
||||
self.is_playing = False
|
||||
dpg.configure_item("play_pause_button", texture_tag="play_texture")
|
||||
return self.current_time_s
|
||||
|
||||
|
||||
@@ -109,7 +111,6 @@ class MainController:
|
||||
dpg.add_theme_style(dpg.mvPlotStyleVar_LineWeight, scaled_thickness, category=dpg.mvThemeCat_Plots)
|
||||
dpg.add_theme_color(dpg.mvPlotCol_Line, (255, 0, 0, 128), category=dpg.mvThemeCat_Plots)
|
||||
|
||||
|
||||
def on_data_loaded(self, data: dict):
|
||||
duration = data.get('duration', 0.0)
|
||||
self.playback_manager.set_route_duration(duration)
|
||||
@@ -121,7 +122,7 @@ class MainController:
|
||||
dpg.set_value("load_status", "Loading...")
|
||||
dpg.set_value("timeline_slider", 0.0)
|
||||
dpg.configure_item("timeline_slider", max_value=0.0)
|
||||
dpg.configure_item("play_pause_button", label="Play")
|
||||
dpg.configure_item("play_pause_button", texture_tag="play_texture")
|
||||
dpg.configure_item("load_button", enabled=True)
|
||||
elif data.get('loading_complete'):
|
||||
num_paths = len(self.data_manager.get_all_paths())
|
||||
@@ -134,6 +135,12 @@ class MainController:
|
||||
dpg.configure_item("timeline_slider", max_value=duration)
|
||||
|
||||
def setup_ui(self):
|
||||
with dpg.texture_registry():
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
for image in ["play", "pause", "x", "split_h", "split_v"]:
|
||||
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")
|
||||
|
||||
with dpg.window(tag="Primary Window"):
|
||||
with dpg.group(horizontal=True):
|
||||
# Left panel - Data tree
|
||||
@@ -147,16 +154,17 @@ class MainController:
|
||||
|
||||
# Right panel - Plots and timeline
|
||||
with dpg.group(tag="right_panel"):
|
||||
with dpg.child_window(label="Plot Window", border=True, height=-(30 + 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")
|
||||
|
||||
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):
|
||||
dpg.add_table_column(width_fixed=True, init_width_or_weight=int(50 * self.scale)) # Play button
|
||||
btn_size = int(13 * self.scale)
|
||||
dpg.add_table_column(width_fixed=True, init_width_or_weight=(btn_size + 8)) # Play button
|
||||
dpg.add_table_column(width_stretch=True) # Timeline slider
|
||||
dpg.add_table_column(width_fixed=True, init_width_or_weight=int(50 * self.scale)) # FPS counter
|
||||
with dpg.table_row():
|
||||
dpg.add_button(label="Play", tag="play_pause_button", callback=self.toggle_play_pause, width=int(50 * self.scale))
|
||||
dpg.add_image_button(texture_tag="play_texture", tag="play_pause_button", callback=self.toggle_play_pause, width=btn_size, height=btn_size)
|
||||
dpg.add_slider_float(tag="timeline_slider", default_value=0.0, label="", width=-1, callback=self.timeline_drag)
|
||||
dpg.add_text("", tag="fps_counter")
|
||||
with dpg.item_handler_registry(tag="plot_resize_handler"):
|
||||
@@ -177,12 +185,9 @@ class MainController:
|
||||
|
||||
def toggle_play_pause(self, sender):
|
||||
self.playback_manager.toggle_play_pause()
|
||||
label = "Pause" if self.playback_manager.is_playing else "Play"
|
||||
dpg.configure_item(sender, label=label)
|
||||
|
||||
def timeline_drag(self, sender, app_data):
|
||||
self.playback_manager.seek(app_data)
|
||||
dpg.configure_item("play_pause_button", label="Play")
|
||||
|
||||
def update_frame(self, font):
|
||||
self.data_tree.update_frame(font)
|
||||
@@ -210,7 +215,7 @@ def main(route_to_load=None):
|
||||
scale = 1
|
||||
|
||||
with dpg.font_registry():
|
||||
default_font = dpg.add_font(os.path.join(BASEDIR, "selfdrive/assets/fonts/Inter-Regular.ttf"), int(13 * scale))
|
||||
default_font = dpg.add_font(os.path.join(BASEDIR, "selfdrive/assets/fonts/JetBrainsMono-Medium.ttf"), int(13 * scale))
|
||||
dpg.bind_font(default_font)
|
||||
|
||||
viewport_width, viewport_height = int(1200 * scale), int(800 * scale)
|
||||
|
||||
Reference in New Issue
Block a user