mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
244 lines
11 KiB
Python
244 lines
11 KiB
Python
"""Immutable Settings entry data and narrow, explicit preview navigation."""
|
|
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass, field, replace
|
|
from enum import StrEnum
|
|
|
|
from openpilot.starpilot.ui.device_state import DeviceAction, DeviceState
|
|
from openpilot.starpilot.ui.feature_settings_state import FEATURE_BACK_WIDTH, FEATURE_HEADER_HEIGHT
|
|
from openpilot.starpilot.ui.home_state import HomeAction, HomeActionKind
|
|
from openpilot.starpilot.ui.presentation import Profile
|
|
from openpilot.starpilot.ui.software_state import SoftwareAction, SoftwareState
|
|
from openpilot.starpilot.ui.toggles_state import ToggleRequest, TogglesState
|
|
|
|
|
|
class Destination(StrEnum):
|
|
STAR = "star"
|
|
DEVICE = "device"
|
|
NETWORK = "network"
|
|
BLUETOOTH = "bluetooth"
|
|
TOGGLES = "toggles"
|
|
SOFTWARE = "software"
|
|
DEVELOPER = "developer"
|
|
SOUNDS = "sounds"
|
|
DRIVING_MODEL = "driving_model"
|
|
DRIVING_CONTROLS = "driving_controls"
|
|
SYSTEM = "system"
|
|
APPEARANCE = "appearance"
|
|
VEHICLE = "vehicle"
|
|
FIREHOSE = "firehose"
|
|
FORCE_DRIVE = "force_drive"
|
|
GALAXY = "galaxy"
|
|
PAIR = "pair"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DestinationAvailability:
|
|
destination: Destination
|
|
available: bool = False
|
|
reason: str = "This destination is not implemented in the development preview."
|
|
request_value: str = ""
|
|
request_revision: str | None = None
|
|
|
|
|
|
RAIL = ((Destination.STAR, "StarPilot"), (Destination.DEVICE, "Device"), (Destination.NETWORK, "Network"),
|
|
(Destination.BLUETOOTH, "Bluetooth"), (Destination.TOGGLES, "Toggles"), (Destination.SOFTWARE, "Software"),
|
|
(Destination.DEVELOPER, "Developer"))
|
|
TILES = ((Destination.SOUNDS, "Sounds & Alerts", "sound"), (Destination.DRIVING_MODEL, "Driving Model", "aicar"),
|
|
(Destination.DRIVING_CONTROLS, "Driving Controls", "steering"), (Destination.SYSTEM, "System", "system"),
|
|
(Destination.APPEARANCE, "Appearance", "display"), (Destination.VEHICLE, "Vehicle Settings", "vehicle"))
|
|
|
|
COMPACT_MENU = (
|
|
(Destination.TOGGLES, "Toggles", "icons_mici/settings.png", 64, 64),
|
|
(Destination.NETWORK, "Network", "icons_mici/settings/network/wifi_strength_full.png", 76, 56),
|
|
(Destination.BLUETOOTH, "Bluetooth", "icons_mici/settings/bluetooth.png", 64, 64),
|
|
(Destination.FORCE_DRIVE, "Force Drive State", None, 0, 0),
|
|
(Destination.VEHICLE, "Vehicle", "icons_mici/settings/vehicle.png", 64, 57),
|
|
(Destination.DEVICE, "Device", "icons_mici/settings/device_icon.png", 72, 58),
|
|
(Destination.SOFTWARE, "Software", "icons_mici/settings/device/update.png", 64, 75),
|
|
(Destination.DRIVING_MODEL, "Driving Model", "icons_mici/settings/device/lkas.png", 72, 56),
|
|
(Destination.DRIVING_CONTROLS, "Driving Controls", "icons_mici/settings/device/lkas.png", 72, 56),
|
|
(Destination.APPEARANCE, "Visuals", "icons_mici/settings/device/cameras.png", 64, 64),
|
|
(Destination.GALAXY, "Galaxy", "icons_mici/settings/galaxy.png", 64, 64),
|
|
(Destination.PAIR, "Pair to Connect", "icons_mici/settings/comma_icon.png", 33, 60),
|
|
(Destination.DEVELOPER, "Developer", "icons_mici/settings/developer_icon.png", 64, 60),
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SettingsState:
|
|
sidebar_expanded: bool = True
|
|
# Compact rendering accepts a static position snapshot. It does not reproduce
|
|
# the original scroller, entry bounce or dismissal animation.
|
|
compact_y: float = 0.0
|
|
compact_scroll_x: float = 0.0
|
|
paired: bool = False
|
|
nav_bar_y: float = 6.0
|
|
nav_bar_alpha: float = 1.0
|
|
force_drive_label: str = "force drive state"
|
|
availability: tuple[DestinationAvailability, ...] = field(default_factory=lambda: tuple(DestinationAvailability(d) for d in Destination))
|
|
|
|
def destination(self, destination: Destination) -> DestinationAvailability:
|
|
return next(item for item in self.availability if item.destination == destination)
|
|
|
|
|
|
def compact_menu(state: SettingsState):
|
|
return tuple((item[0], state.force_drive_label, *item[2:]) if item[0] == Destination.FORCE_DRIVE else item
|
|
for item in COMPACT_MENU if not (state.paired and item[0] == Destination.PAIR))
|
|
|
|
|
|
class SettingsActionKind(StrEnum):
|
|
CLOSE = "close"
|
|
TOGGLE_SIDEBAR = "toggle_sidebar"
|
|
REQUEST_DESTINATION = "request_destination"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SettingsAction:
|
|
kind: SettingsActionKind
|
|
destination: DestinationAvailability | None = None
|
|
|
|
|
|
def tile_rects(state: SettingsState) -> tuple[tuple[float, float, float, float], ...]:
|
|
x, width = (520.0, 1620.0) if state.sidebar_expanded else (20.0, 2120.0)
|
|
tile_width = (width - 32) / 3
|
|
return tuple((round(x + column * (tile_width + 16)), 112 + row * 477, round(tile_width), 461)
|
|
for row in range(2) for column in range(3))
|
|
|
|
|
|
class SettingsInput:
|
|
"""Single-pointer static entry hit testing; drags cancel and never scroll.
|
|
|
|
Compact back and motion belong to the eventual application adapter. The
|
|
development router exposes a separate Back control, never an invented swipe.
|
|
"""
|
|
|
|
def __init__(self, profile: Profile, emit: Callable[[SettingsAction], None],
|
|
selected: Callable[[], Destination] | None = None):
|
|
self.profile, self.emit = profile, emit
|
|
self.selected = selected or (lambda: Destination.STAR)
|
|
self._pressed: tuple[float, float, str] | None = None
|
|
|
|
def _target(self, x: float, y: float, state: SettingsState) -> str | None:
|
|
def inside(rect):
|
|
rx, ry, width, height = rect
|
|
return rx <= x <= rx + width and ry <= y <= ry + height
|
|
if not (0 <= x < self.profile.size[0] and 0 <= y < self.profile.size[1]):
|
|
return None
|
|
if self.profile == Profile.COMPACT:
|
|
for index, (destination, _, _, _, _) in enumerate(compact_menu(state)):
|
|
if inside((20 + 422 * index + state.compact_scroll_x, state.compact_y + 30, 402, 180)):
|
|
return destination
|
|
return None
|
|
if inside((0, 511, 40, 140)):
|
|
return "collapse"
|
|
if state.sidebar_expanded:
|
|
if inside((150, 60, 200, 200)):
|
|
return "close"
|
|
for index, (destination, _) in enumerate(RAIL):
|
|
if inside((50, 300 + index * 110, 350, 110)):
|
|
return destination
|
|
if self.selected() == Destination.STAR:
|
|
if (not state.sidebar_expanded and 20 <= x < 20 + FEATURE_BACK_WIDTH and
|
|
12 <= y <= 12 + FEATURE_HEADER_HEIGHT):
|
|
return "close"
|
|
for (destination, _, _), rect in zip(TILES, tile_rects(state), strict=True):
|
|
if inside(rect):
|
|
return destination
|
|
return None
|
|
|
|
def press(self, x: float, y: float, state: SettingsState) -> None:
|
|
target = self._target(x, y, state)
|
|
self._pressed = (x, y, target) if target is not None else None
|
|
|
|
def move(self, x: float, y: float, state: SettingsState) -> None:
|
|
if self._pressed is not None:
|
|
px, py, target = self._pressed
|
|
if ((self.profile == Profile.COMPACT and (abs(x - px) > 5 or abs(y - py) > 5)) or
|
|
self._target(x, y, state) != target):
|
|
self.cancel()
|
|
|
|
def release(self, x: float, y: float, state: SettingsState) -> None:
|
|
self.move(x, y, state)
|
|
if self._pressed is None:
|
|
return
|
|
_, _, target = self._pressed
|
|
self.cancel()
|
|
if target == "close":
|
|
self.emit(SettingsAction(SettingsActionKind.CLOSE))
|
|
elif target == "collapse":
|
|
self.emit(SettingsAction(SettingsActionKind.TOGGLE_SIDEBAR))
|
|
elif isinstance(target, Destination):
|
|
self.emit(SettingsAction(SettingsActionKind.REQUEST_DESTINATION, state.destination(Destination(target))))
|
|
|
|
def cancel(self) -> None:
|
|
self._pressed = None
|
|
|
|
|
|
class PreviewRouter:
|
|
"""Development-only Home/Settings/large leaf routing and inert requests."""
|
|
|
|
def __init__(self, profile: Profile = Profile.LARGE):
|
|
self.profile = profile
|
|
self.in_settings = False
|
|
self.settings = SettingsState()
|
|
if profile == Profile.LARGE:
|
|
self.settings = replace(self.settings, availability=tuple(
|
|
replace(item, available=True, reason="Available in the large development preview.")
|
|
if item.destination in (Destination.DEVICE, Destination.SOFTWARE, Destination.TOGGLES) else item for item in self.settings.availability))
|
|
self.selected = Destination.STAR
|
|
self.device = DeviceState()
|
|
self.software = SoftwareState()
|
|
self.toggles = TogglesState()
|
|
self.notice = "Development preview — large Device, Toggles and Software navigation is available; other leaves and compact scrolling are unavailable."
|
|
self.requests: list[SettingsAction] = []
|
|
self.device_requests: list[DeviceAction] = []
|
|
self.software_requests: list[SoftwareAction] = []
|
|
self.toggle_requests: list[ToggleRequest] = []
|
|
|
|
def home_action(self, action: HomeAction) -> None:
|
|
if action.kind == HomeActionKind.OPEN_SETTINGS:
|
|
self.in_settings = True
|
|
self.selected = Destination.STAR
|
|
else:
|
|
self.notice = "Development preview — this Home action has no connected backend."
|
|
|
|
def settings_action(self, action: SettingsAction) -> None:
|
|
if action.kind == SettingsActionKind.CLOSE:
|
|
if self.selected in (Destination.DEVICE, Destination.SOFTWARE, Destination.TOGGLES):
|
|
self.selected = Destination.STAR
|
|
else:
|
|
self.in_settings = False
|
|
elif action.kind == SettingsActionKind.TOGGLE_SIDEBAR:
|
|
self.settings = replace(self.settings, sidebar_expanded=not self.settings.sidebar_expanded)
|
|
else:
|
|
if action.destination is not None:
|
|
destination = action.destination.destination
|
|
if destination == Destination.STAR:
|
|
self.selected = Destination.STAR
|
|
elif destination == Destination.DEVICE and self.profile == Profile.LARGE:
|
|
self.selected = Destination.DEVICE
|
|
elif destination == Destination.SOFTWARE and self.profile == Profile.LARGE:
|
|
self.selected = Destination.SOFTWARE
|
|
elif destination == Destination.TOGGLES and self.profile == Profile.LARGE:
|
|
self.selected = Destination.TOGGLES
|
|
else:
|
|
self.requests.append(action)
|
|
self.notice = f"Preview request: {destination}. {action.destination.reason}"
|
|
|
|
def device_action(self, action: DeviceAction) -> None:
|
|
if self.in_settings and self.selected == Destination.DEVICE:
|
|
self.device_requests.append(action)
|
|
self.notice = f"Preview request: {action.request}. No device operation is connected."
|
|
|
|
def software_action(self, action: SoftwareAction) -> None:
|
|
if self.profile == Profile.LARGE and self.in_settings and self.selected == Destination.SOFTWARE:
|
|
self.software_requests.append(action)
|
|
unavailable = "No update, install, uninstall, configuration change or file read is connected."
|
|
self.notice = f"Preview request: {action.request}. {unavailable}"
|
|
|
|
def toggle_action(self, action: ToggleRequest) -> None:
|
|
if self.profile == Profile.LARGE and self.in_settings and self.selected == Destination.TOGGLES:
|
|
self.toggle_requests.append(action)
|
|
self.notice = f"Preview request: {action}. No setting change is connected."
|