mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-09 19:39:00 +08:00
BigUI WIP: System Settings
This commit is contained in:
@@ -1466,3 +1466,125 @@ def draw_toggle_pill(rect: rl.Rectangle, is_on: bool, is_enabled: bool, title: s
|
||||
ts = measure_text_cached(font, status_str, 24)
|
||||
status_x = rect.x + rect.width - ts.x - 24
|
||||
rl.draw_text_ex(font, status_str, rl.Vector2(round(status_x), round(title_y)), 24, 0, text_color)
|
||||
|
||||
|
||||
class AetherVerticalSlider(Widget):
|
||||
"""Touch-first vertical slider for inline dashboard use.
|
||||
Designed for automotive touch targets (60px+ wide tracks).
|
||||
Renders: title above, vertical fill track, value label below."""
|
||||
|
||||
MIN_TRACK_WIDTH = 60 # Automotive touch minimum
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
min_val: float,
|
||||
max_val: float,
|
||||
step: float,
|
||||
current_val: float,
|
||||
on_change: Callable[[float], None],
|
||||
title: str = "",
|
||||
unit: str = "",
|
||||
labels: dict[float, str] | None = None,
|
||||
color: rl.Color | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
self.min_val = min_val
|
||||
self.max_val = max_val
|
||||
self.base_step = step
|
||||
self.current_val = current_val
|
||||
self.on_change = on_change
|
||||
self.title = title
|
||||
self.unit = unit
|
||||
self.labels = labels or {}
|
||||
self.color = color or AetherListColors.PRIMARY
|
||||
|
||||
self._is_dragging = False
|
||||
self._last_mouse_y = 0.0
|
||||
self._smooth_value = current_val
|
||||
self._track_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self._font = gui_app.font(FontWeight.BOLD)
|
||||
|
||||
def _handle_mouse_press(self, mouse_pos: MousePos):
|
||||
if rl.check_collision_point_rec(mouse_pos, self._rect):
|
||||
self._is_dragging = True
|
||||
self._last_mouse_y = mouse_pos.y
|
||||
self._update_val_from_y(mouse_pos.y, self.base_step)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
self._is_dragging = False
|
||||
|
||||
def _handle_mouse_event(self, mouse_event: MouseEvent):
|
||||
if self._is_dragging:
|
||||
dt = rl.get_frame_time()
|
||||
dy = mouse_event.pos.y - self._last_mouse_y
|
||||
self._last_mouse_y = mouse_event.pos.y
|
||||
velocity = abs(dy / max(dt, 0.001))
|
||||
if velocity > 1500:
|
||||
step = self.base_step * 10
|
||||
elif velocity > 500:
|
||||
step = self.base_step * 5
|
||||
else:
|
||||
step = self.base_step
|
||||
self._update_val_from_y(mouse_event.pos.y, step)
|
||||
|
||||
def _update_val_from_y(self, mouse_y: float, step: float):
|
||||
tr_rect = self._track_rect
|
||||
if tr_rect.height <= 0:
|
||||
return
|
||||
# Inverted: top = max, bottom = min
|
||||
frac = 1.0 - max(0.0, min(1.0, (mouse_y - tr_rect.y) / tr_rect.height))
|
||||
val = self.min_val + frac * (self.max_val - self.min_val)
|
||||
snapped = round((val - self.min_val) / step) * step + self.min_val
|
||||
snapped = max(self.min_val, min(self.max_val, snapped))
|
||||
if snapped != self.current_val:
|
||||
self.current_val = snapped
|
||||
self.on_change(self.current_val)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self.set_rect(rect)
|
||||
dt = rl.get_frame_time()
|
||||
self._smooth_value += (self.current_val - self._smooth_value) * (1 - math.exp(-dt / 0.060))
|
||||
|
||||
# Layout: title (20px) + gap(6) + track + gap(6) + value (20px)
|
||||
title_h = 20
|
||||
value_h = 20
|
||||
gap = 6
|
||||
track_top = rect.y + title_h + gap
|
||||
track_h = rect.height - title_h - gap - value_h - gap
|
||||
track_w = max(self.MIN_TRACK_WIDTH, min(rect.width * 0.7, rect.width - 16))
|
||||
track_x = rect.x + (rect.width - track_w) / 2
|
||||
self._track_rect = rl.Rectangle(track_x, track_top, track_w, track_h)
|
||||
|
||||
# Title (centered above track)
|
||||
ts = measure_text_cached(self._font, self.title, 18)
|
||||
tx = rect.x + (rect.width - ts.x) / 2
|
||||
rl.draw_text_ex(self._font, self.title, rl.Vector2(round(tx), round(rect.y)), 18, 0, AetherListColors.SUBTEXT)
|
||||
|
||||
# Track background
|
||||
rl.draw_rectangle_rounded(self._track_rect, 0.3, 10, rl.Color(40, 42, 50, 255))
|
||||
|
||||
# Fill (from bottom up)
|
||||
frac = max(0.0, min(1.0, (self._smooth_value - self.min_val) / (self.max_val - self.min_val)))
|
||||
fill_h = frac * track_h
|
||||
if fill_h > 1:
|
||||
fill_rect = rl.Rectangle(track_x, track_top + track_h - fill_h, track_w, fill_h)
|
||||
rl.draw_rectangle_rounded(fill_rect, 0.3, 10, self.color)
|
||||
if fill_h > 8:
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(fill_rect.x + 1, fill_rect.y + 1, fill_rect.width - 2, fill_rect.height - 2), 0.3, 10, rl.Color(255, 255, 255, 25))
|
||||
|
||||
# Grab indicator at fill edge
|
||||
if 2 < fill_h < track_h - 2:
|
||||
edge_y = track_top + track_h - fill_h
|
||||
rl.draw_rectangle_rec(rl.Rectangle(track_x + 6, edge_y - 1, track_w - 12, 3), rl.Color(255, 255, 255, 100))
|
||||
|
||||
# Active glow when dragging
|
||||
if self._is_dragging:
|
||||
rl.draw_rectangle_rounded_lines_ex(self._track_rect, 0.3, 10, 2, self.color)
|
||||
|
||||
# Value label (centered below track)
|
||||
val_str = self.labels.get(self.current_val, f"{self.current_val:.1f}{self.unit}" if isinstance(self.current_val, float) and self.base_step < 1 else f"{int(self.current_val)}{self.unit}")
|
||||
vs = measure_text_cached(self._font, val_str, 18)
|
||||
vx = rect.x + (rect.width - vs.x) / 2
|
||||
vy = track_top + track_h + gap
|
||||
rl.draw_text_ex(self._font, val_str, rl.Vector2(round(vx), round(vy)), 18, 0, rl.WHITE)
|
||||
|
||||
|
||||
@@ -4,17 +4,14 @@ import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from dataclasses import replace
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MouseEvent, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2
|
||||
from openpilot.system.ui.widgets import DialogResult, Widget
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog
|
||||
from openpilot.system.ui.widgets.keyboard import Keyboard
|
||||
@@ -26,11 +23,9 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPan
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import (
|
||||
AETHER_LIST_METRICS,
|
||||
AetherListColors,
|
||||
AetherScrollbar,
|
||||
AetherContinuousSlider,
|
||||
AetherVerticalSlider,
|
||||
build_list_panel_frame,
|
||||
draw_list_panel_shell,
|
||||
draw_list_scroll_fades,
|
||||
draw_toggle_pill,
|
||||
)
|
||||
|
||||
@@ -67,40 +62,42 @@ REPORT_CATEGORIES = [
|
||||
"Something else (please describe)",
|
||||
]
|
||||
|
||||
|
||||
class SystemSettingsManagerView(Widget):
|
||||
"""Single-view, zero-scroll, fully-flat system settings dashboard.
|
||||
Left column: always-visible slider bands + toggles.
|
||||
Right column: action groups filling full height."""
|
||||
|
||||
ROW_PAD = 4
|
||||
BAND_GAP = 8 # gap between slider bands and between band zone and toggles
|
||||
|
||||
def __init__(self, controller: "StarPilotSystemLayout"):
|
||||
super().__init__()
|
||||
self._controller = controller
|
||||
self._scroll_panel = GuiScrollPanel2(horizontal=False)
|
||||
self._scrollbar = AetherScrollbar()
|
||||
self._content_height = 0.0
|
||||
self._scroll_offset = 0.0
|
||||
self._pressed_target: str | None = None
|
||||
self._can_click = True
|
||||
|
||||
self._action_rects: dict[str, rl.Rectangle] = {}
|
||||
self._toggle_rects: dict[str, rl.Rectangle] = {}
|
||||
self._shell_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self._scroll_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
|
||||
shutdown_labels = {0: tr("5 mins")}
|
||||
for i in range(1, 4): shutdown_labels[i] = f"{i * 15} mins"
|
||||
for i in range(4, 34): shutdown_labels[i] = f"{i - 3} " + (tr("hour") if i == 4 else tr("hours"))
|
||||
|
||||
brightness_labels = {101: tr("Auto"), 0: tr("Off")}
|
||||
|
||||
self._sliders = {
|
||||
"ScreenBrightness": AetherContinuousSlider(0, 101, 1, self._controller._params.get_int("ScreenBrightness"), lambda v: self._controller._set_brightness("ScreenBrightness", v), title=tr("Brightness (Offroad)"), unit="%", labels=brightness_labels, color=AetherListColors.PRIMARY),
|
||||
"ScreenBrightnessOnroad": AetherContinuousSlider(1, 101, 1, max(1, self._controller._params.get_int("ScreenBrightnessOnroad")), lambda v: self._controller._set_brightness("ScreenBrightnessOnroad", max(1, int(v))), title=tr("Brightness (Onroad)"), unit="%", labels=brightness_labels, color=AetherListColors.PRIMARY),
|
||||
"ScreenTimeout": AetherContinuousSlider(5, 60, 5, self._controller._params.get_int("ScreenTimeout"), lambda v: self._controller._params.put_int("ScreenTimeout", int(v)), title=tr("Timeout (Offroad)"), unit="s", color=AetherListColors.PRIMARY),
|
||||
"ScreenTimeoutOnroad": AetherContinuousSlider(5, 60, 5, self._controller._params.get_int("ScreenTimeoutOnroad"), lambda v: self._controller._params.put_int("ScreenTimeoutOnroad", int(v)), title=tr("Timeout (Onroad)"), unit="s", color=AetherListColors.PRIMARY),
|
||||
"DeviceShutdown": AetherContinuousSlider(0, 33, 1, self._controller._params.get_int("DeviceShutdown"), lambda v: self._controller._params.put_int("DeviceShutdown", int(v)), title=tr("Device Shutdown"), labels=shutdown_labels, color=AetherListColors.PRIMARY),
|
||||
"LowVoltageShutdown": AetherContinuousSlider(11.8, 12.5, 0.1, self._controller._params.get_float("LowVoltageShutdown"), lambda v: self._controller._params.put_float("LowVoltageShutdown", float(v)), title=tr("Low-Voltage Cutoff"), unit="V", color=AetherListColors.PRIMARY),
|
||||
self._vsliders: dict[str, AetherVerticalSlider] = {
|
||||
"ScreenBrightness": AetherVerticalSlider(0, 101, 1, self._controller._params.get_int("ScreenBrightness"), lambda v: self._controller._set_brightness("ScreenBrightness", v), title="Offroad", unit="%", labels=brightness_labels, color=AetherListColors.PRIMARY),
|
||||
"ScreenBrightnessOnroad": AetherVerticalSlider(1, 101, 1, max(1, self._controller._params.get_int("ScreenBrightnessOnroad")), lambda v: self._controller._set_brightness("ScreenBrightnessOnroad", max(1, int(v))), title="Onroad", unit="%", labels=brightness_labels, color=AetherListColors.PRIMARY),
|
||||
"ScreenTimeout": AetherVerticalSlider(5, 60, 5, self._controller._params.get_int("ScreenTimeout"), lambda v: self._controller._params.put_int("ScreenTimeout", int(v)), title="Off Timer", unit="s", color=AetherListColors.PRIMARY),
|
||||
"ScreenTimeoutOnroad": AetherVerticalSlider(5, 60, 5, self._controller._params.get_int("ScreenTimeoutOnroad"), lambda v: self._controller._params.put_int("ScreenTimeoutOnroad", int(v)), title="On Timer", unit="s", color=AetherListColors.PRIMARY),
|
||||
"DeviceShutdown": AetherVerticalSlider(0, 33, 1, self._controller._params.get_int("DeviceShutdown"), lambda v: self._controller._params.put_int("DeviceShutdown", int(v)), title="Shutdown", labels=shutdown_labels, color=AetherListColors.PRIMARY),
|
||||
"LowVoltageShutdown": AetherVerticalSlider(11.8, 12.5, 0.1, self._controller._params.get_float("LowVoltageShutdown"), lambda v: self._controller._params.put_float("LowVoltageShutdown", float(v)), title="Low Volt", unit="V", color=AetherListColors.PRIMARY),
|
||||
}
|
||||
|
||||
# Slider bands: (label, slider_keys)
|
||||
self._display_band = ["ScreenBrightness", "ScreenBrightnessOnroad", "ScreenTimeout", "ScreenTimeoutOnroad"]
|
||||
self._power_band = ["DeviceShutdown", "LowVoltageShutdown"]
|
||||
|
||||
def _clear_ephemeral_state(self):
|
||||
self._pressed_target = None
|
||||
self._can_click = True
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
@@ -112,190 +109,196 @@ class SystemSettingsManagerView(Widget):
|
||||
|
||||
def _handle_mouse_press(self, mouse_pos: MousePos):
|
||||
self._pressed_target = None
|
||||
self._can_click = True
|
||||
|
||||
for action_id, rect in self._action_rects.items():
|
||||
visible_rect = rl.get_collision_rec(rect, self._scroll_rect)
|
||||
if visible_rect.width > 0 and visible_rect.height > 0 and rl.check_collision_point_rec(mouse_pos, visible_rect):
|
||||
if rl.check_collision_point_rec(mouse_pos, rect):
|
||||
self._pressed_target = f"action:{action_id}"
|
||||
return
|
||||
|
||||
for toggle_id, rect in self._toggle_rects.items():
|
||||
visible_rect = rl.get_collision_rec(rect, self._scroll_rect)
|
||||
if visible_rect.width > 0 and visible_rect.height > 0 and rl.check_collision_point_rec(mouse_pos, visible_rect):
|
||||
if rl.check_collision_point_rec(mouse_pos, rect):
|
||||
self._pressed_target = f"toggle:{toggle_id}"
|
||||
return
|
||||
|
||||
for slider in self._sliders.values():
|
||||
for slider in self._vsliders.values():
|
||||
slider._handle_mouse_press(mouse_pos)
|
||||
|
||||
def _handle_mouse_event(self, mouse_event: MouseEvent):
|
||||
if not self._scroll_panel.is_touch_valid():
|
||||
self._can_click = False
|
||||
for slider in self._sliders.values():
|
||||
for slider in self._vsliders.values():
|
||||
slider._handle_mouse_event(mouse_event)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
target = self._pressed_target
|
||||
self._pressed_target = None
|
||||
|
||||
if target and self._can_click:
|
||||
if target.startswith("action:"):
|
||||
action_id = target.split(":", 1)[1]
|
||||
rect = self._action_rects.get(action_id)
|
||||
elif target.startswith("toggle:"):
|
||||
toggle_id = target.split(":", 1)[1]
|
||||
rect = self._toggle_rects.get(toggle_id)
|
||||
|
||||
if rect:
|
||||
visible_rect = rl.get_collision_rec(rect, self._scroll_rect)
|
||||
if visible_rect.width > 0 and visible_rect.height > 0 and rl.check_collision_point_rec(mouse_pos, visible_rect):
|
||||
self._activate_target(target)
|
||||
|
||||
for slider in self._sliders.values():
|
||||
if target:
|
||||
prefix, action_id = target.split(":", 1)
|
||||
rect = self._action_rects.get(action_id) if prefix == "action" else self._toggle_rects.get(action_id)
|
||||
if rect and rl.check_collision_point_rec(mouse_pos, rect):
|
||||
self._controller.handle_action(action_id)
|
||||
for slider in self._vsliders.values():
|
||||
slider._handle_mouse_release(mouse_pos)
|
||||
|
||||
def _activate_target(self, target: str):
|
||||
action_id = target.split(":", 1)[1]
|
||||
self._controller.handle_action(action_id)
|
||||
# --- Main render ---
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self.set_rect(rect)
|
||||
self._action_rects.clear()
|
||||
self._toggle_rects.clear()
|
||||
|
||||
metrics = replace(AETHER_LIST_METRICS, header_height=110)
|
||||
frame = build_list_panel_frame(rect, metrics)
|
||||
self._shell_rect = frame.shell
|
||||
frame = build_list_panel_frame(rect)
|
||||
draw_list_panel_shell(frame)
|
||||
|
||||
header_rect = frame.header
|
||||
self._draw_header(header_rect)
|
||||
hdr = frame.header
|
||||
gui_label(rl.Rectangle(hdr.x, hdr.y + 4, hdr.width * 0.55, 40), tr("System Settings"), 40, AetherListColors.HEADER, FontWeight.SEMI_BOLD)
|
||||
gui_label(rl.Rectangle(hdr.x, hdr.y + 48, hdr.width * 0.7, 36), tr("Manage device behavior, power, and storage."), 24, AetherListColors.SUBTEXT, FontWeight.NORMAL)
|
||||
|
||||
scroll_rect = frame.scroll
|
||||
self._scroll_rect = scroll_rect
|
||||
actual_header_h = 100
|
||||
content_y = hdr.y + actual_header_h
|
||||
content_h = (frame.shell.y + frame.shell.height) - content_y - AETHER_LIST_METRICS.panel_padding_bottom
|
||||
content_w = frame.scroll.width
|
||||
section_gap = AETHER_LIST_METRICS.section_gap
|
||||
|
||||
content_width = scroll_rect.width - AETHER_LIST_METRICS.content_right_gutter
|
||||
self._content_height = self._measure_content_height()
|
||||
self._scroll_offset = self._scroll_panel.update(scroll_rect, max(self._content_height, scroll_rect.height))
|
||||
col_w = (content_w - section_gap) / 2
|
||||
left_col = rl.Rectangle(frame.scroll.x, content_y, col_w, content_h)
|
||||
right_col = rl.Rectangle(frame.scroll.x + col_w + section_gap, content_y, col_w, content_h)
|
||||
|
||||
rl.begin_scissor_mode(int(scroll_rect.x), int(scroll_rect.y), int(scroll_rect.width), int(scroll_rect.height))
|
||||
self._draw_scroll_content(scroll_rect, content_width)
|
||||
rl.end_scissor_mode()
|
||||
self._sync_slider_values()
|
||||
self._draw_left_column(left_col)
|
||||
self._draw_actions_column(right_col)
|
||||
|
||||
if self._content_height > scroll_rect.height:
|
||||
self._draw_scrollbar(scroll_rect)
|
||||
def _sync_slider_values(self):
|
||||
params = self._controller._params
|
||||
for key, slider in self._vsliders.items():
|
||||
if slider._is_dragging:
|
||||
continue
|
||||
pval = float(params.get_float(key)) if key == "LowVoltageShutdown" else float(params.get_int(key))
|
||||
if slider.current_val != pval:
|
||||
slider.current_val = pval
|
||||
|
||||
draw_list_scroll_fades(scroll_rect, self._content_height, self._scroll_offset, AetherListColors.PANEL_BG, fade_height=AETHER_LIST_METRICS.fade_height)
|
||||
# --- Left column: slider bands + toggles ---
|
||||
|
||||
def _draw_header(self, rect: rl.Rectangle):
|
||||
title_rect = rl.Rectangle(rect.x, rect.y + 4, rect.width * 0.55, 40)
|
||||
gui_label(title_rect, tr("System Settings"), 40, AetherListColors.HEADER, FontWeight.SEMI_BOLD)
|
||||
subtitle_rect = rl.Rectangle(rect.x, rect.y + 48, rect.width * 0.58, 36)
|
||||
gui_label(subtitle_rect, tr("Manage device behavior, power, and storage seamlessly."), 24, AetherListColors.SUBTEXT, FontWeight.NORMAL)
|
||||
def _draw_left_column(self, rect: rl.Rectangle):
|
||||
params = self._controller._params
|
||||
no_uploads = params.get_bool("NoUploads")
|
||||
disable_onroad = params.get_bool("DisableOnroadUploads")
|
||||
|
||||
def _measure_column_height(self, sections: list[dict]) -> float:
|
||||
total_height = 0
|
||||
for section in sections:
|
||||
total_height += AETHER_LIST_METRICS.section_header_height + AETHER_LIST_METRICS.section_header_gap
|
||||
for row in section["rows"]:
|
||||
if row["type"] == "slider":
|
||||
total_height += 100 + 16
|
||||
elif row["type"] in ["toggle", "toggle_row"]:
|
||||
total_height += 90 + 16
|
||||
elif row["type"] == "action_group":
|
||||
total_height += 110 + 16
|
||||
total_height += AETHER_LIST_METRICS.section_gap
|
||||
return max(total_height - AETHER_LIST_METRICS.section_gap, 0.0)
|
||||
# Slider bands take fixed proportion of height (40%), toggles take rest (60%)
|
||||
band_zone_h = rect.height * 0.40
|
||||
toggle_zone_h = rect.height - band_zone_h - self.BAND_GAP
|
||||
|
||||
def _measure_content_height(self) -> float:
|
||||
cols = self._controller.utility_columns()
|
||||
return max(self._measure_column_height(cols["left"]), self._measure_column_height(cols["right"]), 0.0)
|
||||
# Two slider bands split the band zone: display (4 sliders) gets 55%, power (2 sliders) gets 45%
|
||||
display_h = band_zone_h * 0.55
|
||||
power_h = band_zone_h * 0.45 - self.BAND_GAP
|
||||
|
||||
def _draw_scroll_content(self, rect: rl.Rectangle, width: float):
|
||||
cols = self._controller.utility_columns()
|
||||
col_w = (width - AETHER_LIST_METRICS.section_gap) / 2
|
||||
left_x = rect.x
|
||||
right_x = rect.x + col_w + AETHER_LIST_METRICS.section_gap
|
||||
|
||||
self._draw_column(rl.Rectangle(left_x, rect.y + self._scroll_offset, col_w, rect.height), cols["left"])
|
||||
self._draw_column(rl.Rectangle(right_x, rect.y + self._scroll_offset, col_w, rect.height), cols["right"])
|
||||
self._draw_slider_band(rl.Rectangle(rect.x, rect.y, rect.width, display_h), tr("Display"), self._display_band)
|
||||
self._draw_slider_band(rl.Rectangle(rect.x, rect.y + display_h + self.BAND_GAP, rect.width, power_h), tr("Power"), self._power_band)
|
||||
|
||||
def _draw_column(self, rect: rl.Rectangle, sections: list[dict]):
|
||||
y = rect.y
|
||||
# Toggles below
|
||||
toggle_y = rect.y + band_zone_h + self.BAND_GAP
|
||||
toggles = [
|
||||
("StandbyMode", tr("Standby Mode"), params.get_bool("StandbyMode"), True),
|
||||
("IncreaseThermalLimits", tr("Raise Thermal Limits"), params.get_bool("IncreaseThermalLimits"), True),
|
||||
("NoUploads", tr("Disable Uploads"), no_uploads, True),
|
||||
("UseKonikServer", tr("Use Konik Server"), self._controller._get_konik_state(), True),
|
||||
("DisableOnroadUploads", tr("No Onroad Uploads"), disable_onroad, not no_uploads),
|
||||
("NoLogging", tr("Disable Logging"), params.get_bool("NoLogging"), True),
|
||||
("HigherBitrate", tr("HD Recording"), params.get_bool("HigherBitrate"), not disable_onroad and not no_uploads),
|
||||
("DebugMode", tr("Debug Mode"), params.get_bool("DebugMode"), True),
|
||||
]
|
||||
toggle_row_h = toggle_zone_h / len(toggles)
|
||||
for i, (tid, title, val, enabled) in enumerate(toggles):
|
||||
slot_y = toggle_y + i * toggle_row_h
|
||||
inner = rl.Rectangle(rect.x, slot_y + self.ROW_PAD, rect.width, toggle_row_h - 2 * self.ROW_PAD)
|
||||
self._render_toggle_pill(inner, tid, title, val, enabled)
|
||||
|
||||
def _draw_slider_band(self, rect, label, slider_keys):
|
||||
"""Draws a labeled card containing vertical sliders arranged horizontally."""
|
||||
# Card background
|
||||
rl.draw_rectangle_rounded(rect, 0.15, 16, rl.Color(28, 30, 36, 255))
|
||||
rl.draw_rectangle_rounded_lines_ex(rect, 0.15, 16, 1, rl.Color(255, 255, 255, 15))
|
||||
|
||||
# Section label at top-left
|
||||
label_h = 18
|
||||
gui_label(rl.Rectangle(rect.x + 12, rect.y + 4, rect.width * 0.3, label_h), label, 16, AetherListColors.MUTED, FontWeight.MEDIUM)
|
||||
|
||||
# Slider zone
|
||||
slider_top = rect.y + label_h + 6
|
||||
slider_h = rect.height - label_h - 10
|
||||
slider_area_w = rect.width - 16
|
||||
n = len(slider_keys)
|
||||
gap = 8
|
||||
col_w = (slider_area_w - (n - 1) * gap) / n
|
||||
start_x = rect.x + 8
|
||||
|
||||
for i, key in enumerate(slider_keys):
|
||||
col_x = start_x + i * (col_w + gap)
|
||||
self._vsliders[key].render(rl.Rectangle(col_x, slider_top, col_w, slider_h))
|
||||
|
||||
# --- Right column: action groups ---
|
||||
|
||||
def _draw_actions_column(self, rect: rl.Rectangle):
|
||||
state = self._controller._get_force_drive_state()
|
||||
groups = [
|
||||
(tr("Storage & Logs"), [
|
||||
{"id": "Storage", "label": tr("Clear Data"), "danger": True},
|
||||
{"id": "ErrorLogs", "label": tr("Clear Logs"), "danger": True}]),
|
||||
(tr("System Backups"), [
|
||||
{"id": "CreateBackup", "label": tr("Create")},
|
||||
{"id": "RestoreBackup", "label": tr("Restore")},
|
||||
{"id": "DeleteBackup", "label": tr("Delete"), "danger": True}]),
|
||||
(tr("Toggle Backups"), [
|
||||
{"id": "CreateToggleBackup", "label": tr("Create")},
|
||||
{"id": "RestoreToggleBackup", "label": tr("Restore")},
|
||||
{"id": "DeleteToggleBackup", "label": tr("Delete"), "danger": True}]),
|
||||
(tr("Drive Mode"), [
|
||||
{"id": "DriveDefault", "label": tr("Auto"), "active": state == tr("Default")},
|
||||
{"id": "DriveOnroad", "label": tr("Onroad"), "active": state == tr("Onroad")},
|
||||
{"id": "DriveOffroad", "label": tr("Offroad"), "active": state == tr("Offroad")}]),
|
||||
(tr("Quick Actions"), [
|
||||
{"id": "FlashPanda", "label": tr("Flash Panda")},
|
||||
{"id": "ReportIssue", "label": tr("Report Issue")}]),
|
||||
(tr("Reset"), [
|
||||
{"id": "ResetDefaults", "label": tr("Reset Toggles"), "danger": True},
|
||||
{"id": "ResetStock", "label": tr("Stock OP"), "danger": True}]),
|
||||
]
|
||||
row_h = rect.height / len(groups)
|
||||
for i, (title, actions) in enumerate(groups):
|
||||
slot_y = rect.y + i * row_h
|
||||
inner = rl.Rectangle(rect.x, slot_y + self.ROW_PAD, rect.width, row_h - 2 * self.ROW_PAD)
|
||||
self._render_action_group(inner, title, actions)
|
||||
|
||||
# --- Primitives ---
|
||||
|
||||
def _render_toggle_pill(self, rect, toggle_id, title, value, enabled=True):
|
||||
if enabled:
|
||||
self._toggle_rects[toggle_id] = rect
|
||||
mouse_pos = gui_app.last_mouse_event.pos
|
||||
|
||||
for section in sections:
|
||||
title_rect = rl.Rectangle(rect.x, y, rect.width, AETHER_LIST_METRICS.section_header_height)
|
||||
gui_label(title_rect, section["title"], 26, AetherListColors.SUBTEXT, FontWeight.MEDIUM)
|
||||
y += AETHER_LIST_METRICS.section_header_height + AETHER_LIST_METRICS.section_header_gap
|
||||
|
||||
for row in section["rows"]:
|
||||
if row["type"] == "slider":
|
||||
slider = self._sliders[row["id"]]
|
||||
slider.render(rl.Rectangle(rect.x, y, rect.width, 100))
|
||||
y += 100 + 16
|
||||
elif row["type"] == "toggle_row":
|
||||
items = row["items"]
|
||||
item_w = (rect.width - 16 * (len(items) - 1)) / len(items)
|
||||
for i, item in enumerate(items):
|
||||
enabled = item.get("enabled", True)
|
||||
toggle_rect = rl.Rectangle(rect.x + i * (item_w + 16), y, item_w, 90)
|
||||
if enabled:
|
||||
self._toggle_rects[item["id"]] = toggle_rect
|
||||
hovered = rl.check_collision_point_rec(mouse_pos, toggle_rect)
|
||||
pressed = self._pressed_target == f"toggle:{item['id']}"
|
||||
draw_toggle_pill(toggle_rect, item["value"], enabled, item["title"], tr("ON") if item["value"] else tr("OFF"), hovered, pressed)
|
||||
y += 90 + 16
|
||||
elif row["type"] == "toggle":
|
||||
enabled = row.get("enabled", True)
|
||||
toggle_rect = rl.Rectangle(rect.x, y, rect.width, 90)
|
||||
if enabled:
|
||||
self._toggle_rects[row["id"]] = toggle_rect
|
||||
hovered = rl.check_collision_point_rec(mouse_pos, toggle_rect)
|
||||
pressed = self._pressed_target == f"toggle:{row['id']}"
|
||||
draw_toggle_pill(toggle_rect, row["value"], enabled, row["title"], tr("ON") if row["value"] else tr("OFF"), hovered, pressed)
|
||||
y += 90 + 16
|
||||
elif row["type"] == "action_group":
|
||||
group_rect = rl.Rectangle(rect.x, y, rect.width, 110)
|
||||
self._draw_action_group(group_rect, row, mouse_pos)
|
||||
y += 110 + 16
|
||||
|
||||
y += AETHER_LIST_METRICS.section_gap
|
||||
hovered = rl.check_collision_point_rec(mouse_pos, rect)
|
||||
pressed = self._pressed_target == f"toggle:{toggle_id}"
|
||||
draw_toggle_pill(rect, value, enabled, title, tr("ON") if value else tr("OFF"), hovered, pressed)
|
||||
|
||||
def _draw_action_group(self, rect: rl.Rectangle, row: dict, mouse_pos: MousePos):
|
||||
def _render_action_group(self, rect, title, actions):
|
||||
rl.draw_rectangle_rounded(rect, 0.3, 16, rl.Color(35, 35, 40, 255))
|
||||
|
||||
btn_h = min(52, rect.height - 20)
|
||||
btn_y = rect.y + (rect.height - btn_h) / 2
|
||||
title_y = rect.y + (rect.height - 24) / 2
|
||||
gui_label(rl.Rectangle(rect.x + 24, title_y, rect.width * 0.4, 24), row["title"], 24, rl.WHITE, FontWeight.BOLD)
|
||||
|
||||
actions = row["actions"]
|
||||
btn_gap = 12
|
||||
available_w = rect.width * 0.6 - 40
|
||||
title_w = rect.width * 0.30
|
||||
gui_label(rl.Rectangle(rect.x + 20, title_y, title_w, 24), title, 22, rl.WHITE, FontWeight.BOLD)
|
||||
btn_gap = 10
|
||||
available_w = rect.width - title_w - 48
|
||||
btn_w = (available_w - (len(actions) - 1) * btn_gap) / len(actions)
|
||||
start_x = rect.x + rect.width - available_w - 16
|
||||
|
||||
mouse_pos = gui_app.last_mouse_event.pos
|
||||
for i, action in enumerate(actions):
|
||||
btn_rect = rl.Rectangle(start_x + i * (btn_w + btn_gap), rect.y + 12, btn_w, rect.height - 24)
|
||||
btn_rect = rl.Rectangle(start_x + i * (btn_w + btn_gap), btn_y, btn_w, btn_h)
|
||||
self._action_rects[action["id"]] = btn_rect
|
||||
|
||||
hovered = rl.check_collision_point_rec(mouse_pos, btn_rect)
|
||||
pressed = self._pressed_target == f"action:{action['id']}"
|
||||
|
||||
active = action.get("active", False)
|
||||
color = AetherListColors.PRIMARY if active else rl.Color(60, 60, 65, 255)
|
||||
if action.get("danger"):
|
||||
color = AetherListColors.DANGER
|
||||
|
||||
if hovered: color = rl.Color(min(color.r + 20, 255), min(color.g + 20, 255), min(color.b + 20, 255), 255)
|
||||
if pressed: color = rl.Color(max(color.r - 20, 0), max(color.g - 20, 0), max(color.b - 20, 0), 255)
|
||||
|
||||
rl.draw_rectangle_rounded(btn_rect, 0.4, 16, color)
|
||||
gui_label(btn_rect, action["label"], 24, rl.WHITE, FontWeight.BOLD, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
|
||||
def _draw_scrollbar(self, rect: rl.Rectangle):
|
||||
self._scrollbar.render(rect, self._content_height, self._scroll_offset)
|
||||
|
||||
gui_label(btn_rect, action["label"], 22, rl.WHITE, FontWeight.BOLD, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
|
||||
class StarPilotSystemLayout(StarPilotPanel):
|
||||
def __init__(self):
|
||||
@@ -314,92 +317,7 @@ class StarPilotSystemLayout(StarPilotPanel):
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self._manager_view.render(rect)
|
||||
|
||||
def utility_columns(self) -> dict[str, list[dict]]:
|
||||
state = self._get_force_drive_state()
|
||||
no_uploads = self._params.get_bool("NoUploads")
|
||||
disable_onroad = self._params.get_bool("DisableOnroadUploads")
|
||||
|
||||
screen_management = self._params.get_bool("ScreenManagement")
|
||||
screen_rows = [
|
||||
{"id": "ScreenManagement", "type": "toggle", "title": tr("Screen Management"), "value": screen_management},
|
||||
{"id": "StandbyMode", "type": "toggle", "title": tr("Standby Mode"), "value": self._params.get_bool("StandbyMode"), "enabled": screen_management},
|
||||
]
|
||||
if screen_management:
|
||||
screen_rows.extend([
|
||||
{"id": "ScreenBrightness", "type": "slider"},
|
||||
{"id": "ScreenBrightnessOnroad", "type": "slider"},
|
||||
{"id": "ScreenTimeout", "type": "slider"},
|
||||
{"id": "ScreenTimeoutOnroad", "type": "slider"},
|
||||
])
|
||||
|
||||
device_management = self._params.get_bool("DeviceManagement")
|
||||
device_rows = [
|
||||
{"id": "DeviceManagement", "type": "toggle", "title": tr("Device Management"), "value": device_management},
|
||||
{"id": "IncreaseThermalLimits", "type": "toggle", "title": tr("Raise Thermal Limits"), "value": self._params.get_bool("IncreaseThermalLimits"), "enabled": device_management},
|
||||
]
|
||||
if device_management:
|
||||
device_rows.extend([
|
||||
{"id": "DeviceShutdown", "type": "slider"},
|
||||
{"id": "LowVoltageShutdown", "type": "slider"},
|
||||
])
|
||||
|
||||
network_rows = [
|
||||
{"type": "toggle_row", "items": [
|
||||
{"id": "NoUploads", "title": tr("Disable Uploads"), "value": no_uploads},
|
||||
{"id": "UseKonikServer", "title": tr("Use Konik Server"), "value": self._get_konik_state()}
|
||||
]},
|
||||
{"type": "toggle_row", "items": [
|
||||
{"id": "DisableOnroadUploads", "title": tr("Disable Onroad Uploads"), "value": disable_onroad, "enabled": not no_uploads},
|
||||
{"id": "NoLogging", "title": tr("Disable Logging"), "value": self._params.get_bool("NoLogging")}
|
||||
]},
|
||||
{"id": "HigherBitrate", "type": "toggle", "title": tr("High-Quality Recording"), "value": self._params.get_bool("HigherBitrate"), "enabled": not disable_onroad and not no_uploads}
|
||||
]
|
||||
|
||||
data_rows = [
|
||||
{"id": "Storage", "type": "action_group", "title": tr("Storage & Logs"), "actions": [
|
||||
{"id": "Storage", "label": f"{tr('Clear Data')} ({self._get_storage()})", "danger": True},
|
||||
{"id": "ErrorLogs", "label": tr("Clear Logs"), "danger": True}
|
||||
]},
|
||||
{"id": "SystemBackups", "type": "action_group", "title": tr("System Backups"), "actions": [
|
||||
{"id": "CreateBackup", "label": tr("Create")},
|
||||
{"id": "RestoreBackup", "label": tr("Restore")},
|
||||
{"id": "DeleteBackup", "label": tr("Delete"), "danger": True}
|
||||
]},
|
||||
{"id": "ToggleBackups", "type": "action_group", "title": tr("Toggle Backups"), "actions": [
|
||||
{"id": "CreateToggleBackup", "label": tr("Create")},
|
||||
{"id": "RestoreToggleBackup", "label": tr("Restore")},
|
||||
{"id": "DeleteToggleBackup", "label": tr("Delete"), "danger": True}
|
||||
]},
|
||||
]
|
||||
|
||||
util_rows = [
|
||||
{"type": "toggle_row", "items": [{"id": "DebugMode", "type": "toggle", "title": tr("Debug Mode"), "value": self._params.get_bool("DebugMode")}]},
|
||||
{"id": "ForceDriveState", "type": "action_group", "title": tr("Force Drive State"), "actions": [
|
||||
{"id": "DriveDefault", "label": tr("Auto"), "active": state == tr("Default")},
|
||||
{"id": "DriveOnroad", "label": tr("Onroad"), "active": state == tr("Onroad")},
|
||||
{"id": "DriveOffroad", "label": tr("Offroad"), "active": state == tr("Offroad")}
|
||||
]},
|
||||
{"id": "QuickActions", "type": "action_group", "title": tr("Quick Actions"), "actions": [
|
||||
{"id": "FlashPanda", "label": tr("Flash Panda")},
|
||||
{"id": "ReportIssue", "label": tr("Report Issue")}
|
||||
]},
|
||||
{"id": "FactoryReset", "type": "action_group", "title": tr("Factory Reset"), "actions": [
|
||||
{"id": "ResetDefaults", "label": tr("Toggles"), "danger": True},
|
||||
{"id": "ResetStock", "label": tr("Stock OP"), "danger": True}
|
||||
]},
|
||||
]
|
||||
|
||||
return {
|
||||
"left": [
|
||||
{"title": tr("Display Configuration"), "rows": screen_rows},
|
||||
{"title": tr("Developer & Maintenance"), "rows": util_rows},
|
||||
],
|
||||
"right": [
|
||||
{"title": tr("Power & Thermals"), "rows": device_rows},
|
||||
{"title": tr("Networking & Data"), "rows": network_rows},
|
||||
{"title": tr("Data & Backups"), "rows": data_rows},
|
||||
]
|
||||
}
|
||||
|
||||
def handle_action(self, action_id: str):
|
||||
if action_id == "ScreenManagement":
|
||||
@@ -458,7 +376,7 @@ class StarPilotSystemLayout(StarPilotPanel):
|
||||
|
||||
def _set_brightness(self, key, val):
|
||||
self._params.put_int(key, int(val))
|
||||
if key == "ScreenBrightnessOnroad" or key == "ScreenBrightness":
|
||||
if key in ("ScreenBrightnessOnroad", "ScreenBrightness") and hasattr(HARDWARE, 'set_brightness'):
|
||||
HARDWARE.set_brightness(int(val))
|
||||
|
||||
def _get_konik_state(self):
|
||||
|
||||
Reference in New Issue
Block a user