From 5f3c593640e490d00130c3ac2fca1c4d82a86af6 Mon Sep 17 00:00:00 2001 From: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Date: Tue, 7 Apr 2026 00:01:31 -0400 Subject: [PATCH] BigUI WIP: We need tabs --- .../layouts/settings/starpilot/aethergrid.py | 50 ++++++-- .../ui/layouts/settings/starpilot/data.py | 7 +- .../ui/layouts/settings/starpilot/device.py | 5 +- .../layouts/settings/starpilot/main_panel.py | 77 ++++-------- .../ui/layouts/settings/starpilot/panel.py | 8 +- .../ui/layouts/settings/starpilot/sounds.py | 113 ++++++++++++------ .../settings/starpilot/system_settings.py | 86 +++++++++++++ .../layouts/settings/starpilot/utilities.py | 2 + 8 files changed, 237 insertions(+), 111 deletions(-) create mode 100644 selfdrive/ui/layouts/settings/starpilot/system_settings.py diff --git a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py index 2e76619fd..77342bd7c 100644 --- a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py +++ b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py @@ -83,8 +83,7 @@ class AetherTile(Widget): self._is_pressed = False def _handle_mouse_event(self, mouse_event): - if self._is_pressed and not rl.check_collision_point_rec(mouse_event.pos, self._hit_rect): - self._is_pressed = False + if not rl.check_collision_point_rec(mouse_event.pos, self._hit_rect): self._plate_target = 0.0 def _animate_plate(self, dt: float): @@ -216,35 +215,47 @@ class HubTile(AetherTile): class ToggleTile(AetherTile): def __init__(self, title: str, get_state: Callable[[], bool], set_state: Callable[[bool], None], icon_path: str | None = None, - bg_color: rl.Color | str | None = None, desc: str = ""): + bg_color: rl.Color | str | None = None, desc: str = "", is_enabled: Callable[[], bool] | None = None, disabled_label: str = ""): if bg_color: super().__init__(surface_color=bg_color) else: super().__init__(surface_color=rl.Color(0, 163, 0, 255)) self.title = title self.desc = desc self.get_state = get_state self.set_state = set_state + self.set_enabled(is_enabled or True) self._icon = gui_app.starpilot_texture(icon_path, 80, 80) if icon_path else None self._font = gui_app.font(FontWeight.BOLD) self._font_desc = gui_app.font(FontWeight.NORMAL) self._active_color = self.surface_color self._inactive_color = rl.Color(120, 120, 120, 255) + self._disabled_color = rl.Color(75, 75, 75, 255) + self._disabled_label = disabled_label def _handle_mouse_release(self, mouse_pos: MousePos): if self._is_pressed: - if rl.check_collision_point_rec(mouse_pos, self._hit_rect): + if rl.check_collision_point_rec(mouse_pos, self._hit_rect) and self.enabled: self.set_state(not self.get_state()) self._plate_target = 0.0 self._is_pressed = False def _render(self, rect: rl.Rectangle): + enabled = self.enabled active = self.get_state() - self.surface_color = self._active_color if active else self._inactive_color + if enabled: + self.surface_color = self._active_color if active else self._inactive_color + else: + self.surface_color = self._disabled_color + self._plate_offset = 0.0 + self._plate_target = 0.0 face = self._render_layers(rect) line_heights = [28, 30] _, ty = self._centered_content(face, self._icon, 0.75, 28, len(line_heights), line_heights) max_w = face.width - 40 self._draw_text_fit(self._font, self.title, rl.Vector2(face.x + 20, ty), max_w, 28, align_center=True, uppercase=True) - state_text = tr("ON") if active else tr("OFF") + if enabled: + state_text = tr("ON") if active else tr("OFF") + else: + state_text = tr(self._disabled_label) if self._disabled_label else tr("LOCKED") self._draw_text_fit(self._font, state_text, rl.Vector2(face.x + 20, ty + 28 + 8), max_w, 30, align_center=True, uppercase=True) if self.desc: @@ -557,10 +568,15 @@ class RadioTileGroup(Widget): self._option_targets.append(0.0) for i in range(len(self._option_offsets)): self._option_offsets[i] += (self._option_targets[i] - self._option_offsets[i]) * (1 - math.exp(-dt / PLATE_TAU)) - title_size = measure_text_cached(self._font_title, self.title, 40) - rl.draw_text_ex(self._font_title, self.title, rl.Vector2(round(rect.x), round(rect.y + (rect.height - title_size.y) / 2)), 40, 0, rl.WHITE) - padding, option_w = 20, 200 - start_x = rect.x + rect.width - (len(self.options) * (option_w + padding)) + padding = 20 + option_w = 240 if len(self.options) <= 3 else 200 + total_width = len(self.options) * option_w + max(0, len(self.options) - 1) * padding + if self.title: + title_size = measure_text_cached(self._font_title, self.title, 40) + rl.draw_text_ex(self._font_title, self.title, rl.Vector2(round(rect.x), round(rect.y + (rect.height - title_size.y) / 2)), 40, 0, rl.WHITE) + start_x = rect.x + rect.width - total_width + else: + start_x = rect.x + (rect.width - total_width) / 2 for i, opt in enumerate(self.options): r = rl.Rectangle(start_x + i * (option_w + padding), rect.y, option_w, rect.height) self._option_rects.append(r) @@ -582,9 +598,10 @@ class RadioTileGroup(Widget): class TileGrid(Widget): - def __init__(self, columns: int | None = None, padding: int = 20): + def __init__(self, columns: int | None = None, padding: int = 20, uniform_width: bool = False): super().__init__() self._columns, self.padding, self.tiles = columns, padding, [] + self._uniform_width = uniform_width def add_tile(self, tile: Widget): self.tiles.append(tile) @@ -607,13 +624,20 @@ class TileGrid(Widget): else: cols = 4 rows = (count + cols - 1) // cols tile_h = (rect.height - (self.padding * (rows - 1))) / rows + uniform_tile_w = (rect.width - (self.padding * (cols - 1))) / cols if self._uniform_width else 0 tile_idx = 0 for r in range(rows): remaining = count - tile_idx if remaining <= 0: break items_in_row = min(cols, remaining) - row_tile_w = (rect.width - (self.padding * (items_in_row - 1))) / items_in_row + if self._uniform_width: + row_tile_w = uniform_tile_w + row_width = (row_tile_w * items_in_row) + (self.padding * (items_in_row - 1)) + row_x = rect.x + (rect.width - row_width) / 2 + else: + row_tile_w = (rect.width - (self.padding * (items_in_row - 1))) / items_in_row + row_x = rect.x for c in range(items_in_row): tile = tiles_to_render[tile_idx] - tile.render(rl.Rectangle(rect.x + c * (row_tile_w + self.padding), rect.y + r * (tile_h + self.padding), row_tile_w, tile_h)) + tile.render(rl.Rectangle(row_x + c * (row_tile_w + self.padding), rect.y + r * (tile_h + self.padding), row_tile_w, tile_h)) tile_idx += 1 diff --git a/selfdrive/ui/layouts/settings/starpilot/data.py b/selfdrive/ui/layouts/settings/starpilot/data.py index d7958bc87..29f21528b 100644 --- a/selfdrive/ui/layouts/settings/starpilot/data.py +++ b/selfdrive/ui/layouts/settings/starpilot/data.py @@ -13,6 +13,7 @@ from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dial from openpilot.system.ui.widgets.selection_dialog import SelectionDialog from openpilot.system.ui.widgets.input_dialog import InputDialog from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel +from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid LEGACY_STARPILOT_PARAM_RENAMES = { "FrogPilotApiToken": "StarPilotApiToken", @@ -51,6 +52,7 @@ class StarPilotDataLayout(StarPilotPanel): "toggle_backups": StarPilotToggleBackupsLayout(), "storage": StarPilotStorageLayout(), } + self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True) for name, panel in self._sub_panels.items(): if hasattr(panel, 'set_navigate_callback'): @@ -91,6 +93,7 @@ class StarPilotDataLayout(StarPilotPanel): class StarPilotBackupsLayout(StarPilotPanel): def __init__(self): super().__init__() + self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True) self.CATEGORIES = [ {"title": tr_noop("Create Backup"), "type": "hub", "on_click": self._on_create_backup, "color": "#D43D8A"}, {"title": tr_noop("Restore Backup"), "type": "hub", "on_click": self._on_restore_backup, "color": "#D43D8A"}, @@ -158,6 +161,7 @@ class StarPilotBackupsLayout(StarPilotPanel): class StarPilotToggleBackupsLayout(StarPilotPanel): def __init__(self): super().__init__() + self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True) self.CATEGORIES = [ {"title": tr_noop("Create Toggle Backup"), "type": "hub", "on_click": self._on_create, "color": "#D43D8A"}, {"title": tr_noop("Restore Toggle Backup"), "type": "hub", "on_click": self._on_restore, "color": "#D43D8A"}, @@ -232,8 +236,9 @@ class StarPilotToggleBackupsLayout(StarPilotPanel): class StarPilotStorageLayout(StarPilotPanel): def __init__(self): super().__init__() + self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True) self.CATEGORIES = [ - {"title": tr_noop("Driving Data"), "type": "value", "get_value": self._get_storage, "on_click": lambda: None, "color": "#D43D8A"}, + {"title": tr_noop("Driving Data"), "type": "value", "get_value": self._get_storage, "on_click": lambda: None, "color": "#D43D8A", "is_enabled": lambda: False}, ] self._rebuild_grid() diff --git a/selfdrive/ui/layouts/settings/starpilot/device.py b/selfdrive/ui/layouts/settings/starpilot/device.py index ceba76206..799c08a3e 100644 --- a/selfdrive/ui/layouts/settings/starpilot/device.py +++ b/selfdrive/ui/layouts/settings/starpilot/device.py @@ -9,7 +9,7 @@ from openpilot.system.ui.widgets import DialogResult from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.widgets.selection_dialog import SelectionDialog from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel -from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import AetherSliderDialog +from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import AetherSliderDialog, TileGrid class StarPilotDeviceLayout(StarPilotPanel): @@ -61,6 +61,7 @@ class StarPilotDeviceLayout(StarPilotPanel): "screen": StarPilotScreenLayout(), "device_settings": StarPilotDeviceManagementLayout(), } + self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True) for name, panel in self._sub_panels.items(): if hasattr(panel, 'set_navigate_callback'): @@ -128,6 +129,7 @@ class StarPilotDeviceLayout(StarPilotPanel): class StarPilotScreenLayout(StarPilotPanel): def __init__(self): super().__init__() + self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True) self.CATEGORIES = [ { "title": tr_noop("Screen Settings"), @@ -222,6 +224,7 @@ class StarPilotScreenLayout(StarPilotPanel): class StarPilotDeviceManagementLayout(StarPilotPanel): def __init__(self): super().__init__() + self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True) self.CATEGORIES = [ { "title": tr_noop("Device Settings"), diff --git a/selfdrive/ui/layouts/settings/starpilot/main_panel.py b/selfdrive/ui/layouts/settings/starpilot/main_panel.py index fa7fa29ba..62d99b638 100644 --- a/selfdrive/ui/layouts/settings/starpilot/main_panel.py +++ b/selfdrive/ui/layouts/settings/starpilot/main_panel.py @@ -16,6 +16,7 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.maps import StarPilotMaps from openpilot.selfdrive.ui.layouts.settings.starpilot.navigation import StarPilotNavigationLayout from openpilot.selfdrive.ui.layouts.settings.starpilot.data import StarPilotDataLayout from openpilot.selfdrive.ui.layouts.settings.starpilot.device import StarPilotDeviceLayout +from openpilot.selfdrive.ui.layouts.settings.starpilot.system_settings import StarPilotSystemLayout from openpilot.selfdrive.ui.layouts.settings.starpilot.utilities import StarPilotUtilitiesLayout from openpilot.selfdrive.ui.layouts.settings.starpilot.visuals import StarPilotVisualsLayout from openpilot.selfdrive.ui.layouts.settings.starpilot.themes import StarPilotThemesLayout @@ -52,8 +53,8 @@ class StarPilotLayout(Widget): { "title": "System Settings", "icon": "icon_system.png", - "desc": "Manage backups, device settings, screen options, storage, and tools to keep StarPilot running smoothly.", - "buttons": [("DATA", "DATA", 0), ("DEVICE CONTROLS", "DEVICE", 0), ("UTILITIES", "UTILITIES", 0)], + "desc": "Adjust device behavior, manage storage and backups, and access maintenance tools from one touch-friendly settings page.", + "buttons": [("MANAGE", "SYSTEM", 0)], "color": "#D946EF", }, { @@ -87,6 +88,7 @@ class StarPilotLayout(Widget): self._panels = { StarPilotPanelType.MAIN: StarPilotPanelInfo("", None), StarPilotPanelType.SOUNDS: StarPilotPanelInfo(tr_noop("Sounds"), StarPilotSoundsLayout()), + StarPilotPanelType.SYSTEM: StarPilotPanelInfo(tr_noop("System Settings"), StarPilotSystemLayout()), StarPilotPanelType.DRIVING_MODEL: StarPilotPanelInfo(tr_noop("Driving Model"), StarPilotDrivingModelLayout()), StarPilotPanelType.LONGITUDINAL: StarPilotPanelInfo(tr_noop("Gas / Brake"), StarPilotLongitudinalLayout()), StarPilotPanelType.LATERAL: StarPilotPanelInfo(tr_noop("Steering"), StarPilotLateralLayout()), @@ -101,11 +103,14 @@ class StarPilotLayout(Widget): StarPilotPanelType.WHEEL: StarPilotPanelInfo(tr_noop("Wheel Controls"), StarPilotWheelLayout()), } - self._setup_longitudinal_sub_panels() - self._setup_sounds_sub_panels() - self._setup_lateral_sub_panels() - self._setup_navigation_sub_panels() - self._setup_maps_sub_panels() + self._setup_sub_panels( + StarPilotPanelType.LONGITUDINAL, + StarPilotPanelType.SOUNDS, + StarPilotPanelType.SYSTEM, + StarPilotPanelType.LATERAL, + StarPilotPanelType.NAVIGATION, + StarPilotPanelType.MAPS, + ) self._main_grid = TileGrid(columns=None, padding=20) self._rebuild_grid() @@ -162,66 +167,28 @@ class StarPilotLayout(Widget): self._update_depth() def _update_sub_panel_visibility(self): - if self._current_panel == StarPilotPanelType.LONGITUDINAL: - longitudinal = self._panels[StarPilotPanelType.LONGITUDINAL].instance - if longitudinal: - current_sub = self._get_current_sub_panel() - if hasattr(longitudinal, 'set_current_sub_panel'): - longitudinal.set_current_sub_panel(current_sub) - elif self._current_panel == StarPilotPanelType.SOUNDS: - sounds = self._panels[StarPilotPanelType.SOUNDS].instance - if sounds: - current_sub = self._get_current_sub_panel() - if hasattr(sounds, '_navigate_to'): - sounds._current_sub_panel = current_sub - elif self._current_panel == StarPilotPanelType.NAVIGATION: - nav = self._panels[StarPilotPanelType.NAVIGATION].instance - if nav: - current_sub = self._get_current_sub_panel() - if hasattr(nav, '_navigate_to'): - nav._current_sub_panel = current_sub - elif self._current_panel == StarPilotPanelType.MAPS: - maps = self._panels[StarPilotPanelType.MAPS].instance - if maps: - current_sub = self._get_current_sub_panel() - if hasattr(maps, '_navigate_to'): - maps._current_sub_panel = current_sub + panel = self._panels[self._current_panel].instance + current_sub = self._get_current_sub_panel() + if panel and hasattr(panel, 'set_current_sub_panel'): + panel.set_current_sub_panel(current_sub) def _get_current_sub_panel(self) -> str: if self._panel_stack and self._panel_stack[-1][0] == self._current_panel: return self._panel_stack[-1][1] return "" - def _setup_longitudinal_sub_panels(self): - longitudinal = self._panels[StarPilotPanelType.LONGITUDINAL].instance - if longitudinal and hasattr(longitudinal, 'set_navigate_callback'): - longitudinal.set_navigate_callback(self._push_sub_panel) - - def _setup_sounds_sub_panels(self): - sounds = self._panels[StarPilotPanelType.SOUNDS].instance - if sounds and hasattr(sounds, 'set_navigate_callback'): - sounds.set_navigate_callback(self._push_sub_panel) - - def _setup_lateral_sub_panels(self): - lateral = self._panels[StarPilotPanelType.LATERAL].instance - if lateral and hasattr(lateral, 'set_navigate_callback'): - lateral.set_navigate_callback(self._push_sub_panel) - - def _setup_navigation_sub_panels(self): - nav = self._panels[StarPilotPanelType.NAVIGATION].instance - if nav and hasattr(nav, 'set_navigate_callback'): - nav.set_navigate_callback(self._push_sub_panel) - - def _setup_maps_sub_panels(self): - maps = self._panels[StarPilotPanelType.MAPS].instance - if maps and hasattr(maps, 'set_navigate_callback'): - maps.set_navigate_callback(self._push_sub_panel) + def _setup_sub_panels(self, *panel_types: StarPilotPanelType): + for panel_type in panel_types: + panel = self._panels[panel_type].instance + if panel and hasattr(panel, 'set_navigate_callback'): + panel.set_navigate_callback(self._push_sub_panel) def _rebuild_grid(self): self._main_grid.clear() panel_type_map = { "SOUNDS": StarPilotPanelType.SOUNDS, + "SYSTEM": StarPilotPanelType.SYSTEM, "DRIVING_MODEL": StarPilotPanelType.DRIVING_MODEL, "LONGITUDINAL": StarPilotPanelType.LONGITUDINAL, "LATERAL": StarPilotPanelType.LATERAL, diff --git a/selfdrive/ui/layouts/settings/starpilot/panel.py b/selfdrive/ui/layouts/settings/starpilot/panel.py index 32ce560b3..622d78be3 100644 --- a/selfdrive/ui/layouts/settings/starpilot/panel.py +++ b/selfdrive/ui/layouts/settings/starpilot/panel.py @@ -25,6 +25,7 @@ class StarPilotPanelType(IntEnum): THEMES = 11 VEHICLE = 12 WHEEL = 13 + SYSTEM = 14 @dataclass @@ -55,6 +56,9 @@ class StarPilotPanel(Widget): def set_back_callback(self, callback: Callable): self._back_callback = callback + def set_current_sub_panel(self, sub_panel: str): + self._current_sub_panel = sub_panel + def _rebuild_grid(self): if not self.CATEGORIES: return @@ -84,9 +88,9 @@ class StarPilotPanel(Widget): bg_color=cat.get("color"), ) elif tile_type == "toggle": - tile = ToggleTile(title=tr(cat["title"]), get_state=cat["get_state"], set_state=cat["set_state"], icon_path=cat.get("icon"), bg_color=cat.get("color"), desc=tr(cat.get("desc", ""))) + tile = ToggleTile(title=tr(cat["title"]), get_state=cat["get_state"], set_state=cat["set_state"], icon_path=cat.get("icon"), bg_color=cat.get("color"), desc=tr(cat.get("desc", "")), is_enabled=cat.get("is_enabled"), disabled_label=cat.get("disabled_label", "")) elif tile_type == "value": - tile = ValueTile(title=tr(cat["title"]), get_value=cat["get_value"], on_click=cat["on_click"], icon_path=cat.get("icon"), bg_color=cat.get("color"), desc=tr(cat.get("desc", ""))) + tile = ValueTile(title=tr(cat["title"]), get_value=cat["get_value"], on_click=cat["on_click"], icon_path=cat.get("icon"), bg_color=cat.get("color"), is_enabled=cat.get("is_enabled"), desc=tr(cat.get("desc", ""))) else: continue diff --git a/selfdrive/ui/layouts/settings/starpilot/sounds.py b/selfdrive/ui/layouts/settings/starpilot/sounds.py index e696e9ce4..9e1d085ac 100644 --- a/selfdrive/ui/layouts/settings/starpilot/sounds.py +++ b/selfdrive/ui/layouts/settings/starpilot/sounds.py @@ -2,16 +2,17 @@ from __future__ import annotations import subprocess from pathlib import Path +import pyray as rl + from openpilot.common.basedir import BASEDIR from openpilot.starpilot.common.starpilot_variables import ACTIVE_THEME_PATH from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.multilang import tr, tr_noop from openpilot.system.ui.widgets import DialogResult -from openpilot.system.ui.widgets.selection_dialog import SelectionDialog from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel -from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, ToggleTile, AetherSliderDialog +from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, ToggleTile, AetherSliderDialog, RadioTileGroup class StarPilotSoundsLayout(StarPilotPanel): COOLDOWN_KEY = "SwitchbackModeCooldown" @@ -36,41 +37,73 @@ class StarPilotSoundsLayout(StarPilotPanel): def __init__(self): super().__init__() - + self._section_names = ["volume_control", "custom_alerts"] + self._active_section = self._section_names[0] self._sub_panels = { "volume_control": StarPilotVolumeControlLayout(), "custom_alerts": StarPilotCustomAlertsLayout(), } - - self.CATEGORIES = [ - { - "title": tr_noop("Alert Volume Controller"), - "panel": "volume_control", - "desc": tr_noop("Adjust volume levels for different alert types."), - "icon": "toggle_icons/icon_mute.png", - "color": "#E63956" - }, - { - "title": tr_noop("StarPilot Alerts"), - "panel": "custom_alerts", - "desc": tr_noop("Enable or disable specific StarPilot-only alerts."), - "icon": "toggle_icons/icon_green_light.png", - "color": "#E63956" - }, - ] + self._section_tabs = RadioTileGroup( + "", + [tr("Volumes"), tr("Alerts")], + 0, + self._on_section_change, + ) for name, panel in self._sub_panels.items(): if hasattr(panel, 'set_navigate_callback'): - panel.set_navigate_callback(self._navigate_to) + panel.set_navigate_callback(lambda sub_panel, section_name=name: self._navigate_to_child(section_name, sub_panel)) if hasattr(panel, 'set_back_callback'): panel.set_back_callback(self._go_back) - self._rebuild_grid() + def _on_section_change(self, index: int): + if 0 <= index < len(self._section_names): + self._set_active_section(self._section_names[index]) - def refresh_visibility(self): - for panel in self._sub_panels.values(): - if hasattr(panel, 'refresh_visibility'): - panel.refresh_visibility() + def _set_active_section(self, section_name: str, child_panel: str = ""): + if section_name not in self._sub_panels: + return + + if section_name != self._active_section: + self._sub_panels[self._active_section].hide_event() + self._active_section = section_name + self._sub_panels[self._active_section].show_event() + + self._section_tabs.set_index(self._section_names.index(section_name)) + panel = self._sub_panels[section_name] + if hasattr(panel, 'set_current_sub_panel'): + panel.set_current_sub_panel(child_panel) + + def _navigate_to_child(self, section_name: str, child_panel: str): + self._set_active_section(section_name, child_panel) + if self._navigate_callback: + self._navigate_callback(f"{section_name}:{child_panel}") + + def set_current_sub_panel(self, sub_panel: str): + super().set_current_sub_panel(sub_panel) + if not sub_panel: + self._set_active_section(self._active_section, "") + return + + if ":" in sub_panel: + section_name, child_panel = sub_panel.split(":", 1) + self._set_active_section(section_name, child_panel) + elif sub_panel in self._section_names: + self._set_active_section(sub_panel) + + def _render(self, rect): + tab_rect = rl.Rectangle(rect.x, rect.y, rect.width, 110) + panel_rect = rl.Rectangle(rect.x, rect.y + 140, rect.width, rect.height - 140) + self._section_tabs.render(tab_rect) + self._sub_panels[self._active_section].render(panel_rect) + + def show_event(self): + super().show_event() + self._sub_panels[self._active_section].show_event() + + def hide_event(self): + super().hide_event() + self._sub_panels[self._active_section].hide_event() class StarPilotVolumeControlLayout(StarPilotPanel): COOLDOWN_INFO = {"title": tr_noop("Switchback Mode Cooldown"), "icon": "toggle_icons/icon_mute.png", "min": 0, "max": 30} @@ -90,6 +123,7 @@ class StarPilotVolumeControlLayout(StarPilotPanel): def __init__(self): super().__init__() self._init_sound_player() + self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True) self.CATEGORIES = [] for key in StarPilotSoundsLayout.VOLUME_KEYS: @@ -217,6 +251,7 @@ class StarPilotCustomAlertsLayout(StarPilotPanel): def __init__(self): super().__init__() + self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True) self.CATEGORIES = [] for key in StarPilotSoundsLayout.CUSTOM_ALERTS_KEYS: info = self.ALERT_INFO[key] @@ -231,32 +266,32 @@ class StarPilotCustomAlertsLayout(StarPilotPanel): }) self._rebuild_grid() - def refresh_visibility(self): - self._rebuild_grid() - def _rebuild_grid(self): - # Override to add custom BSM/SLC visibility logic - if not self.CATEGORIES: return - if self._tile_grid is None: self._tile_grid = TileGrid(columns=None, padding=20) + if not self.CATEGORIES: + return self._tile_grid.clear() for cat in self.CATEGORIES: key = cat.get("key") - visible = True + is_enabled = lambda: True + disabled_label = "" if key == "LoudBlindspotAlert": - visible &= starpilot_state.car_state.hasBSM + is_enabled = lambda: starpilot_state.car_state.hasBSM + disabled_label = tr_noop("Needs BSM") elif key == "SpeedLimitChangedAlert": - visible &= self._params.get_bool("ShowSpeedLimits") or ( + is_enabled = lambda: self._params.get_bool("ShowSpeedLimits") or ( starpilot_state.car_state.hasOpenpilotLongitudinal and self._params.get_bool("SpeedLimitController") ) + disabled_label = tr_noop("Needs Speed Limits") - if not visible: continue - - tile = ToggleTile( title=tr(cat["title"]), + tile = ToggleTile( + title=tr(cat["title"]), get_state=cat["get_state"], set_state=cat["set_state"], icon_path=cat.get("icon"), - bg_color=cat.get("color") + bg_color=cat.get("color"), + is_enabled=is_enabled, + disabled_label=tr(disabled_label) if disabled_label else "", ) self._tile_grid.add_tile(tile) diff --git a/selfdrive/ui/layouts/settings/starpilot/system_settings.py b/selfdrive/ui/layouts/settings/starpilot/system_settings.py new file mode 100644 index 000000000..cfbd5a994 --- /dev/null +++ b/selfdrive/ui/layouts/settings/starpilot/system_settings.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import pyray as rl + +from openpilot.system.ui.lib.multilang import tr + +from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel +from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import RadioTileGroup +from openpilot.selfdrive.ui.layouts.settings.starpilot.data import StarPilotDataLayout +from openpilot.selfdrive.ui.layouts.settings.starpilot.device import StarPilotDeviceLayout +from openpilot.selfdrive.ui.layouts.settings.starpilot.utilities import StarPilotUtilitiesLayout + + +class StarPilotSystemLayout(StarPilotPanel): + def __init__(self): + super().__init__() + + self._section_names = ["device", "data_and_backups", "utilities"] + self._active_section = self._section_names[0] + self._sub_panels = { + "device": StarPilotDeviceLayout(), + "data_and_backups": StarPilotDataLayout(), + "utilities": StarPilotUtilitiesLayout(), + } + + self._section_tabs = RadioTileGroup( + "", + [tr("Device"), tr("Data"), tr("Utilities")], + 0, + self._on_section_change, + ) + + for name, panel in self._sub_panels.items(): + if hasattr(panel, 'set_navigate_callback'): + panel.set_navigate_callback(lambda sub_panel, section_name=name: self._navigate_to_child(section_name, sub_panel)) + if hasattr(panel, 'set_back_callback'): + panel.set_back_callback(self._go_back) + + def _on_section_change(self, index: int): + if 0 <= index < len(self._section_names): + self._set_active_section(self._section_names[index]) + + def _set_active_section(self, section_name: str, child_panel: str = ""): + if section_name not in self._sub_panels: + return + + if section_name != self._active_section: + self._sub_panels[self._active_section].hide_event() + self._active_section = section_name + self._sub_panels[self._active_section].show_event() + + self._section_tabs.set_index(self._section_names.index(section_name)) + panel = self._sub_panels[section_name] + if hasattr(panel, 'set_current_sub_panel'): + panel.set_current_sub_panel(child_panel) + + def _navigate_to_child(self, section_name: str, child_panel: str): + self._set_active_section(section_name, child_panel) + if self._navigate_callback: + self._navigate_callback(f"{section_name}:{child_panel}") + + def set_current_sub_panel(self, sub_panel: str): + super().set_current_sub_panel(sub_panel) + if not sub_panel: + self._set_active_section(self._active_section, "") + return + + if ":" in sub_panel: + section_name, child_panel = sub_panel.split(":", 1) + self._set_active_section(section_name, child_panel) + elif sub_panel in self._section_names: + self._set_active_section(sub_panel) + + def _render(self, rect): + tab_rect = rl.Rectangle(rect.x, rect.y, rect.width, 110) + panel_rect = rl.Rectangle(rect.x, rect.y + 140, rect.width, rect.height - 140) + self._section_tabs.render(tab_rect) + self._sub_panels[self._active_section].render(panel_rect) + + def show_event(self): + super().show_event() + self._sub_panels[self._active_section].show_event() + + def hide_event(self): + super().hide_event() + self._sub_panels[self._active_section].hide_event() diff --git a/selfdrive/ui/layouts/settings/starpilot/utilities.py b/selfdrive/ui/layouts/settings/starpilot/utilities.py index e6321ca8b..7eec0b8f1 100644 --- a/selfdrive/ui/layouts/settings/starpilot/utilities.py +++ b/selfdrive/ui/layouts/settings/starpilot/utilities.py @@ -8,6 +8,7 @@ from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dial from openpilot.system.ui.widgets.selection_dialog import SelectionDialog from openpilot.system.ui.widgets.input_dialog import InputDialog from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel +from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid EXCLUDED_KEYS = { "AvailableModels", @@ -38,6 +39,7 @@ REPORT_CATEGORIES = [ class StarPilotUtilitiesLayout(StarPilotPanel): def __init__(self): super().__init__() + self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True) self.CATEGORIES = [ { "title": tr_noop("Debug Mode"),