|
|
@ -20,17 +20,35 @@ class LayoutManager: |
|
|
|
self.tabs: dict = {0: {"name": "Tab 1", "panel_layout": initial_panel_layout}} |
|
|
|
self.tabs: dict = {0: {"name": "Tab 1", "panel_layout": initial_panel_layout}} |
|
|
|
self._next_tab_id = self.active_tab + 1 |
|
|
|
self._next_tab_id = self.active_tab + 1 |
|
|
|
|
|
|
|
|
|
|
|
self._create_tab_themes() |
|
|
|
def to_dict(self) -> dict: |
|
|
|
|
|
|
|
return { |
|
|
|
def _create_tab_themes(self): |
|
|
|
"tabs": { |
|
|
|
for tag, color in (("active_tab_theme", (37, 37, 38, 255)), ("inactive_tab_theme", (70, 70, 75, 255))): |
|
|
|
str(tab_id): { |
|
|
|
with dpg.theme(tag=tag): |
|
|
|
"name": tab_data["name"], |
|
|
|
for cmp, target in ((dpg.mvChildWindow, dpg.mvThemeCol_ChildBg), (dpg.mvInputText, dpg.mvThemeCol_FrameBg), (dpg.mvImageButton, dpg.mvThemeCol_Button)): |
|
|
|
"panel_layout": tab_data["panel_layout"].to_dict() |
|
|
|
with dpg.theme_component(cmp): |
|
|
|
} |
|
|
|
dpg.add_theme_color(target, color) |
|
|
|
for tab_id, tab_data in self.tabs.items() |
|
|
|
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 clear_and_load_from_dict(self, data: dict): |
|
|
|
|
|
|
|
tab_ids_to_close = list(self.tabs.keys()) |
|
|
|
|
|
|
|
for tab_id in tab_ids_to_close: |
|
|
|
|
|
|
|
self.close_tab(tab_id, force=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for tab_id_str, tab_data in data["tabs"].items(): |
|
|
|
|
|
|
|
tab_id = int(tab_id_str) |
|
|
|
|
|
|
|
panel_layout = PanelLayoutManager.load_from_dict( |
|
|
|
|
|
|
|
tab_data["panel_layout"], self.data_manager, self.playback_manager, |
|
|
|
|
|
|
|
self.worker_manager, self.scale |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
self.tabs[tab_id] = { |
|
|
|
|
|
|
|
"name": tab_data["name"], |
|
|
|
|
|
|
|
"panel_layout": panel_layout |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.active_tab = min(self.tabs.keys()) if self.tabs else 0 |
|
|
|
|
|
|
|
self._next_tab_id = max(self.tabs.keys()) + 1 if self.tabs else 1 |
|
|
|
|
|
|
|
|
|
|
|
def create_ui(self, parent_tag: str): |
|
|
|
def create_ui(self, parent_tag: str): |
|
|
|
if dpg.does_item_exist(self.container_tag): |
|
|
|
if dpg.does_item_exist(self.container_tag): |
|
|
@ -52,7 +70,7 @@ class LayoutManager: |
|
|
|
|
|
|
|
|
|
|
|
def _create_tab_ui(self, tab_id: int, tab_name: str): |
|
|
|
def _create_tab_ui(self, tab_id: int, tab_name: str): |
|
|
|
text_size = int(13 * self.scale) |
|
|
|
text_size = int(13 * self.scale) |
|
|
|
tab_width = int(120 * self.scale) |
|
|
|
tab_width = int(140 * 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.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}"): |
|
|
|
with dpg.group(horizontal=True, tag=f"tab_group_{tab_id}"): |
|
|
|
dpg.add_input_text( |
|
|
|
dpg.add_input_text( |
|
|
@ -70,20 +88,21 @@ class LayoutManager: |
|
|
|
|
|
|
|
|
|
|
|
def _create_tab_content(self): |
|
|
|
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): |
|
|
|
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"] |
|
|
|
if self.active_tab in self.tabs: |
|
|
|
active_panel_layout.create_ui() |
|
|
|
active_panel_layout = self.tabs[self.active_tab]["panel_layout"] |
|
|
|
|
|
|
|
active_panel_layout.create_ui() |
|
|
|
|
|
|
|
|
|
|
|
def add_tab(self): |
|
|
|
def add_tab(self): |
|
|
|
new_panel_layout = PanelLayoutManager(self.data_manager, self.playback_manager, self.worker_manager, self.scale) |
|
|
|
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} |
|
|
|
new_tab = {"name": f"Tab {self._next_tab_id + 1}", "panel_layout": new_panel_layout} |
|
|
|
self.tabs[ self._next_tab_id] = new_tab |
|
|
|
self.tabs[self._next_tab_id] = new_tab |
|
|
|
self._create_tab_ui( self._next_tab_id, new_tab["name"]) |
|
|
|
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 |
|
|
|
dpg.move_item("add_tab_button", parent="tab_bar_group") # move plus button to end |
|
|
|
self.switch_tab( self._next_tab_id) |
|
|
|
self.switch_tab(self._next_tab_id) |
|
|
|
self._next_tab_id += 1 |
|
|
|
self._next_tab_id += 1 |
|
|
|
|
|
|
|
|
|
|
|
def close_tab(self, tab_id: int): |
|
|
|
def close_tab(self, tab_id: int, force = False): |
|
|
|
if len(self.tabs) <= 1: |
|
|
|
if len(self.tabs) <= 1 and not force: |
|
|
|
return # don't allow closing the last tab |
|
|
|
return # don't allow closing the last tab |
|
|
|
|
|
|
|
|
|
|
|
tab_to_close = self.tabs[tab_id] |
|
|
|
tab_to_close = self.tabs[tab_id] |
|
|
@ -94,7 +113,7 @@ class LayoutManager: |
|
|
|
dpg.delete_item(tag) |
|
|
|
dpg.delete_item(tag) |
|
|
|
del self.tabs[tab_id] |
|
|
|
del self.tabs[tab_id] |
|
|
|
|
|
|
|
|
|
|
|
if self.active_tab == tab_id: # switch to another tab if we closed the active one |
|
|
|
if self.active_tab == tab_id and self.tabs: # switch to another tab if we closed the active one |
|
|
|
self.active_tab = next(iter(self.tabs.keys())) |
|
|
|
self.active_tab = next(iter(self.tabs.keys())) |
|
|
|
self._switch_tab_content() |
|
|
|
self._switch_tab_content() |
|
|
|
dpg.bind_item_theme(f"tab_window_{self.active_tab}", "active_tab_theme") |
|
|
|
dpg.bind_item_theme(f"tab_window_{self.active_tab}", "active_tab_theme") |
|
|
@ -134,6 +153,8 @@ class PanelLayoutManager: |
|
|
|
self.scale = scale |
|
|
|
self.scale = scale |
|
|
|
self.active_panels: list = [] |
|
|
|
self.active_panels: list = [] |
|
|
|
self.parent_tag = "tab_content_area" |
|
|
|
self.parent_tag = "tab_content_area" |
|
|
|
|
|
|
|
self._queue_resize = False |
|
|
|
|
|
|
|
self._created_handler_tags: set[str] = set() |
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
@ -141,19 +162,70 @@ class PanelLayoutManager: |
|
|
|
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 to_dict(self) -> dict: |
|
|
|
|
|
|
|
return self._layout_to_dict(self.layout) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _layout_to_dict(self, layout: dict) -> dict: |
|
|
|
|
|
|
|
if layout["type"] == "panel": |
|
|
|
|
|
|
|
return { |
|
|
|
|
|
|
|
"type": "panel", |
|
|
|
|
|
|
|
"panel": layout["panel"].to_dict() |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
else: # split |
|
|
|
|
|
|
|
return { |
|
|
|
|
|
|
|
"type": "split", |
|
|
|
|
|
|
|
"orientation": layout["orientation"], |
|
|
|
|
|
|
|
"proportions": layout["proportions"], |
|
|
|
|
|
|
|
"children": [self._layout_to_dict(child) for child in layout["children"]] |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod |
|
|
|
|
|
|
|
def load_from_dict(cls, data: dict, data_manager, playback_manager, worker_manager, scale: float = 1.0): |
|
|
|
|
|
|
|
manager = cls(data_manager, playback_manager, worker_manager, scale) |
|
|
|
|
|
|
|
manager.layout = manager._dict_to_layout(data) |
|
|
|
|
|
|
|
return manager |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _dict_to_layout(self, data: dict) -> dict: |
|
|
|
|
|
|
|
if data["type"] == "panel": |
|
|
|
|
|
|
|
panel_data = data["panel"] |
|
|
|
|
|
|
|
if panel_data["type"] == "timeseries": |
|
|
|
|
|
|
|
panel = TimeSeriesPanel.load_from_dict( |
|
|
|
|
|
|
|
panel_data, self.data_manager, self.playback_manager, self.worker_manager |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
return {"type": "panel", "panel": panel} |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
# Handle future panel types here or make a general mapping |
|
|
|
|
|
|
|
raise ValueError(f"Unknown panel type: {panel_data['type']}") |
|
|
|
|
|
|
|
else: # split |
|
|
|
|
|
|
|
return { |
|
|
|
|
|
|
|
"type": "split", |
|
|
|
|
|
|
|
"orientation": data["orientation"], |
|
|
|
|
|
|
|
"proportions": data["proportions"], |
|
|
|
|
|
|
|
"children": [self._dict_to_layout(child) for child in data["children"]] |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
def create_ui(self): |
|
|
|
def create_ui(self): |
|
|
|
self.active_panels.clear() |
|
|
|
self.active_panels.clear() |
|
|
|
|
|
|
|
|
|
|
|
if dpg.does_item_exist(self.parent_tag): |
|
|
|
if dpg.does_item_exist(self.parent_tag): |
|
|
|
dpg.delete_item(self.parent_tag, children_only=True) |
|
|
|
dpg.delete_item(self.parent_tag, children_only=True) |
|
|
|
|
|
|
|
self._cleanup_all_handlers() |
|
|
|
|
|
|
|
|
|
|
|
container_width, container_height = dpg.get_item_rect_size(self.parent_tag) |
|
|
|
container_width, container_height = dpg.get_item_rect_size(self.parent_tag) |
|
|
|
|
|
|
|
if container_width == 0 and container_height == 0: |
|
|
|
|
|
|
|
self._queue_resize = True |
|
|
|
self._create_ui_recursive(self.layout, self.parent_tag, [], container_width, container_height) |
|
|
|
self._create_ui_recursive(self.layout, self.parent_tag, [], container_width, container_height) |
|
|
|
|
|
|
|
|
|
|
|
def destroy_ui(self): |
|
|
|
def destroy_ui(self): |
|
|
|
self._cleanup_ui_recursive(self.layout, []) |
|
|
|
self._cleanup_ui_recursive(self.layout, []) |
|
|
|
|
|
|
|
self._cleanup_all_handlers() |
|
|
|
self.active_panels.clear() |
|
|
|
self.active_panels.clear() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _cleanup_all_handlers(self): |
|
|
|
|
|
|
|
for handler_tag in list(self._created_handler_tags): |
|
|
|
|
|
|
|
if dpg.does_item_exist(handler_tag): |
|
|
|
|
|
|
|
dpg.delete_item(handler_tag) |
|
|
|
|
|
|
|
self._created_handler_tags.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": |
|
|
|
self._create_panel_ui(layout, parent_tag, path, width, height) |
|
|
|
self._create_panel_ui(layout, parent_tag, path, width, height) |
|
|
@ -165,13 +237,14 @@ class PanelLayoutManager: |
|
|
|
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 + 64) else (text_size + 8) # adjust height to allow for scrollbar |
|
|
|
bar_height = (text_size + 24) if width < int(329 * self.scale + 64) else (text_size + 8) # adjust height to allow for scrollbar |
|
|
|
|
|
|
|
|
|
|
|
with dpg.child_window(parent=parent_tag, border=False, 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): |
|
|
|
dpg.add_input_text(default_value=panel.title, width=int(100 * self.scale), callback=lambda s, v: setattr(panel, "title", v)) |
|
|
|
# if you change the widths make sure to change the sum of widths (currently 329 * scale) |
|
|
|
|
|
|
|
dpg.add_input_text(default_value=panel.title, width=int(150 * 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_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="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_h_texture", callback=lambda: self.split_panel(path, 0), width=text_size, height=text_size) |
|
|
@ -280,11 +353,16 @@ class PanelLayoutManager: |
|
|
|
handler_tag = f"{self._path_to_tag(path, f'grip_{i}')}_handler" |
|
|
|
handler_tag = f"{self._path_to_tag(path, f'grip_{i}')}_handler" |
|
|
|
if dpg.does_item_exist(handler_tag): |
|
|
|
if dpg.does_item_exist(handler_tag): |
|
|
|
dpg.delete_item(handler_tag) |
|
|
|
dpg.delete_item(handler_tag) |
|
|
|
|
|
|
|
self._created_handler_tags.discard(handler_tag) |
|
|
|
|
|
|
|
|
|
|
|
for i, child in enumerate(layout["children"]): |
|
|
|
for i, child in enumerate(layout["children"]): |
|
|
|
self._cleanup_ui_recursive(child, path + [i]) |
|
|
|
self._cleanup_ui_recursive(child, path + [i]) |
|
|
|
|
|
|
|
|
|
|
|
def update_all_panels(self): |
|
|
|
def update_all_panels(self): |
|
|
|
|
|
|
|
if self._queue_resize: |
|
|
|
|
|
|
|
if (size := dpg.get_item_rect_size(self.parent_tag)) != [0, 0]: |
|
|
|
|
|
|
|
self._queue_resize = False |
|
|
|
|
|
|
|
self._resize_splits_recursive(self.layout, [], *size) |
|
|
|
for panel in self.active_panels: |
|
|
|
for panel in self.active_panels: |
|
|
|
panel.update() |
|
|
|
panel.update() |
|
|
|
|
|
|
|
|
|
|
@ -308,7 +386,7 @@ class PanelLayoutManager: |
|
|
|
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 + 64): # 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(329 * 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)) |
|
|
@ -342,16 +420,18 @@ class PanelLayoutManager: |
|
|
|
|
|
|
|
|
|
|
|
def _create_grip(self, parent_tag: str, path: list[int], grip_index: int, orientation: int): |
|
|
|
def _create_grip(self, parent_tag: str, path: list[int], grip_index: int, orientation: int): |
|
|
|
grip_tag = self._path_to_tag(path, f"grip_{grip_index}") |
|
|
|
grip_tag = self._path_to_tag(path, f"grip_{grip_index}") |
|
|
|
|
|
|
|
handler_tag = f"{grip_tag}_handler" |
|
|
|
width, height = [(self.grip_size, -1), (-1, self.grip_size)][orientation] |
|
|
|
width, height = [(self.grip_size, -1), (-1, self.grip_size)][orientation] |
|
|
|
|
|
|
|
|
|
|
|
with dpg.child_window(tag=grip_tag, parent=parent_tag, width=width, height=height, no_scrollbar=True, border=False): |
|
|
|
with dpg.child_window(tag=grip_tag, parent=parent_tag, width=width, height=height, no_scrollbar=True, border=False): |
|
|
|
button_tag = dpg.add_button(label="", width=-1, height=-1) |
|
|
|
button_tag = dpg.add_button(label="", width=-1, height=-1) |
|
|
|
|
|
|
|
|
|
|
|
with dpg.item_handler_registry(tag=f"{grip_tag}_handler"): |
|
|
|
with dpg.item_handler_registry(tag=handler_tag): |
|
|
|
user_data = (path, grip_index, orientation) |
|
|
|
user_data = (path, grip_index, orientation) |
|
|
|
dpg.add_item_active_handler(callback=self._on_grip_drag, user_data=user_data) |
|
|
|
dpg.add_item_active_handler(callback=self._on_grip_drag, user_data=user_data) |
|
|
|
dpg.add_item_deactivated_handler(callback=self._on_grip_end, user_data=user_data) |
|
|
|
dpg.add_item_deactivated_handler(callback=self._on_grip_end, user_data=user_data) |
|
|
|
dpg.bind_item_handler_registry(button_tag, f"{grip_tag}_handler") |
|
|
|
dpg.bind_item_handler_registry(button_tag, handler_tag) |
|
|
|
|
|
|
|
self._created_handler_tags.add(handler_tag) |
|
|
|
|
|
|
|
|
|
|
|
def _on_grip_drag(self, sender, app_data, user_data): |
|
|
|
def _on_grip_drag(self, sender, app_data, user_data): |
|
|
|
path, grip_index, orientation = user_data |
|
|
|
path, grip_index, orientation = user_data |
|
|
|