diff --git a/selfdrive/ui/layouts/settings/starpilot/appearance.py b/selfdrive/ui/layouts/settings/starpilot/appearance.py new file mode 100644 index 000000000..9a7104f38 --- /dev/null +++ b/selfdrive/ui/layouts/settings/starpilot/appearance.py @@ -0,0 +1,611 @@ +from __future__ import annotations +import re +from pathlib import Path + +from openpilot.system.hardware import HARDWARE +from openpilot.system.hardware.hw import Paths +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.option_dialog import MultiOptionDialog + +from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state +from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import _SettingsPage +from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ( + AETHER_LIST_METRICS, + AetherSliderDialog, + DEFAULT_PANEL_STYLE, +) +from openpilot.selfdrive.ui.layouts.settings.starpilot.longitudinal import ( + SettingRow, + SettingSection, + AetherSettingsView, +) + +PANEL_STYLE = DEFAULT_PANEL_STYLE +UTILITY_ROW_HEIGHT = AETHER_LIST_METRICS.utility_row_height +ROW_HEIGHT = AETHER_LIST_METRICS.row_height + +# ── Theme paths & config (preserved from themes.py) ── + +if HARDWARE.get_device_type() == "pc": + THEME_SAVE_PATH = Path(Paths.comma_home()) / "starpilot" / "data" / "themes" +else: + THEME_SAVE_PATH = Path("/data/themes") + +HOLIDAY_THEME_NAMES = { + "new_years": "New Year's", + "valentines_day": "Valentine's Day", + "st_patricks_day": "St. Patrick's Day", + "world_frog_day": "World Frog Day", + "april_fools": "April Fools", + "easter_week": "Easter", + "may_the_fourth": "May the Fourth", + "cinco_de_mayo": "Cinco de Mayo", + "stitch_day": "Stitch Day", + "fourth_of_july": "Fourth of July", + "halloween_week": "Halloween", + "thanksgiving_week": "Thanksgiving", + "christmas_week": "Christmas", +} + +THEME_KEY_CONFIG = { + "BootLogo": { + "default": "starpilot", + "kind": "files", + "path": THEME_SAVE_PATH / "bootlogos", + "extra": [], + }, + "ColorScheme": { + "default": "stock", + "kind": "themes", + "path": THEME_SAVE_PATH / "theme_packs", + "subfolder": "colors", + "extra": [("stock", "Stock"), *HOLIDAY_THEME_NAMES.items()], + }, + "DistanceIconPack": { + "default": "stock", + "kind": "themes", + "path": THEME_SAVE_PATH / "theme_packs", + "subfolder": "distance_icons", + "extra": [("stock", "Stock"), *HOLIDAY_THEME_NAMES.items()], + }, + "IconPack": { + "default": "stock", + "kind": "themes", + "path": THEME_SAVE_PATH / "theme_packs", + "subfolder": "icons", + "extra": [("stock", "Stock"), *HOLIDAY_THEME_NAMES.items()], + }, + "SignalAnimation": { + "default": "stock", + "kind": "themes", + "path": THEME_SAVE_PATH / "theme_packs", + "subfolder": "signals", + "extra": [("none", "None"), *HOLIDAY_THEME_NAMES.items()], + }, + "SoundPack": { + "default": "stock", + "kind": "themes", + "path": THEME_SAVE_PATH / "theme_packs", + "subfolder": "sounds", + "extra": [("stock", "Stock"), *HOLIDAY_THEME_NAMES.items()], + }, + "WheelIcon": { + "default": "stock", + "kind": "files", + "path": THEME_SAVE_PATH / "steering_wheels", + "extra": [("none", "None"), ("stock", "Stock"), *HOLIDAY_THEME_NAMES.items()], + }, +} + +COLOR_PRESETS = ["Stock", "#FFFFFF", "#178644", "#3B82F6", "#E63956", "#8B5CF6", "#F59E0B"] +CAMERA_VIEWS = ["Auto", "Driver", "Standard", "Wide"] + + +def _theme_display_name(value: str) -> str: + if not value: + return "Stock" + lowered = value.lower() + if lowered in HOLIDAY_THEME_NAMES: + return HOLIDAY_THEME_NAMES[lowered] + if lowered == "stock": + return "Stock" + if lowered == "none": + return "None" + base, creator = (value.split("~", 1) + [""])[:2] if "~" in value else (value, "") + user_created_suffixes = ("-user_created", "_user_created", "-user-created", "_user-created") + user_created = False + for suffix in user_created_suffixes: + if base.endswith(suffix): + base = base[:-len(suffix)] + user_created = True + break + parts = [part for part in re.split(r"[-_]+", base) if part] + display = " ".join(part[:1].upper() + part[1:] for part in parts) if parts else value + if user_created: + display += " (User Created)" + if creator: + display += f" - by: {creator}" + return display + + +# ═══════════════════════════════════════════════════════════════ +# Theme Personalize sub-panel +# ═══════════════════════════════════════════════════════════════ + +class StarPilotAppearancePersonalizeLayout(_SettingsPage): + def __init__(self): + super().__init__() + self._build_view() + + def _build_view(self): + sections: list[SettingSection] = [ + SettingSection(tr_noop("Theme Components"), [ + SettingRow("BootLogo", "value", tr_noop("Boot Logo"), + subtitle="", + get_value=lambda: self._get_theme_value("BootLogo"), + on_click=lambda: self._show_theme_selector("BootLogo")), + SettingRow("ColorScheme", "value", tr_noop("Color Scheme"), + subtitle="", + get_value=lambda: self._get_theme_value("ColorScheme"), + on_click=lambda: self._show_theme_selector("ColorScheme")), + SettingRow("DistanceIconPack", "value", tr_noop("Distance Icons"), + subtitle="", + get_value=lambda: self._get_theme_value("DistanceIconPack"), + on_click=lambda: self._show_theme_selector("DistanceIconPack")), + SettingRow("IconPack", "value", tr_noop("Icon Pack"), + subtitle="", + get_value=lambda: self._get_theme_value("IconPack"), + on_click=lambda: self._show_theme_selector("IconPack")), + SettingRow("SignalAnimation", "value", tr_noop("Turn Signals"), + subtitle="", + get_value=lambda: self._get_theme_value("SignalAnimation"), + on_click=lambda: self._show_theme_selector("SignalAnimation")), + SettingRow("SoundPack", "value", tr_noop("Sound Pack"), + subtitle="", + get_value=lambda: self._get_theme_value("SoundPack"), + on_click=lambda: self._show_theme_selector("SoundPack")), + SettingRow("WheelIcon", "value", tr_noop("Steering Wheel"), + subtitle="", + get_value=lambda: self._get_theme_value("WheelIcon"), + on_click=lambda: self._show_theme_selector("WheelIcon")), + ], row_height=UTILITY_ROW_HEIGHT), + ] + self._manager_view = AetherSettingsView( + self, sections, + header_title=tr_noop("Personalize"), + header_subtitle=tr_noop("Customize the overall look and feel of openpilot."), + panel_style=PANEL_STYLE, + ) + + def _get_downloaded_slugs(self, key: str) -> list[str]: + config = THEME_KEY_CONFIG[key] + path = config["path"] + if not path.exists(): + return [] + slugs = set() + if config["kind"] == "files": + for entry in path.iterdir(): + if entry.is_file(): + slugs.add(entry.stem) + else: + subfolder = config["subfolder"] + for entry in path.iterdir(): + if entry.is_dir() and (entry / subfolder).exists(): + slugs.add(entry.name) + return sorted(slugs, key=str.casefold) + + def _build_theme_options(self, key: str) -> tuple[list[str], dict[str, str], str]: + config = THEME_KEY_CONFIG[key] + current_slug = self._params.get(key, encoding='utf-8') or config["default"] + options_map = {} + for slug in self._get_downloaded_slugs(key): + display = _theme_display_name(slug) + if display not in options_map: + options_map[display] = slug + for slug, display in config["extra"]: + options_map[display] = slug + current_display = _theme_display_name(current_slug) + if current_display not in options_map: + options_map[current_display] = current_slug + options = sorted(options_map.keys(), key=str.casefold) + return options, options_map, current_display + + def _get_theme_value(self, key: str) -> str: + default = THEME_KEY_CONFIG[key]["default"] + return _theme_display_name(self._params.get(key, encoding='utf-8') or default) + + def _show_theme_selector(self, key): + themes, option_map, current = self._build_theme_options(key) + if not themes: + return + + def on_select(res): + if res == DialogResult.CONFIRM and dialog.selection: + selected_slug = option_map.get(dialog.selection) + if selected_slug is None: + return + self._params.put(key, selected_slug) + + dialog = MultiOptionDialog(tr(key), themes, current, callback=on_select) + gui_app.push_widget(dialog) + + +# ═══════════════════════════════════════════════════════════════ +# Unified Appearance panel +# ═══════════════════════════════════════════════════════════════ + +class StarPilotAppearanceLayout(_SettingsPage): + def __init__(self): + super().__init__() + self._sub_panels = { + "personalize": StarPilotAppearancePersonalizeLayout(), + } + self._wire_sub_panels() + self._build_view() + + def _build_view(self): + tab_defs = [ + {"id": "display", "title": tr_noop("Display"), "subtitle": tr_noop("Screen visibility")}, + {"id": "widgets", "title": tr_noop("Widgets"), "subtitle": tr_noop("Driving indicators")}, + {"id": "convenience", "title": tr_noop("Convenience"), "subtitle": tr_noop("QOL & navigation")}, + {"id": "model", "title": tr_noop("Model"), "subtitle": tr_noop("Path visualization")}, + {"id": "theme", "title": tr_noop("Theme"), "subtitle": tr_noop("Customization")}, + ] + + po = lambda: self._params.get_bool("PedalsOnUI") + ol = lambda: starpilot_state.car_state.hasOpenpilotLongitudinal + bsm = lambda: starpilot_state.car_state.hasBSM + + sections: list[SettingSection] = [ + # ═══ Tab 1: Display — screen visibility toggles ═══ + SettingSection(tr_noop("Screen Elements"), [ + SettingRow("AdvancedCustomUI", "toggle", tr_noop("Advanced UI Controls"), + subtitle=tr_noop("Fine-tune which elements appear on screen."), + get_state=lambda: self._params.get_bool("AdvancedCustomUI"), + set_state=lambda s: self._params.put_bool("AdvancedCustomUI", s)), + SettingRow("HideSpeed", "toggle", tr_noop("Hide Speed"), + subtitle="", + get_state=lambda: self._params.get_bool("HideSpeed"), + set_state=lambda s: self._params.put_bool("HideSpeed", s)), + SettingRow("HideMaxSpeed", "toggle", tr_noop("Hide Max Speed"), + subtitle="", + get_state=lambda: self._params.get_bool("HideMaxSpeed"), + set_state=lambda s: self._params.put_bool("HideMaxSpeed", s)), + SettingRow("HideAlerts", "toggle", tr_noop("Hide Alerts"), + subtitle="", + get_state=lambda: self._params.get_bool("HideAlerts"), + set_state=lambda s: self._params.put_bool("HideAlerts", s)), + ], tab_key="display", column_pair="display", row_height=UTILITY_ROW_HEIGHT), + + SettingSection(tr_noop("Speed Info"), [ + SettingRow("HideSpeedLimit", "toggle", tr_noop("Hide Speed Limit"), + subtitle="", + get_state=lambda: self._params.get_bool("HideSpeedLimit"), + set_state=lambda s: self._params.put_bool("HideSpeedLimit", s)), + SettingRow("HideLeadMarker", "toggle", tr_noop("Hide Lead Marker"), + subtitle="", + get_state=lambda: self._params.get_bool("HideLeadMarker"), + set_state=lambda s: self._params.put_bool("HideLeadMarker", s), + visible=ol), + SettingRow("WheelSpeed", "toggle", tr_noop("Wheel Speed"), + subtitle="", + get_state=lambda: self._params.get_bool("WheelSpeed"), + set_state=lambda s: self._params.put_bool("WheelSpeed", s)), + ], tab_key="display", column_pair="display", row_height=UTILITY_ROW_HEIGHT), + + # ═══ Tab 2: Widgets — driving screen widget toggles ═══ + SettingSection(tr_noop("Path Overlays"), [ + SettingRow("CustomUI", "toggle", tr_noop("Driving Screen Widgets"), + subtitle=tr_noop("Show interactive indicators on the driving screen."), + get_state=lambda: self._params.get_bool("CustomUI"), + set_state=lambda s: self._params.put_bool("CustomUI", s)), + SettingRow("AccelerationPath", "toggle", tr_noop("Acceleration Path"), + subtitle="", + get_state=lambda: self._params.get_bool("AccelerationPath"), + set_state=lambda s: self._params.put_bool("AccelerationPath", s), + visible=ol), + SettingRow("AdjacentPath", "toggle", tr_noop("Adjacent Lanes"), + subtitle="", + get_state=lambda: self._params.get_bool("AdjacentPath"), + set_state=lambda s: self._params.put_bool("AdjacentPath", s)), + SettingRow("AdjacentPathMetrics", "toggle", tr_noop("Adjacent Lane Metrics"), + subtitle="", + get_state=lambda: self._params.get_bool("AdjacentPathMetrics"), + set_state=lambda s: self._params.put_bool("AdjacentPathMetrics", s)), + SettingRow("BlindSpotPath", "toggle", tr_noop("Blind Spot Path"), + subtitle="", + get_state=lambda: self._params.get_bool("BlindSpotPath"), + set_state=lambda s: self._params.put_bool("BlindSpotPath", s), + visible=bsm), + ], tab_key="widgets", column_pair="widgets", row_height=UTILITY_ROW_HEIGHT), + + SettingSection(tr_noop("Dashboard Controls"), [ + SettingRow("Compass", "toggle", tr_noop("Compass"), + subtitle="", + get_state=lambda: self._params.get_bool("Compass"), + set_state=lambda s: self._params.put_bool("Compass", s)), + SettingRow("OnroadDistanceButton", "toggle", tr_noop("Personality Button"), + subtitle="", + get_state=lambda: self._params.get_bool("OnroadDistanceButton"), + set_state=lambda s: self._params.put_bool("OnroadDistanceButton", s)), + SettingRow("PedalsOnUI", "toggle", tr_noop("Pedal Indicators"), + subtitle="", + get_state=lambda: self._params.get_bool("PedalsOnUI"), + set_state=lambda s: self._params.put_bool("PedalsOnUI", s), + visible=ol), + SettingRow("DynamicPedalsOnUI", "toggle", tr_noop("Dynamic Pedals"), + subtitle="", + get_state=lambda: self._params.get_bool("DynamicPedalsOnUI"), + set_state=lambda s: self._set_exclusive_pedal("DynamicPedalsOnUI", "StaticPedalsOnUI", s), + visible=lambda: po() and ol()), + SettingRow("StaticPedalsOnUI", "toggle", tr_noop("Static Pedals"), + subtitle="", + get_state=lambda: self._params.get_bool("StaticPedalsOnUI"), + set_state=lambda s: self._set_exclusive_pedal("StaticPedalsOnUI", "DynamicPedalsOnUI", s), + visible=lambda: po() and ol()), + SettingRow("RotatingWheel", "toggle", tr_noop("Rotating Wheel"), + subtitle="", + get_state=lambda: self._params.get_bool("RotatingWheel"), + set_state=lambda s: self._params.put_bool("RotatingWheel", s)), + ], tab_key="widgets", column_pair="widgets", row_height=UTILITY_ROW_HEIGHT), + + # ═══ Tab 3: Convenience — QOL + Navigation ═══ + SettingSection(tr_noop("Quality of Life"), [ + SettingRow("QOLVisuals", "toggle", tr_noop("Quality of Life"), + subtitle=tr_noop("Convenience features for everyday driving."), + get_state=lambda: self._params.get_bool("QOLVisuals"), + set_state=lambda s: self._params.put_bool("QOLVisuals", s)), + SettingRow("CameraView", "value", tr_noop("Camera View"), + subtitle="", + get_value=lambda: tr(CAMERA_VIEWS[self._params.get_int("CameraView")]), + on_click=self._show_camera_view_selector), + SettingRow("DriverCamera", "toggle", tr_noop("Driver Camera"), + subtitle="", + get_state=lambda: self._params.get_bool("DriverCamera"), + set_state=lambda s: self._params.put_bool("DriverCamera", s)), + SettingRow("StoppedTimer", "toggle", tr_noop("Stopped Timer"), + subtitle="", + get_state=lambda: self._params.get_bool("StoppedTimer"), + set_state=lambda s: self._params.put_bool("StoppedTimer", s)), + ], tab_key="convenience", column_pair="convenience", row_height=UTILITY_ROW_HEIGHT), + + SettingSection(tr_noop("Navigation"), [ + SettingRow("NavigationUI", "toggle", tr_noop("Navigation Widgets"), + subtitle=tr_noop("Show navigation info on the driving screen."), + get_state=lambda: self._params.get_bool("NavigationUI"), + set_state=lambda s: self._params.put_bool("NavigationUI", s)), + SettingRow("RoadNameUI", "toggle", tr_noop("Road Name"), + subtitle="", + get_state=lambda: self._params.get_bool("RoadNameUI"), + set_state=lambda s: self._params.put_bool("RoadNameUI", s)), + SettingRow("ShowSpeedLimits", "toggle", tr_noop("Speed Limits"), + subtitle="", + get_state=lambda: self._params.get_bool("ShowSpeedLimits"), + set_state=lambda s: self._params.put_bool("ShowSpeedLimits", s)), + SettingRow("UseVienna", "toggle", tr_noop("Vienna Signs"), + subtitle="", + get_state=lambda: self._params.get_bool("UseVienna"), + set_state=lambda s: self._params.put_bool("UseVienna", s)), + ], tab_key="convenience", column_pair="convenience", row_height=UTILITY_ROW_HEIGHT), + + # ═══ Tab 4: Model — path/lane visualization ═══ + SettingSection(tr_noop("Path & Lanes"), [ + SettingRow("ModelUI", "toggle", tr_noop("Model UI"), + subtitle=tr_noop("Display the driving model path, lanes, and road edges."), + get_state=lambda: self._params.get_bool("ModelUI"), + set_state=lambda s: self._params.put_bool("ModelUI", s)), + SettingRow("DynamicPathWidth", "toggle", tr_noop("Dynamic Path"), + subtitle="", + get_state=lambda: self._params.get_bool("DynamicPathWidth"), + set_state=lambda s: self._params.put_bool("DynamicPathWidth", s)), + SettingRow("LaneLinesWidth", "value", tr_noop("Lane Line Width"), + subtitle="", + get_value=lambda: self._get_lane_lines_display(), + on_click=lambda: self._show_int_selector("LaneLinesWidth", 0, 24, self._get_lane_lines_unit())), + SettingRow("LaneLinesColor", "value", tr_noop("Lane Line Color"), + subtitle="", + get_value=lambda: self._get_color_display("LaneLinesColor"), + on_click=lambda: self._show_color_selector("LaneLinesColor")), + SettingRow("PathWidth", "value", tr_noop("Path Width"), + subtitle="", + get_value=lambda: self._get_path_width_display(), + on_click=self._show_path_width_selector), + ], tab_key="model", column_pair="model", row_height=UTILITY_ROW_HEIGHT), + + SettingSection(tr_noop("Edges & Colors"), [ + SettingRow("PathEdgeWidth", "value", tr_noop("Path Edge Width"), + subtitle="", + get_value=lambda: f"{self._params.get_int('PathEdgeWidth')}%", + on_click=lambda: self._show_int_selector("PathEdgeWidth", 0, 100, "%")), + SettingRow("PathEdgesColor", "value", tr_noop("Path Edge Color"), + subtitle="", + get_value=lambda: self._get_color_display("PathEdgesColor"), + on_click=lambda: self._show_color_selector("PathEdgesColor")), + SettingRow("PathColor", "value", tr_noop("Path Color"), + subtitle="", + get_value=lambda: self._get_color_display("PathColor"), + on_click=lambda: self._show_color_selector("PathColor")), + SettingRow("RoadEdgesWidth", "value", tr_noop("Road Edge Width"), + subtitle="", + get_value=lambda: self._get_road_edges_display(), + on_click=lambda: self._show_int_selector("RoadEdgesWidth", 0, 24, self._get_road_edges_unit())), + SettingRow("BorderWidth", "value", tr_noop("Border Width"), + subtitle="", + get_value=lambda: f"{int(round(self._params.get_float('BorderWidth')))}%", + on_click=lambda: self._show_float_selector("BorderWidth", 25, 250, 5, "%")), + ], tab_key="model", column_pair="model", row_height=UTILITY_ROW_HEIGHT), + + # ═══ Tab 5: Theme — customization ═══ + SettingSection(tr_noop("Customization"), [ + SettingRow("CustomThemes", "toggle", tr_noop("Custom Themes"), + subtitle=tr_noop("Enable custom theme assets on the driving screen."), + get_state=lambda: self._params.get_bool("CustomThemes"), + set_state=lambda s: self._params.put_bool("CustomThemes", s)), + SettingRow("Personalize", "value", tr_noop("Personalize openpilot"), + subtitle=tr_noop("Choose boot logo, color scheme, icons, sounds, and more."), + get_value=lambda: tr_noop("Customize"), + navigate_to="personalize"), + SettingRow("HolidayThemes", "toggle", tr_noop("Holiday Themes"), + subtitle="", + get_state=lambda: self._params.get_bool("HolidayThemes"), + set_state=lambda s: self._params.put_bool("HolidayThemes", s)), + SettingRow("RainbowPath", "toggle", tr_noop("Rainbow Path"), + subtitle="", + get_state=lambda: self._params.get_bool("RainbowPath"), + set_state=lambda s: self._params.put_bool("RainbowPath", s)), + ], tab_key="theme", column_pair="theme", row_height=UTILITY_ROW_HEIGHT), + + SettingSection(tr_noop("Options"), [ + SettingRow("RandomEvents", "toggle", tr_noop("Random Events"), + subtitle="", + get_state=lambda: self._params.get_bool("RandomEvents"), + set_state=lambda s: self._params.put_bool("RandomEvents", s)), + SettingRow("RandomThemes", "toggle", tr_noop("Random Themes"), + subtitle="", + get_state=lambda: self._params.get_bool("RandomThemes"), + set_state=lambda s: self._params.put_bool("RandomThemes", s)), + SettingRow("StartupAlert", "value", tr_noop("Startup Alert"), + subtitle="", + get_value=self._get_startup_alert_display, + on_click=self._show_startup_alert_selector), + ], tab_key="theme", column_pair="theme", row_height=UTILITY_ROW_HEIGHT), + ] + + self._manager_view = AetherSettingsView( + self, sections, + header_title=tr_noop("Appearance"), + header_subtitle=tr_noop("Customize your display, driving widgets, model visualization, and themes."), + tab_defs=tab_defs, + panel_style=PANEL_STYLE, + ) + + # ── Widget helpers ── + + def _set_exclusive_pedal(self, key, other_key, state): + self._params.put_bool(key, state) + if state: + self._params.put_bool(other_key, False) + + # ── Camera view ── + + def _show_camera_view_selector(self): + current = self._params.get_int("CameraView") + + def on_select(res): + if res == DialogResult.CONFIRM and dialog.selection: + idx = CAMERA_VIEWS.index(dialog.selection) + self._params.put_int("CameraView", idx) + + dialog = MultiOptionDialog(tr("Camera View"), CAMERA_VIEWS, CAMERA_VIEWS[current], callback=on_select) + gui_app.push_widget(dialog) + + # ── Color selectors ── + + def _get_color_display(self, key): + val = self._params.get(key, encoding='utf-8') or "" + if not val: + return "Stock" + return val.upper() + + def _show_color_selector(self, key): + current = self._params.get(key, encoding='utf-8') or "Stock" + + def on_select(res): + if res == DialogResult.CONFIRM and dialog.selection: + if dialog.selection == "Stock": + self._params.remove(key) + else: + self._params.put(key, dialog.selection) + + dialog = MultiOptionDialog(tr(key), COLOR_PRESETS, current, callback=on_select) + gui_app.push_widget(dialog) + + # ── Numeric sliders (int / float) ── + + def _show_int_selector(self, key, min_v, max_v, unit=""): + def on_close(res, val): + if res == DialogResult.CONFIRM: + self._params.put_int(key, int(val)) + gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, + unit=unit, color="#8B5CF6")) + + def _show_float_selector(self, key, min_v, max_v, step, unit="", convert=None, unconvert=None): + current = self._params.get_float(key) + if convert: + current = convert(current) + + def on_close(res, val): + if res == DialogResult.CONFIRM: + v = float(val) + if unconvert: + v = unconvert(v) + self._params.put_float(key, v) + + gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, step, current, on_close, + unit=unit, color="#8B5CF6")) + + # ── Unit-aware display helpers ── + + def _is_metric(self): + return self._params.get_bool("IsMetric") + + def _get_lane_lines_unit(self): + return "cm" if self._is_metric() else "in" + + def _get_lane_lines_display(self): + val = self._params.get_int("LaneLinesWidth") + if self._is_metric(): + return f"{int(val * 2.54)}cm" + return f"{val}in" + + def _get_road_edges_unit(self): + return "cm" if self._is_metric() else "in" + + def _get_road_edges_display(self): + val = self._params.get_int("RoadEdgesWidth") + if self._is_metric(): + return f"{int(val * 2.54)}cm" + return f"{val}in" + + def _get_path_width_display(self): + val = self._params.get_float("PathWidth") + if self._is_metric(): + return f"{val / 3.28084:.1f}m" + return f"{val:.1f}ft" + + def _show_path_width_selector(self): + if self._is_metric(): + self._show_float_selector("PathWidth", 0, 10, 0.1, "m", convert=lambda v: v / 3.28084, unconvert=lambda v: v * 3.28084) + else: + self._show_float_selector("PathWidth", 0, 10, 0.1, "ft") + + # ── Startup alert ── + + def _get_startup_alert_display(self): + current_top = self._params.get("StartupMessageTop", encoding='utf-8') or "" + if current_top == "Be ready to take over at any time": + return "Stock" + if current_top == "Hop in and buckle up!": + return "StarPilot" + return "Clear" + + def _show_startup_alert_selector(self): + options = ["Stock", "StarPilot", "Clear"] + current = self._get_startup_alert_display() + + def on_select(res): + if res == DialogResult.CONFIRM and dialog.selection: + if dialog.selection == "Stock": + self._params.put("StartupMessageTop", "Be ready to take over at any time") + self._params.put("StartupMessageBottom", "Always keep hands on wheel and eyes on road") + elif dialog.selection == "StarPilot": + self._params.put("StartupMessageTop", "Hop in and buckle up!") + self._params.put("StartupMessageBottom", "Human-tested, frog-approved") + else: + self._params.remove("StartupMessageTop") + self._params.remove("StartupMessageBottom") + + dialog = MultiOptionDialog(tr("Startup Alert"), options, current, callback=on_select) + gui_app.push_widget(dialog) diff --git a/selfdrive/ui/layouts/settings/starpilot/main_panel.py b/selfdrive/ui/layouts/settings/starpilot/main_panel.py index 9d4975658..ddd4f5d2e 100644 --- a/selfdrive/ui/layouts/settings/starpilot/main_panel.py +++ b/selfdrive/ui/layouts/settings/starpilot/main_panel.py @@ -14,8 +14,7 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.longitudinal import StarP from openpilot.selfdrive.ui.layouts.settings.starpilot.lateral import StarPilotLateralLayout from openpilot.selfdrive.ui.layouts.settings.starpilot.maps import StarPilotMapsLayout from openpilot.selfdrive.ui.layouts.settings.starpilot.system_settings import StarPilotSystemLayout -from openpilot.selfdrive.ui.layouts.settings.starpilot.visuals import StarPilotVisualsLayout -from openpilot.selfdrive.ui.layouts.settings.starpilot.themes import StarPilotThemesLayout +from openpilot.selfdrive.ui.layouts.settings.starpilot.appearance import StarPilotAppearanceLayout from openpilot.selfdrive.ui.layouts.settings.starpilot.vehicle import StarPilotVehicleSettingsLayout from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, HubTile, RadioTileGroup, SPACING @@ -27,37 +26,37 @@ class StarPilotLayout(Widget): { "title": "Alerts & Sounds", "icon": "icon_sound.png", - "buttons": [("MANAGE", "SOUNDS", 0)], + "panel": "SOUNDS", "color": "#E63956", }, { "title": "Driving Controls", "icon": "icon_steering.png", - "buttons": [("DRIVING MODEL", "DRIVING_MODEL", 0), ("GAS / BRAKE", "LONGITUDINAL", 0), ("STEERING", "LATERAL", 0)], + "buttons": [("DRIVING MODEL", "DRIVING_MODEL"), ("GAS / BRAKE", "LONGITUDINAL"), ("STEERING", "LATERAL")], "color": "#3B82F6", }, { "title": "Map Data", "icon": "icon_navigate.png", - "buttons": [("MAP DATA", "MAPS", 0)], + "panel": "MAPS", "color": "#10B981", }, { "title": "System", "icon": "icon_system.png", - "buttons": [("MANAGE", "SYSTEM", 0)], + "panel": "SYSTEM", "color": "#D946EF", }, { "title": "Appearance", "icon": "icon_display.png", - "buttons": [("APPEARANCE", "VISUALS", 0), ("THEME", "THEMES", 0)], + "panel": "VISUALS", "color": "#8B5CF6", }, { "title": "Vehicle Settings", "icon": "icon_vehicle.png", - "buttons": [("VEHICLE SETTINGS", "VEHICLE", 0)], + "panel": "VEHICLE", "color": "#64748B", }, ] @@ -82,8 +81,7 @@ class StarPilotLayout(Widget): StarPilotPanelType.LONGITUDINAL: StarPilotPanelInfo(tr_noop("Gas / Brake"), StarPilotLongitudinalLayout()), StarPilotPanelType.LATERAL: StarPilotPanelInfo(tr_noop("Steering"), StarPilotLateralLayout()), StarPilotPanelType.MAPS: StarPilotPanelInfo(tr_noop("Map Data"), StarPilotMapsLayout()), - StarPilotPanelType.VISUALS: StarPilotPanelInfo(tr_noop("Appearance"), StarPilotVisualsLayout()), - StarPilotPanelType.THEMES: StarPilotPanelInfo(tr_noop("Themes"), StarPilotThemesLayout()), + StarPilotPanelType.VISUALS: StarPilotPanelInfo(tr_noop("Appearance"), StarPilotAppearanceLayout()), StarPilotPanelType.VEHICLE: StarPilotPanelInfo(tr_noop("Vehicle Settings"), StarPilotVehicleSettingsLayout()), } @@ -94,7 +92,6 @@ class StarPilotLayout(Widget): StarPilotPanelType.LATERAL, StarPilotPanelType.MAPS, StarPilotPanelType.VISUALS, - StarPilotPanelType.THEMES, StarPilotPanelType.VEHICLE, ) @@ -115,8 +112,7 @@ class StarPilotLayout(Widget): elif self._current_panel != StarPilotPanelType.MAIN: if self._current_category_idx is not None: cat_info = self.CATEGORIES[self._current_category_idx] - vis_btns = cat_info["buttons"] - if len(vis_btns) > 1: + if "buttons" in cat_info: self._set_current_panel(StarPilotPanelType.MAIN) else: self._current_category_idx = None @@ -134,8 +130,7 @@ class StarPilotLayout(Widget): if self._current_panel != StarPilotPanelType.MAIN: if self._current_category_idx is not None: cat_info = self.CATEGORIES[self._current_category_idx] - vis_btns = cat_info["buttons"] - depth = 2 if len(vis_btns) > 1 else 1 + depth = 2 if "buttons" in cat_info else 1 else: depth = 1 # Deep nesting check @@ -184,25 +179,19 @@ class StarPilotLayout(Widget): "LATERAL": StarPilotPanelType.LATERAL, "MAPS": StarPilotPanelType.MAPS, "VISUALS": StarPilotPanelType.VISUALS, - "THEMES": StarPilotPanelType.THEMES, "VEHICLE": StarPilotPanelType.VEHICLE, } if self._current_category_idx is None: # Main Categories Grid for i, cat in enumerate(self.CATEGORIES): - visible_buttons = cat["buttons"] - if not visible_buttons: - continue - def on_click(idx=i): cat_info = self.CATEGORIES[idx] - vis_btns = cat_info["buttons"] - if len(vis_btns) == 1: - self._current_category_idx = idx - self._set_current_panel(panel_type_map[vis_btns[0][1]]) + self._current_category_idx = idx + panel_key = cat_info.get("panel") + if panel_key is not None: + self._set_current_panel(panel_type_map[panel_key]) else: - self._current_category_idx = idx self._rebuild_grid() if self._depth_callback: self._depth_callback(1) @@ -221,7 +210,7 @@ class StarPilotLayout(Widget): cat = self.CATEGORIES[self._current_category_idx] visible_buttons = cat["buttons"] - for label, panel_key, _ in visible_buttons: + for label, panel_key in visible_buttons: p_type = panel_type_map[panel_key] def on_btn_click(p=p_type): self._set_current_panel(p) @@ -229,7 +218,7 @@ class StarPilotLayout(Widget): tile = HubTile( title=tr(label), desc="", - icon_path=f"{STARPILOT_ICONS_DIR}/{cat['icon']}", # Reuse category icon for sub-tiles + icon_path=f"{STARPILOT_ICONS_DIR}/{cat['icon']}", on_click=on_btn_click, starpilot_icon=True, bg_color=cat.get("color") diff --git a/selfdrive/ui/layouts/settings/starpilot/tabbed_panel.py b/selfdrive/ui/layouts/settings/starpilot/tabbed_panel.py deleted file mode 100644 index b23f9db42..000000000 --- a/selfdrive/ui/layouts/settings/starpilot/tabbed_panel.py +++ /dev/null @@ -1,119 +0,0 @@ -from __future__ import annotations - -from collections.abc import Callable -from dataclasses import dataclass - -import pyray as rl - -from openpilot.system.ui.lib.multilang import tr -from openpilot.system.ui.widgets import Widget - -from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import RadioTileGroup, SPACING - - -@dataclass(frozen=True) -class TabSectionSpec: - key: str - label: str - panel: Widget - - -class TabbedSectionHost(Widget): - def __init__(self, sections: list[TabSectionSpec]): - super().__init__() - if not sections: - raise ValueError("TabbedSectionHost requires at least one section") - - self._sections = {spec.key: spec.panel for spec in sections} - self._section_order = [spec.key for spec in sections] - self._active_section = self._section_order[0] - self._navigate_callback: Callable | None = None - self._back_callback: Callable | None = None - self._current_sub_panel = "" - self._tab_height = SPACING.tab_height - self._panel_top = self._tab_height + SPACING.tab_panel_gap - self._section_tabs = RadioTileGroup("", [tr(spec.label) for spec in sections], 0, self._on_tab_change) - - for key, panel in self._sections.items(): - if hasattr(panel, "set_navigate_callback"): - panel.set_navigate_callback(lambda sub_panel, section_key=key: self._on_child_navigate(section_key, sub_panel)) - if hasattr(panel, "set_back_callback"): - panel.set_back_callback(self._go_back) - - def set_navigate_callback(self, callback: Callable): - self._navigate_callback = callback - - 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 - if not sub_panel: - panel = self._sections[self._active_section] - if hasattr(panel, "set_current_sub_panel"): - panel.set_current_sub_panel("") - return - - if ":" in sub_panel: - section_key, child_panel = sub_panel.split(":", 1) - self._activate_section(section_key, child_panel) - elif sub_panel in self._sections: - self._activate_section(sub_panel) - else: - panel = self._sections[self._active_section] - if hasattr(panel, "set_current_sub_panel"): - panel.set_current_sub_panel(sub_panel) - - def _on_tab_change(self, index: int): - if 0 <= index < len(self._section_order): - self._current_sub_panel = "" - self._activate_section(self._section_order[index], "") - if self._navigate_callback: - self._navigate_callback("") - - def _activate_section(self, section_key: str, child_panel: str = ""): - if section_key not in self._sections: - return - - previous = self._active_section - if section_key != previous: - previous_panel = self._sections[previous] - if hasattr(previous_panel, "set_current_sub_panel"): - previous_panel.set_current_sub_panel("") - self._sections[previous].hide_event() - self._active_section = section_key - self._sections[section_key].show_event() - - self._section_tabs.set_index(self._section_order.index(section_key)) - panel = self._sections[section_key] - if hasattr(panel, "set_current_sub_panel"): - panel.set_current_sub_panel(child_panel) - - def _on_child_navigate(self, section_key: str, sub_panel: str): - self._current_sub_panel = f"{section_key}:{sub_panel}" if sub_panel else section_key - if self._navigate_callback: - self._navigate_callback(self._current_sub_panel) - - def _go_back(self): - self._current_sub_panel = "" - panel = self._sections[self._active_section] - if hasattr(panel, "set_current_sub_panel"): - panel.set_current_sub_panel("") - if self._back_callback: - self._back_callback() - - def _render(self, rect: rl.Rectangle): - tab_rect = rl.Rectangle(rect.x, rect.y, rect.width, self._tab_height) - panel_rect = rl.Rectangle(rect.x, rect.y + self._panel_top, rect.width, rect.height - self._panel_top) - self._section_tabs.render(tab_rect) - self._sections[self._active_section].render(panel_rect) - - def show_event(self): - super().show_event() - self._section_tabs.show_event() - self._sections[self._active_section].show_event() - - def hide_event(self): - super().hide_event() - self._section_tabs.hide_event() - self._sections[self._active_section].hide_event() diff --git a/selfdrive/ui/layouts/settings/starpilot/themes.py b/selfdrive/ui/layouts/settings/starpilot/themes.py deleted file mode 100644 index a4d2b41c5..000000000 --- a/selfdrive/ui/layouts/settings/starpilot/themes.py +++ /dev/null @@ -1,330 +0,0 @@ -from __future__ import annotations -from pathlib import Path -import re - -from openpilot.system.hardware import HARDWARE -from openpilot.system.hardware.hw import Paths -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.option_dialog import MultiOptionDialog -from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel - -if HARDWARE.get_device_type() == "pc": - THEME_SAVE_PATH = Path(Paths.comma_home()) / "starpilot" / "data" / "themes" -else: - THEME_SAVE_PATH = Path("/data/themes") - -HOLIDAY_THEME_NAMES = { - "new_years": "New Year's", - "valentines_day": "Valentine's Day", - "st_patricks_day": "St. Patrick's Day", - "world_frog_day": "World Frog Day", - "april_fools": "April Fools", - "easter_week": "Easter", - "may_the_fourth": "May the Fourth", - "cinco_de_mayo": "Cinco de Mayo", - "stitch_day": "Stitch Day", - "fourth_of_july": "Fourth of July", - "halloween_week": "Halloween", - "thanksgiving_week": "Thanksgiving", - "christmas_week": "Christmas", -} - -THEME_KEY_CONFIG = { - "BootLogo": { - "default": "starpilot", - "kind": "files", - "path": THEME_SAVE_PATH / "bootlogos", - "extra": [], - }, - "ColorScheme": { - "default": "stock", - "kind": "themes", - "path": THEME_SAVE_PATH / "theme_packs", - "subfolder": "colors", - "extra": [("stock", "Stock"), *HOLIDAY_THEME_NAMES.items()], - }, - "DistanceIconPack": { - "default": "stock", - "kind": "themes", - "path": THEME_SAVE_PATH / "theme_packs", - "subfolder": "distance_icons", - "extra": [("stock", "Stock"), *HOLIDAY_THEME_NAMES.items()], - }, - "IconPack": { - "default": "stock", - "kind": "themes", - "path": THEME_SAVE_PATH / "theme_packs", - "subfolder": "icons", - "extra": [("stock", "Stock"), *HOLIDAY_THEME_NAMES.items()], - }, - "SignalAnimation": { - "default": "stock", - "kind": "themes", - "path": THEME_SAVE_PATH / "theme_packs", - "subfolder": "signals", - "extra": [("none", "None"), *HOLIDAY_THEME_NAMES.items()], - }, - "SoundPack": { - "default": "stock", - "kind": "themes", - "path": THEME_SAVE_PATH / "theme_packs", - "subfolder": "sounds", - "extra": [("stock", "Stock"), *HOLIDAY_THEME_NAMES.items()], - }, - "WheelIcon": { - "default": "stock", - "kind": "files", - "path": THEME_SAVE_PATH / "steering_wheels", - "extra": [("none", "None"), ("stock", "Stock"), *HOLIDAY_THEME_NAMES.items()], - }, -} - - -class StarPilotThemesLayout(StarPilotPanel): - def __init__(self): - super().__init__() - - self._sub_panels = { - "personalize": StarPilotPersonalizeLayout(), - } - - self.CATEGORIES = [ - { - "title": tr_noop("Custom Themes"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("CustomThemes"), - "set_state": lambda s: self._params.put_bool("CustomThemes", s), - "icon": "toggle_icons/icon_frog.png", - "color": "#542A71", - }, - { - "title": tr_noop("Personalize openpilot"), - "panel": "personalize", - "icon": "toggle_icons/icon_frog.png", - "color": "#542A71", - "desc": tr_noop("Customize the overall look and feel."), - "visible": lambda: self._params.get_bool("CustomThemes"), - }, - { - "title": tr_noop("Holiday Themes"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("HolidayThemes"), - "set_state": lambda s: self._params.put_bool("HolidayThemes", s), - "icon": "toggle_icons/icon_calendar.png", - "color": "#542A71", - }, - { - "title": tr_noop("Rainbow Path"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("RainbowPath"), - "set_state": lambda s: self._params.put_bool("RainbowPath", s), - "icon": "toggle_icons/icon_rainbow.png", - "color": "#542A71", - }, - { - "title": tr_noop("Random Events"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("RandomEvents"), - "set_state": lambda s: self._params.put_bool("RandomEvents", s), - "icon": "toggle_icons/icon_random.png", - "color": "#542A71", - }, - { - "title": tr_noop("Random Themes"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("RandomThemes"), - "set_state": lambda s: self._params.put_bool("RandomThemes", s), - "icon": "toggle_icons/icon_random_themes.png", - "color": "#542A71", - "visible": lambda: self._params.get_bool("CustomThemes"), - }, - {"title": tr_noop("Startup Alert"), "type": "hub", "on_click": self._on_startup_alert, "color": "#542A71"}, - ] - - for name, panel in self._sub_panels.items(): - if hasattr(panel, 'set_navigate_callback'): - panel.set_navigate_callback(self._navigate_to) - if hasattr(panel, 'set_back_callback'): - panel.set_back_callback(self._go_back) - - self._rebuild_grid() - - def _on_startup_alert(self): - options = ["Stock", "StarPilot", "Clear"] - current_top = self._params.get("StartupMessageTop", encoding='utf-8') or "" - if current_top == "Be ready to take over at any time": - current = "Stock" - elif current_top == "Hop in and buckle up!": - current = "StarPilot" - else: - current = "Clear" - - def on_select(res): - if res == DialogResult.CONFIRM and dialog.selection: - if dialog.selection == "Stock": - self._params.put("StartupMessageTop", "Be ready to take over at any time") - self._params.put("StartupMessageBottom", "Always keep hands on wheel and eyes on road") - elif dialog.selection == "StarPilot": - self._params.put("StartupMessageTop", "Hop in and buckle up!") - self._params.put("StartupMessageBottom", "Human-tested, frog-approved") - else: - self._params.remove("StartupMessageTop") - self._params.remove("StartupMessageBottom") - - dialog = MultiOptionDialog(tr("Startup Alert"), options, current, callback=on_select) - gui_app.push_widget(dialog) - - -class StarPilotPersonalizeLayout(StarPilotPanel): - def __init__(self): - super().__init__() - self.CATEGORIES = [ - { - "title": tr_noop("Boot Logo"), - "type": "value", - "get_value": lambda: self._get_theme_value("BootLogo"), - "on_click": lambda: self._show_theme_selector("BootLogo"), - "color": "#542A71", - "visible": lambda: self._params.get_bool("CustomThemes"), - }, - { - "title": tr_noop("Color Scheme"), - "type": "value", - "get_value": lambda: self._get_theme_value("ColorScheme"), - "on_click": lambda: self._show_theme_selector("ColorScheme"), - "color": "#542A71", - "visible": lambda: self._params.get_bool("CustomThemes"), - }, - { - "title": tr_noop("Distance Icons"), - "type": "value", - "get_value": lambda: self._get_theme_value("DistanceIconPack"), - "on_click": lambda: self._show_theme_selector("DistanceIconPack"), - "color": "#542A71", - "visible": lambda: self._params.get_bool("CustomThemes"), - }, - { - "title": tr_noop("Icon Pack"), - "type": "value", - "get_value": lambda: self._get_theme_value("IconPack"), - "on_click": lambda: self._show_theme_selector("IconPack"), - "color": "#542A71", - "visible": lambda: self._params.get_bool("CustomThemes"), - }, - { - "title": tr_noop("Turn Signals"), - "type": "value", - "get_value": lambda: self._get_theme_value("SignalAnimation"), - "on_click": lambda: self._show_theme_selector("SignalAnimation"), - "color": "#542A71", - "visible": lambda: self._params.get_bool("CustomThemes"), - }, - { - "title": tr_noop("Sound Pack"), - "type": "value", - "get_value": lambda: self._get_theme_value("SoundPack"), - "on_click": lambda: self._show_theme_selector("SoundPack"), - "color": "#542A71", - "visible": lambda: self._params.get_bool("CustomThemes"), - }, - { - "title": tr_noop("Steering Wheel"), - "type": "value", - "get_value": lambda: self._get_theme_value("WheelIcon"), - "on_click": lambda: self._show_theme_selector("WheelIcon"), - "color": "#542A71", - "visible": lambda: self._params.get_bool("CustomThemes"), - }, - ] - self._rebuild_grid() - - @staticmethod - def _display_name(value: str) -> str: - if not value: - return "Stock" - - lowered = value.lower() - if lowered in HOLIDAY_THEME_NAMES: - return HOLIDAY_THEME_NAMES[lowered] - if lowered == "stock": - return "Stock" - if lowered == "none": - return "None" - - base, creator = (value.split("~", 1) + [""])[:2] if "~" in value else (value, "") - user_created_suffixes = ("-user_created", "_user_created", "-user-created", "_user-created") - user_created = False - for suffix in user_created_suffixes: - if base.endswith(suffix): - base = base[:-len(suffix)] - user_created = True - break - - parts = [part for part in re.split(r"[-_]+", base) if part] - display = " ".join(part[:1].upper() + part[1:] for part in parts) if parts else value - if user_created: - display += " (User Created)" - if creator: - display += f" - by: {creator}" - return display - - def _get_downloaded_slugs(self, key: str) -> list[str]: - config = THEME_KEY_CONFIG[key] - path = config["path"] - if not path.exists(): - return [] - - slugs = set() - if config["kind"] == "files": - for entry in path.iterdir(): - if entry.is_file(): - slugs.add(entry.stem) - else: - subfolder = config["subfolder"] - for entry in path.iterdir(): - if entry.is_dir() and (entry / subfolder).exists(): - slugs.add(entry.name) - - return sorted(slugs, key=str.casefold) - - def _build_theme_options(self, key: str) -> tuple[list[str], dict[str, str], str]: - config = THEME_KEY_CONFIG[key] - current_slug = self._params.get(key, encoding='utf-8') or config["default"] - - options_map = {} - for slug in self._get_downloaded_slugs(key): - display = self._display_name(slug) - if display not in options_map: - options_map[display] = slug - - for slug, display in config["extra"]: - options_map[display] = slug - - current_display = self._display_name(current_slug) - if current_display not in options_map: - options_map[current_display] = current_slug - - options = sorted(options_map.keys(), key=str.casefold) - return options, options_map, current_display - - def _get_theme_value(self, key: str) -> str: - default = THEME_KEY_CONFIG[key]["default"] - return self._display_name(self._params.get(key, encoding='utf-8') or default) - - def _show_theme_selector(self, key): - themes, option_map, current = self._build_theme_options(key) - if not themes: - return - - def on_select(res): - if res == DialogResult.CONFIRM and dialog.selection: - selected_slug = option_map.get(dialog.selection) - if selected_slug is None: - return - self._params.put(key, selected_slug) - self._rebuild_grid() - - dialog = MultiOptionDialog(tr(key), themes, current, callback=on_select) - gui_app.push_widget(dialog) diff --git a/selfdrive/ui/layouts/settings/starpilot/visuals.py b/selfdrive/ui/layouts/settings/starpilot/visuals.py deleted file mode 100644 index 891ef1ab2..000000000 --- a/selfdrive/ui/layouts/settings/starpilot/visuals.py +++ /dev/null @@ -1,578 +0,0 @@ -from __future__ import annotations -from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state -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.option_dialog import MultiOptionDialog -from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel, create_tile_panel -from openpilot.selfdrive.ui.layouts.settings.starpilot.tabbed_panel import TabSectionSpec, TabbedSectionHost -from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, ToggleTile, AetherSliderDialog -class StarPilotVisualsLayout(StarPilotPanel): - def __init__(self): - super().__init__() - display_panel = create_tile_panel([ - {"title": tr_noop("Advanced UI Controls"), "panel": "advanced", "icon": "toggle_icons/icon_advanced_device.png", "color": "#8B5CF6"}, - {"title": tr_noop("Quality of Life"), "panel": "qol", "icon": "toggle_icons/icon_quality_of_life.png", "color": "#8B5CF6"}, - ], { - "advanced": StarPilotAdvancedVisualsLayout(), - "qol": StarPilotVisualQOLLayout(), - }) - - self._section_tabs = TabbedSectionHost([ - TabSectionSpec("display", "Display", display_panel), - TabSectionSpec("widgets", "Widgets", StarPilotVisualWidgetsLayout()), - TabSectionSpec("model", "Model", StarPilotModelUILayout()), - TabSectionSpec("navigation", "Nav", StarPilotNavigationVisualsLayout()), - ]) - - def set_navigate_callback(self, callback): - self._section_tabs.set_navigate_callback(callback) - - def set_back_callback(self, callback): - self._section_tabs.set_back_callback(callback) - - def _render(self, rect): - self._section_tabs.render(rect) - - def set_current_sub_panel(self, sub_panel: str): - self._section_tabs.set_current_sub_panel(sub_panel) - - def show_event(self): - self._section_tabs.show_event() - - def hide_event(self): - self._section_tabs.hide_event() - - -class StarPilotAdvancedVisualsLayout(StarPilotPanel): - def __init__(self): - super().__init__() - self.CATEGORIES = [ - { - "title": tr_noop("Advanced UI Controls"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("AdvancedCustomUI"), - "set_state": lambda s: self._params.put_bool("AdvancedCustomUI", s), - "icon": "toggle_icons/icon_advanced_device.png", - "color": "#8B5CF6", - }, - { - "title": tr_noop("Hide Speed"), - "type": "toggle", - "key": "HideSpeed", - "get_state": lambda: self._params.get_bool("HideSpeed"), - "set_state": lambda s: self._params.put_bool("HideSpeed", s), - "icon": "toggle_icons/icon_display.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("AdvancedCustomUI"), - }, - { - "title": tr_noop("Hide Lead Marker"), - "type": "toggle", - "key": "HideLeadMarker", - "get_state": lambda: self._params.get_bool("HideLeadMarker"), - "set_state": lambda s: self._params.put_bool("HideLeadMarker", s), - "icon": "toggle_icons/icon_display.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("AdvancedCustomUI"), - }, - { - "title": tr_noop("Hide Max Speed"), - "type": "toggle", - "key": "HideMaxSpeed", - "get_state": lambda: self._params.get_bool("HideMaxSpeed"), - "set_state": lambda s: self._params.put_bool("HideMaxSpeed", s), - "icon": "toggle_icons/icon_display.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("AdvancedCustomUI"), - }, - { - "title": tr_noop("Hide Alerts"), - "type": "toggle", - "key": "HideAlerts", - "get_state": lambda: self._params.get_bool("HideAlerts"), - "set_state": lambda s: self._params.put_bool("HideAlerts", s), - "icon": "toggle_icons/icon_display.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("AdvancedCustomUI"), - }, - { - "title": tr_noop("Hide Speed Limit"), - "type": "toggle", - "key": "HideSpeedLimit", - "get_state": lambda: self._params.get_bool("HideSpeedLimit"), - "set_state": lambda s: self._params.put_bool("HideSpeedLimit", s), - "icon": "toggle_icons/icon_display.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("AdvancedCustomUI"), - }, - { - "title": tr_noop("Wheel Speed"), - "type": "toggle", - "key": "WheelSpeed", - "get_state": lambda: self._params.get_bool("WheelSpeed"), - "set_state": lambda s: self._params.put_bool("WheelSpeed", s), - "icon": "toggle_icons/icon_display.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("AdvancedCustomUI"), - }, - ] - self._rebuild_grid() - - def _rebuild_grid(self): - if not self.CATEGORIES: - return - if self._tile_grid is None: - self._tile_grid = TileGrid(columns=None, padding=20) - self._tile_grid.clear() - - for cat in self.CATEGORIES: - key = cat.get("key") - visible = True - - if key == "HideLeadMarker": - visible &= starpilot_state.car_state.hasOpenpilotLongitudinal - - if not visible: - continue - - 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")) - self._tile_grid.add_tile(tile) - - -class StarPilotVisualWidgetsLayout(StarPilotPanel): - def __init__(self): - super().__init__() - self.CATEGORIES = [ - { - "title": tr_noop("Driving Screen Widgets"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("CustomUI"), - "set_state": lambda s: self._params.put_bool("CustomUI", s), - "icon": "toggle_icons/icon_display.png", - "color": "#8B5CF6", - }, - { - "title": tr_noop("Acceleration Path"), - "type": "toggle", - "key": "AccelerationPath", - "get_state": lambda: self._params.get_bool("AccelerationPath"), - "set_state": lambda s: self._params.put_bool("AccelerationPath", s), - "icon": "toggle_icons/icon_road.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("CustomUI"), - }, - { - "title": tr_noop("Adjacent Lanes"), - "type": "toggle", - "key": "AdjacentPath", - "get_state": lambda: self._params.get_bool("AdjacentPath"), - "set_state": lambda s: self._params.put_bool("AdjacentPath", s), - "icon": "toggle_icons/icon_road.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("CustomUI"), - }, - { - "title": tr_noop("Adjacent Lane Metrics"), - "type": "toggle", - "key": "AdjacentPathMetrics", - "get_state": lambda: self._params.get_bool("AdjacentPathMetrics"), - "set_state": lambda s: self._params.put_bool("AdjacentPathMetrics", s), - "icon": "toggle_icons/icon_road.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("CustomUI"), - }, - { - "title": tr_noop("Blind Spot Path"), - "type": "toggle", - "key": "BlindSpotPath", - "get_state": lambda: self._params.get_bool("BlindSpotPath"), - "set_state": lambda s: self._params.put_bool("BlindSpotPath", s), - "icon": "toggle_icons/icon_road.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("CustomUI"), - }, - { - "title": tr_noop("Compass"), - "type": "toggle", - "key": "Compass", - "get_state": lambda: self._params.get_bool("Compass"), - "set_state": lambda s: self._params.put_bool("Compass", s), - "icon": "toggle_icons/icon_navigate.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("CustomUI"), - }, - { - "title": tr_noop("Personality Button"), - "type": "toggle", - "key": "OnroadDistanceButton", - "get_state": lambda: self._params.get_bool("OnroadDistanceButton"), - "set_state": lambda s: self._params.put_bool("OnroadDistanceButton", s), - "icon": "toggle_icons/icon_personality.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("CustomUI"), - }, - { - "title": tr_noop("Pedal Indicators"), - "type": "toggle", - "key": "PedalsOnUI", - "get_state": lambda: self._params.get_bool("PedalsOnUI"), - "set_state": lambda s: self._params.put_bool("PedalsOnUI", s), - "icon": "toggle_icons/icon_display.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("CustomUI"), - }, - { - "title": tr_noop("Dynamic Pedals"), - "type": "toggle", - "key": "DynamicPedalsOnUI", - "get_state": lambda: self._params.get_bool("DynamicPedalsOnUI"), - "set_state": lambda s: self._set_exclusive_pedal("DynamicPedalsOnUI", "StaticPedalsOnUI", s), - "icon": "toggle_icons/icon_display.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("CustomUI") and self._params.get_bool("PedalsOnUI"), - }, - { - "title": tr_noop("Static Pedals"), - "type": "toggle", - "key": "StaticPedalsOnUI", - "get_state": lambda: self._params.get_bool("StaticPedalsOnUI"), - "set_state": lambda s: self._set_exclusive_pedal("StaticPedalsOnUI", "DynamicPedalsOnUI", s), - "icon": "toggle_icons/icon_display.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("CustomUI") and self._params.get_bool("PedalsOnUI"), - }, - { - "title": tr_noop("Rotating Wheel"), - "type": "toggle", - "key": "RotatingWheel", - "get_state": lambda: self._params.get_bool("RotatingWheel"), - "set_state": lambda s: self._params.put_bool("RotatingWheel", s), - "icon": "toggle_icons/icon_steering.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("CustomUI"), - }, - ] - self._rebuild_grid() - - def _set_exclusive_pedal(self, key, other_key, state): - self._params.put_bool(key, state) - if state: - self._params.put_bool(other_key, False) - self._rebuild_grid() - - def _rebuild_grid(self): - if not self.CATEGORIES: - return - if self._tile_grid is None: - self._tile_grid = TileGrid(columns=None, padding=20) - self._tile_grid.clear() - - pedals_on_ui = self._params.get_bool("PedalsOnUI") - - for cat in self.CATEGORIES: - key = cat.get("key") - visible = True - - if key == "AccelerationPath": - visible &= starpilot_state.car_state.hasOpenpilotLongitudinal - elif key == "BlindSpotPath": - visible &= starpilot_state.car_state.hasBSM - elif key == "PedalsOnUI": - visible &= starpilot_state.car_state.hasOpenpilotLongitudinal - elif key == "DynamicPedalsOnUI": - visible &= starpilot_state.car_state.hasOpenpilotLongitudinal and pedals_on_ui - elif key == "StaticPedalsOnUI": - visible &= starpilot_state.car_state.hasOpenpilotLongitudinal and pedals_on_ui - - if not visible: - continue - - 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")) - self._tile_grid.add_tile(tile) - - -class StarPilotModelUILayout(StarPilotPanel): - def __init__(self): - super().__init__() - self.CATEGORIES = [ - { - "title": tr_noop("Model UI"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("ModelUI"), - "set_state": lambda s: self._params.put_bool("ModelUI", s), - "icon": "toggle_icons/icon_road.png", - "color": "#8B5CF6", - }, - { - "title": tr_noop("Dynamic Path"), - "type": "toggle", - "key": "DynamicPathWidth", - "get_state": lambda: self._params.get_bool("DynamicPathWidth"), - "set_state": lambda s: self._params.put_bool("DynamicPathWidth", s), - "icon": "toggle_icons/icon_road.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("ModelUI"), - }, - { - "title": tr_noop("Lane Line Width"), - "type": "value", - "key": "LaneLinesWidth", - "get_value": lambda: self._get_lane_lines_display(), - "on_click": lambda: self._show_int_selector("LaneLinesWidth", 0, 24, self._get_lane_lines_unit()), - "icon": "toggle_icons/icon_road.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("ModelUI"), - }, - { - "title": tr_noop("Lane Line Color"), - "type": "value", - "key": "LaneLinesColor", - "get_value": lambda: self._get_color_display("LaneLinesColor"), - "on_click": lambda: self._show_color_selector("LaneLinesColor"), - "icon": "toggle_icons/icon_road.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("ModelUI"), - }, - { - "title": tr_noop("Border Width"), - "type": "value", - "key": "BorderWidth", - "get_value": lambda: self._get_border_width_display(), - "on_click": lambda: self._show_float_selector("BorderWidth", 25, 250, 5, "%"), - "icon": "toggle_icons/icon_road.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("ModelUI"), - }, - { - "title": tr_noop("Path Edge Width"), - "type": "value", - "key": "PathEdgeWidth", - "get_value": lambda: f"{self._params.get_int('PathEdgeWidth')}%", - "on_click": lambda: self._show_int_selector("PathEdgeWidth", 0, 100, "%"), - "icon": "toggle_icons/icon_road.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("ModelUI"), - }, - { - "title": tr_noop("Path Edge Color"), - "type": "value", - "key": "PathEdgesColor", - "get_value": lambda: self._get_color_display("PathEdgesColor"), - "on_click": lambda: self._show_color_selector("PathEdgesColor"), - "icon": "toggle_icons/icon_road.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("ModelUI"), - }, - { - "title": tr_noop("Path Width"), - "type": "value", - "key": "PathWidth", - "get_value": lambda: self._get_path_width_display(), - "on_click": lambda: self._show_path_width_selector(), - "icon": "toggle_icons/icon_road.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("ModelUI"), - }, - { - "title": tr_noop("Path Color"), - "type": "value", - "key": "PathColor", - "get_value": lambda: self._get_color_display("PathColor"), - "on_click": lambda: self._show_color_selector("PathColor"), - "icon": "toggle_icons/icon_road.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("ModelUI"), - }, - { - "title": tr_noop("Road Edge Width"), - "type": "value", - "key": "RoadEdgesWidth", - "get_value": lambda: self._get_road_edges_display(), - "on_click": lambda: self._show_int_selector("RoadEdgesWidth", 0, 24, self._get_road_edges_unit()), - "icon": "toggle_icons/icon_road.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("ModelUI"), - }, - ] - self._rebuild_grid() - - def _get_lane_lines_unit(self): - return "cm" if self._params.get_bool("IsMetric") else "in" - - def _get_lane_lines_display(self): - val = self._params.get_int('LaneLinesWidth') - if self._params.get_bool("IsMetric"): - return f"{int(val * 2.54)}cm" - return f"{val}in" - - def _get_path_width_unit(self): - return "m" if self._params.get_bool("IsMetric") else "ft" - - def _get_border_width_display(self): - return f"{int(round(self._params.get_float('BorderWidth')))}%" - - def _get_path_width_display(self): - val = self._params.get_float('PathWidth') - if self._params.get_bool("IsMetric"): - return f"{val / 3.28084:.1f}m" - return f"{val:.1f}ft" - - def _get_road_edges_unit(self): - return "cm" if self._params.get_bool("IsMetric") else "in" - - def _get_road_edges_display(self): - val = self._params.get_int('RoadEdgesWidth') - if self._params.get_bool("IsMetric"): - return f"{int(val * 2.54)}cm" - return f"{val}in" - - def _show_path_width_selector(self): - if self._params.get_bool("IsMetric"): - self._show_float_selector("PathWidth", 0, 10, 0.1, "m", convert=lambda v: v / 3.28084, unconvert=lambda v: v * 3.28084) - else: - self._show_float_selector("PathWidth", 0, 10, 0.1, "ft") - - def _show_int_selector(self, key, min_v, max_v, unit=""): - def on_close(res, val): - if res == DialogResult.CONFIRM: - self._params.put_int(key, int(val)) - self._rebuild_grid() - - gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#8B5CF6")) - - def _show_float_selector(self, key, min_v, max_v, step, unit="", convert=None, unconvert=None): - current = self._params.get_float(key) - if convert: - current = convert(current) - - def on_close(res, val): - if res == DialogResult.CONFIRM: - v = float(val) - if unconvert: - v = unconvert(v) - self._params.put_float(key, v) - self._rebuild_grid() - - gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, step, current, on_close, unit=unit, color="#8B5CF6")) - - def _get_color_display(self, key): - val = self._params.get(key, encoding='utf-8') or "" - if not val: - return "Stock" - return val.upper() - - def _show_color_selector(self, key): - presets = ["Stock", "#FFFFFF", "#178644", "#3B82F6", "#E63956", "#8B5CF6", "#F59E0B"] - current = self._params.get(key, encoding='utf-8') or "Stock" - - def on_select(res): - if res == DialogResult.CONFIRM and dialog.selection: - if dialog.selection == "Stock": - self._params.remove(key) - else: - self._params.put(key, dialog.selection) - self._rebuild_grid() - - dialog = MultiOptionDialog(tr(key), presets, current, callback=on_select) - gui_app.push_widget(dialog) - - -class StarPilotNavigationVisualsLayout(StarPilotPanel): - def __init__(self): - super().__init__() - self.CATEGORIES = [ - { - "title": tr_noop("Navigation Widgets"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("NavigationUI"), - "set_state": lambda s: self._params.put_bool("NavigationUI", s), - "icon": "toggle_icons/icon_map.png", - "color": "#8B5CF6", - }, - { - "title": tr_noop("Road Name"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("RoadNameUI"), - "set_state": lambda s: self._params.put_bool("RoadNameUI", s), - "icon": "toggle_icons/icon_navigate.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("NavigationUI"), - }, - { - "title": tr_noop("Speed Limits"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("ShowSpeedLimits"), - "set_state": lambda s: self._params.put_bool("ShowSpeedLimits", s), - "icon": "toggle_icons/icon_speed_limit.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("NavigationUI"), - }, - { - "title": tr_noop("Vienna Signs"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("UseVienna"), - "set_state": lambda s: self._params.put_bool("UseVienna", s), - "icon": "toggle_icons/icon_speed_limit.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("NavigationUI"), - }, - ] - self._rebuild_grid() - - -class StarPilotVisualQOLLayout(StarPilotPanel): - def __init__(self): - super().__init__() - self.CAMERA_VIEWS = ["Auto", "Driver", "Standard", "Wide"] - self.CATEGORIES = [ - { - "title": tr_noop("Quality of Life"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("QOLVisuals"), - "set_state": lambda s: self._params.put_bool("QOLVisuals", s), - "icon": "toggle_icons/icon_quality_of_life.png", - "color": "#8B5CF6", - }, - { - "title": tr_noop("Camera View"), - "type": "value", - "key": "CameraView", - "get_value": lambda: tr(self.CAMERA_VIEWS[self._params.get_int('CameraView')]), - "on_click": lambda: self._show_camera_view_selector(), - "icon": "toggle_icons/icon_display.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("QOLVisuals"), - }, - { - "title": tr_noop("Driver Camera"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("DriverCamera"), - "set_state": lambda s: self._params.put_bool("DriverCamera", s), - "icon": "toggle_icons/icon_display.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("QOLVisuals"), - }, - { - "title": tr_noop("Stopped Timer"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("StoppedTimer"), - "set_state": lambda s: self._params.put_bool("StoppedTimer", s), - "icon": "toggle_icons/icon_display.png", - "color": "#8B5CF6", - "visible": lambda: self._params.get_bool("QOLVisuals"), - }, - ] - self._rebuild_grid() - - def _show_camera_view_selector(self): - current = self._params.get_int("CameraView") - - def on_select(res): - if res == DialogResult.CONFIRM and dialog.selection: - idx = self.CAMERA_VIEWS.index(dialog.selection) - self._params.put_int("CameraView", idx) - self._rebuild_grid() - - dialog = MultiOptionDialog(tr("Camera View"), self.CAMERA_VIEWS, self.CAMERA_VIEWS[current], callback=on_select) - gui_app.push_widget(dialog)