BigUI WIP: Apperance + Cleanup

This commit is contained in:
firestarsdog
2026-05-03 02:50:46 -04:00
parent 650b79941f
commit 989b69ee92
5 changed files with 627 additions and 1054 deletions
@@ -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)
@@ -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")
@@ -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()
@@ -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)
@@ -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)