Raindrops on Roses & Whiskers on Kittens

This commit is contained in:
firestar5683
2026-06-14 23:50:41 -05:00
parent 0262b934c4
commit 3ecd8be1f1
22 changed files with 1332 additions and 33 deletions
+1
View File
@@ -316,6 +316,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"StarPilotCarParams", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BYTES, "", ""}},
{"StarPilotCarParamsPersistent", {PERSISTENT, BYTES, "", ""}},
{"StarPilotDongleId", {PERSISTENT | DONT_LOG, STRING, "", "", 0}},
{"StarPilotFavoriteSlots", {PERSISTENT, JSON, "[]", "[]", 1}},
{"StarPilotStats", {PERSISTENT | DONT_LOG, JSON, "{}", "{}"}},
{"StarPilotTogglesUpdated", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"FrogsGoMoosTweak", {PERSISTENT, BOOL, "1", "0", 2}},
@@ -52,6 +52,9 @@ ACTION_OPTIONS = [
{"id": 8, "name": tr_noop("Create Bookmark")},
{"id": 9, "name": tr_noop("Toggle Always On Lateral")},
{"id": 10, "name": tr_noop("Adopt Current Speed Limit")},
{"id": 11, "name": tr_noop("Favorite #1")},
{"id": 12, "name": tr_noop("Favorite #2")},
{"id": 13, "name": tr_noop("Favorite #3")},
]
ACTION_NAMES = [o["name"] for o in ACTION_OPTIONS]
ACTION_IDS = {o["name"]: o["id"] for o in ACTION_OPTIONS}
@@ -529,9 +532,9 @@ class StarPilotVehicleSettingsLayout(_SettingsPage):
def _get_available_actions(self, key: str | None = None) -> list[str]:
cs = starpilot_state.car_state
if key == "MainCruiseButtonControl":
allowed_ids = {0, 9, 10}
allowed_ids = {0, 9, 10, 11, 12, 13}
return [tr(o["name"]) for o in ACTION_OPTIONS if o["id"] in allowed_ids]
allowed_ids = set(range(9))
allowed_ids = set(range(9)) | {11, 12, 13}
if key == "LKASButtonControl":
allowed_ids.add(9)
return [tr(o["name"]) for o in ACTION_OPTIONS
+115 -1
View File
@@ -20,7 +20,10 @@ from openpilot.selfdrive.ui.mici.onroad.starpilot_status import (
)
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
from openpilot.selfdrive.ui.lib.starpilot_visuals import get_border_width
from openpilot.starpilot.common.favorite_slots import load_favorite_slots, toggle_favorite_slot
from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets import Widget
from openpilot.common.filter_simple import BounceFilter
@@ -147,6 +150,114 @@ class BookmarkIcon(Widget):
rl.draw_texture(self._icon, int(icon_x), int(icon_y), rl.WHITE)
class FavoriteSlotsOverlay(Widget):
EDGE_MARGIN = 8
BUTTON_GAP = 8
MAX_BUTTON_SIZE = 208
INDICATOR_SIZE = 18
def __init__(self):
super().__init__()
self._font = gui_app.font(FontWeight.SEMI_BOLD)
self._button_rects: list[tuple[int, rl.Rectangle]] = []
self._pressed_slot: int | None = None
self._interacting = False
def interacting(self):
interacting, self._interacting = self._interacting, False
return interacting
def _visible_slots(self) -> list[tuple[int, dict]]:
visible = []
for index, slot in enumerate(load_favorite_slots(ui_state.params)):
if slot.get("enabled") and slot.get("show_onroad") and slot.get("key"):
visible.append((index, slot))
return visible
def _slot_rects(self, rect: rl.Rectangle, slots: list[tuple[int, dict]]) -> list[tuple[int, rl.Rectangle]]:
if not slots:
return []
slot_count = len(slots)
available_width = rect.width - (2 * self.EDGE_MARGIN) - ((slot_count - 1) * self.BUTTON_GAP)
available_height = rect.height - (2 * self.EDGE_MARGIN)
button_size = min(self.MAX_BUTTON_SIZE, available_height, available_width / slot_count)
row_width = slot_count * button_size + (slot_count - 1) * self.BUTTON_GAP
x = rect.x + (rect.width - row_width) / 2
y = rect.y + (rect.height - button_size) / 2
return [
(slot_index, rl.Rectangle(x + draw_index * (button_size + self.BUTTON_GAP), y, button_size, button_size))
for draw_index, (slot_index, _slot) in enumerate(slots)
]
def _fit_label(self, label: str, max_width: float, max_height: float) -> tuple[list[str], int]:
label = label or "Favorite"
lines = [label]
for font_size in range(30, 17, -1):
if any(measure_text_cached(self._font, word, font_size).x > max_width for word in label.split()):
continue
lines = wrap_text(self._font, label, font_size, int(max_width)) or [label]
line_height = font_size * 1.12
if len(lines) <= 3 and len(lines) * line_height <= max_height:
return lines, font_size
return lines[:3], 18
def _render(self, rect: rl.Rectangle):
visible_slots = self._visible_slots()
self._button_rects = self._slot_rects(rect, visible_slots)
slot_by_index = dict(visible_slots)
for slot_index, button_rect in self._button_rects:
slot = slot_by_index[slot_index]
pressed = self._pressed_slot == slot_index
bg = rl.Color(0, 0, 0, 190 if pressed else 166)
border = rl.Color(255, 255, 255, 120 if pressed else 78)
rl.draw_rectangle_rounded(button_rect, 0.18, 12, bg)
rl.draw_rectangle_rounded_lines_ex(button_rect, 0.18, 12, 2, border)
key = slot.get("key")
active = ui_state.params.get_bool(key) if key else False
indicator = rl.Color(48, 255, 156, 255) if active else rl.Color(135, 135, 135, 255)
indicator_x = button_rect.x + button_rect.width - self.INDICATOR_SIZE - 12
indicator_y = button_rect.y + 12
rl.draw_circle(int(indicator_x + self.INDICATOR_SIZE / 2), int(indicator_y + self.INDICATOR_SIZE / 2), self.INDICATOR_SIZE / 2, indicator)
slot_text = f"#{slot_index + 1}"
rl.draw_text_ex(self._font, slot_text, rl.Vector2(button_rect.x + 12, button_rect.y + 9), 22, 0, rl.Color(255, 255, 255, 180))
lines, font_size = self._fit_label(
slot.get("label") or key or "Favorite",
button_rect.width - 24,
button_rect.height - 54,
)
line_height = font_size * 1.12
text_y = button_rect.y + 38 + (button_rect.height - 50 - len(lines) * line_height) / 2
for line in lines:
text_size = rl.measure_text_ex(self._font, line, font_size, 0)
text_x = button_rect.x + (button_rect.width - text_size.x) / 2
rl.draw_text_ex(self._font, line, rl.Vector2(text_x, text_y), font_size, 0, rl.Color(255, 255, 255, 242))
text_y += line_height
def _slot_at(self, pos: MousePos) -> int | None:
for slot_index, rect in self._button_rects:
if rl.check_collision_point_rec(pos, rect):
return slot_index
return None
def _handle_mouse_press(self, mouse_pos: MousePos):
self._pressed_slot = self._slot_at(mouse_pos)
if self._pressed_slot is not None:
self._interacting = True
def _handle_mouse_release(self, mouse_pos: MousePos):
released_slot = self._slot_at(mouse_pos)
if self._pressed_slot is not None and released_slot == self._pressed_slot:
toggle_favorite_slot(self._pressed_slot, ui_state.params, ui_state.params_memory)
self._interacting = True
self._pressed_slot = None
class MinSteerSpeedBanner(Widget):
"""One-shot-per-drive banner shown for the full first below-min-steer interval."""
@@ -374,6 +485,7 @@ class AugmentedRoadView(CameraView):
self._confidence_ball = ConfidenceBall()
self._min_steer_speed_banner = MinSteerSpeedBanner()
self._standstill_timer = StandstillTimerOverlay()
self._favorite_slots = self._child(FavoriteSlotsOverlay())
self._offroad_label = UnifiedLabel("start the car to\nuse openpilot", 54, FontWeight.DISPLAY,
text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
@@ -416,7 +528,7 @@ class AugmentedRoadView(CameraView):
def _handle_mouse_release(self, mouse_pos: MousePos):
# Don't trigger click callback if bookmark or HUD widgets consumed the tap.
if not self._bookmark_icon.interacting() and not self._hud_renderer.user_interacting():
if not self._bookmark_icon.interacting() and not self._hud_renderer.user_interacting() and not self._favorite_slots.interacting():
super()._handle_mouse_release(mouse_pos)
def _render(self, _):
@@ -512,6 +624,8 @@ class AugmentedRoadView(CameraView):
# Use self._content_rect for positioning within camera bounds
if draw_road_overlays:
self._confidence_ball.render(self.rect)
if draw_hud_controls and (camera_view_none or is_driver_stream or not in_reverse):
self._favorite_slots.render(self._content_rect)
if camera_view_none or is_driver_stream or not in_reverse:
self._draw_border()
+45 -6
View File
@@ -6,6 +6,7 @@
#include <cmath>
#include <exception>
#include <string>
#include <vector>
#include "common/params.h"
#include "common/swaglog.h"
@@ -28,6 +29,11 @@ AnnotatedCameraWidget::AnnotatedCameraWidget(VisionStreamType type, QWidget *par
personality_btn = new DrivingPersonalityButton(this);
personality_btn->setVisible(false);
for (int i = 0; i < static_cast<int>(favorite_btns.size()); ++i) {
favorite_btns[i] = new FavoriteButton(i, this);
favorite_btns[i]->setVisible(false);
}
screen_recorder = new ScreenRecorder(this);
screen_recorder->setVisible(false);
}
@@ -58,14 +64,47 @@ void AnnotatedCameraWidget::updateState(const UIState &s, const StarPilotUIState
: QPoint(experimental_btn->x(), experimental_btn->y());
starpilot_nvg->experimentalButtonPosition = experimental_button_position;
bool onroad_distance_btn_enabled = starpilot_nvg->dmIconPosition != QPoint(0, 0) && !starpilot_nvg->hideBottomIcons && starpilot_toggles.value("onroad_distance_button").toBool();
personality_btn->setVisible(onroad_distance_btn_enabled);
if (onroad_distance_btn_enabled) {
personality_btn->move(starpilot_nvg->rightHandDM ? width() - UI_BORDER_SIZE - personality_btn->width() - (UI_BORDER_SIZE / 2) : UI_BORDER_SIZE, starpilot_nvg->dmIconPosition.y() - personality_btn->height() / 2);
personality_btn->updateState(s, fs);
std::vector<FavoriteButton*> visible_favorite_btns;
const bool favorites_anchor_ready = starpilot_nvg->dmIconPosition != QPoint(0, 0) && !starpilot_nvg->hideBottomIcons;
for (FavoriteButton *favorite_btn : favorite_btns) {
favorite_btn->updateState();
if (favorites_anchor_ready && favorite_btn->shouldShow()) {
visible_favorite_btns.push_back(favorite_btn);
} else {
favorite_btn->setVisible(false);
}
}
dmon.onroad_distance_btn_enabled = onroad_distance_btn_enabled;
const bool onroad_distance_btn_enabled = favorites_anchor_ready && starpilot_toggles.value("onroad_distance_button").toBool();
const int gap = UI_BORDER_SIZE / 2;
int controls_width = 0;
if (!visible_favorite_btns.empty()) {
controls_width = visible_favorite_btns.size() * btn_size + (visible_favorite_btns.size() - 1) * gap;
}
if (onroad_distance_btn_enabled) {
controls_width += (controls_width > 0 ? gap : 0) + personality_btn->width();
}
dmon.onroad_controls_width = controls_width;
const int controls_y = favorites_anchor_ready
? std::clamp(starpilot_nvg->dmIconPosition.y() - (btn_size / 2), UI_BORDER_SIZE, height() - UI_BORDER_SIZE - btn_size)
: 0;
int cursor_x = starpilot_nvg->rightHandDM ? width() - UI_BORDER_SIZE : UI_BORDER_SIZE;
personality_btn->setVisible(onroad_distance_btn_enabled);
if (onroad_distance_btn_enabled) {
const int personality_x = starpilot_nvg->rightHandDM ? cursor_x - personality_btn->width() : cursor_x;
personality_btn->move(personality_x, controls_y);
personality_btn->updateState(s, fs);
cursor_x += starpilot_nvg->rightHandDM ? -(personality_btn->width() + gap) : personality_btn->width() + gap;
}
for (FavoriteButton *favorite_btn : visible_favorite_btns) {
const int favorite_x = starpilot_nvg->rightHandDM ? cursor_x - favorite_btn->width() : cursor_x;
favorite_btn->move(favorite_x, controls_y);
favorite_btn->setVisible(true);
cursor_x += starpilot_nvg->rightHandDM ? -(favorite_btn->width() + gap) : favorite_btn->width() + gap;
}
const QPoint screen_recorder_position = hide_steering_wheel
? experimental_button_position
@@ -1,6 +1,7 @@
#pragma once
#include <QVBoxLayout>
#include <array>
#include <memory>
#include "selfdrive/ui/qt/onroad/hud.h"
#include "selfdrive/ui/qt/onroad/buttons.h"
@@ -42,6 +43,7 @@ private:
void paintEvent(QPaintEvent *event) override;
DrivingPersonalityButton *personality_btn;
std::array<FavoriteButton*, 3> favorite_btns;
ScreenRecorder *screen_recorder;
protected:
+4 -3
View File
@@ -70,11 +70,12 @@ void DriverMonitorRenderer::draw(QPainter &painter, const QRect &surface_rect) {
float x = is_rhd ? surface_rect.width() - offset : offset;
float y = surface_rect.height() - offset;
if (onroad_distance_btn_enabled) {
if (onroad_controls_width > 0) {
const int controls_offset = onroad_controls_width + (2 * UI_BORDER_SIZE);
if (is_rhd) {
x -= UI_BORDER_SIZE + (btn_size + UI_BORDER_SIZE) + UI_BORDER_SIZE;
x -= controls_offset;
} else {
x += UI_BORDER_SIZE + (btn_size + UI_BORDER_SIZE) + UI_BORDER_SIZE;
x += controls_offset;
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ public:
StarPilotAnnotatedCameraWidget *starpilot_nvg;
bool onroad_distance_btn_enabled;
int onroad_controls_width = 0;
QJsonObject starpilot_toggles;
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
from collections.abc import Iterable
from typing import Any
from openpilot.common.params import ParamKeyType, Params
FAVORITE_SLOTS_PARAM = "StarPilotFavoriteSlots"
FAVORITE_SLOT_COUNT = 3
def default_favorite_slots() -> list[dict[str, Any]]:
return [
{"enabled": False, "show_onroad": False, "key": None, "label": ""}
for _ in range(FAVORITE_SLOT_COUNT)
]
def _load_raw_slots(raw_slots: Any) -> list[Any]:
if raw_slots in (None, "", b""):
return default_favorite_slots()
if isinstance(raw_slots, bytes):
raw_slots = raw_slots.decode("utf-8", errors="replace")
if isinstance(raw_slots, str):
try:
raw_slots = json.loads(raw_slots)
except json.JSONDecodeError:
return default_favorite_slots()
if isinstance(raw_slots, dict):
raw_slots = raw_slots.get("slots", [])
return raw_slots if isinstance(raw_slots, list) else default_favorite_slots()
def _get_key_type(params: Params, key: str):
for getter_name in ("get_type", "get_key_type"):
getter = getattr(params, getter_name, None)
if getter is None:
continue
try:
return getter(key)
except Exception:
return None
return None
def is_bool_param(params: Params, key: str | None, eligible_keys: Iterable[str] | None = None) -> bool:
if not key:
return False
if eligible_keys is not None and key not in set(eligible_keys):
return False
return _get_key_type(params, key) == ParamKeyType.BOOL
def normalize_favorite_slots(raw_slots: Any, params: Params | None = None,
eligible_keys: Iterable[str] | None = None) -> list[dict[str, Any]]:
slots = default_favorite_slots()
eligible = set(eligible_keys) if eligible_keys is not None else None
for idx, raw_slot in enumerate(_load_raw_slots(raw_slots)[:FAVORITE_SLOT_COUNT]):
if not isinstance(raw_slot, dict):
continue
key = raw_slot.get("key")
if key is not None:
key = str(key).strip() or None
if key and ((eligible is not None and key not in eligible) or (params is not None and not is_bool_param(params, key))):
key = None
label = str(raw_slot.get("label") or "").strip()
if len(label) > 32:
label = label[:32].rstrip()
slots[idx] = {
"enabled": bool(raw_slot.get("enabled", False)),
"show_onroad": bool(raw_slot.get("show_onroad", False)),
"key": key,
"label": label if key else "",
}
return slots
def load_favorite_slots(params: Params | None = None, eligible_keys: Iterable[str] | None = None) -> list[dict[str, Any]]:
params = params or Params(return_defaults=True)
try:
raw_slots = params.get(FAVORITE_SLOTS_PARAM)
except Exception:
raw_slots = None
return normalize_favorite_slots(raw_slots, params=params, eligible_keys=eligible_keys)
def save_favorite_slots(slots: list[dict[str, Any]], params: Params | None = None) -> list[dict[str, Any]]:
params = params or Params(return_defaults=True)
normalized = normalize_favorite_slots(slots, params=params)
params.put(FAVORITE_SLOTS_PARAM, normalized)
return normalized
def request_starpilot_toggle_refresh(params_memory: Params | None = None) -> None:
params_memory = params_memory or Params(memory=True)
params_memory.put_bool("StarPilotTogglesUpdated", True)
def toggle_favorite_slot(slot_index: int, params: Params | None = None, params_memory: Params | None = None) -> bool:
if slot_index < 0 or slot_index >= FAVORITE_SLOT_COUNT:
return False
params = params or Params(return_defaults=True)
slots = load_favorite_slots(params)
slot = slots[slot_index]
key = slot.get("key")
if not slot.get("enabled") or not is_bool_param(params, key):
return False
next_value = not params.get_bool(key)
put_bool = getattr(params, "put_bool_nonblocking", None) or getattr(params, "put_bool", None)
if put_bool is None:
return False
put_bool(key, next_value)
request_starpilot_toggle_refresh(params_memory)
return True
+21
View File
@@ -137,6 +137,9 @@ BUTTON_FUNCTIONS = {
"BOOKMARK": 8,
"AOL_TOGGLE": 9,
"SLC_ADOPT": 10,
"FAVORITE_1": 11,
"FAVORITE_2": 12,
"FAVORITE_3": 13,
}
CANCEL_BUTTON_MIGRATION_KEY = "CancelButtonControlsMigrated"
@@ -482,6 +485,11 @@ class StarPilotVariables:
# runtime behavior to defaults after the driver has configured them.
return self.get_value(key, cast=float, condition=condition, respect_tuning_level=False)
@staticmethod
def set_favorite_button_flags(toggle, suffix, button_control):
for slot_number in range(1, 4):
setattr(toggle, f"favorite_{slot_number}_via_{suffix}", button_control == BUTTON_FUNCTIONS[f"FAVORITE_{slot_number}"])
def _sync_stock_param(self, key, stock_key, live_value):
try:
live_value = float(live_value)
@@ -876,6 +884,7 @@ class StarPilotVariables:
toggle.switchback_mode_via_distance = distance_button_control == BUTTON_FUNCTIONS["SWITCHBACK_MODE"]
toggle.traffic_mode_via_distance = toggle.openpilot_longitudinal and distance_button_control == BUTTON_FUNCTIONS["TRAFFIC_MODE"]
toggle.bookmark_via_distance = distance_button_control == BUTTON_FUNCTIONS["BOOKMARK"]
self.set_favorite_button_flags(toggle, "distance", distance_button_control)
distance_button_control_long = self.get_button_function("LongDistanceButtonControl")
toggle.experimental_mode_via_distance_long = toggle.openpilot_longitudinal and distance_button_control_long == BUTTON_FUNCTIONS["EXPERIMENTAL_MODE"]
@@ -887,6 +896,7 @@ class StarPilotVariables:
toggle.switchback_mode_via_distance_long = distance_button_control_long == BUTTON_FUNCTIONS["SWITCHBACK_MODE"]
toggle.traffic_mode_via_distance_long = toggle.openpilot_longitudinal and distance_button_control_long == BUTTON_FUNCTIONS["TRAFFIC_MODE"]
toggle.bookmark_via_distance_long = distance_button_control_long == BUTTON_FUNCTIONS["BOOKMARK"]
self.set_favorite_button_flags(toggle, "distance_long", distance_button_control_long)
distance_button_control_very_long = self.get_button_function("VeryLongDistanceButtonControl")
toggle.experimental_mode_via_distance_very_long = toggle.openpilot_longitudinal and distance_button_control_very_long == BUTTON_FUNCTIONS["EXPERIMENTAL_MODE"]
@@ -898,6 +908,7 @@ class StarPilotVariables:
toggle.switchback_mode_via_distance_very_long = distance_button_control_very_long == BUTTON_FUNCTIONS["SWITCHBACK_MODE"]
toggle.traffic_mode_via_distance_very_long = toggle.openpilot_longitudinal and distance_button_control_very_long == BUTTON_FUNCTIONS["TRAFFIC_MODE"]
toggle.bookmark_via_distance_very_long = distance_button_control_very_long == BUTTON_FUNCTIONS["BOOKMARK"]
self.set_favorite_button_flags(toggle, "distance_very_long", distance_button_control_very_long)
cancel_button_control = self.get_button_function("CancelButtonControl", condition=toggle.remap_cancel_to_distance)
toggle.experimental_mode_via_cancel = toggle.openpilot_longitudinal and cancel_button_control == BUTTON_FUNCTIONS["EXPERIMENTAL_MODE"]
@@ -909,6 +920,7 @@ class StarPilotVariables:
toggle.switchback_mode_via_cancel = cancel_button_control == BUTTON_FUNCTIONS["SWITCHBACK_MODE"]
toggle.traffic_mode_via_cancel = toggle.openpilot_longitudinal and cancel_button_control == BUTTON_FUNCTIONS["TRAFFIC_MODE"]
toggle.bookmark_via_cancel = cancel_button_control == BUTTON_FUNCTIONS["BOOKMARK"]
self.set_favorite_button_flags(toggle, "cancel", cancel_button_control)
cancel_button_control_long = self.get_button_function("LongCancelButtonControl", condition=toggle.remap_cancel_to_distance)
toggle.experimental_mode_via_cancel_long = toggle.openpilot_longitudinal and cancel_button_control_long == BUTTON_FUNCTIONS["EXPERIMENTAL_MODE"]
@@ -920,6 +932,7 @@ class StarPilotVariables:
toggle.switchback_mode_via_cancel_long = cancel_button_control_long == BUTTON_FUNCTIONS["SWITCHBACK_MODE"]
toggle.traffic_mode_via_cancel_long = toggle.openpilot_longitudinal and cancel_button_control_long == BUTTON_FUNCTIONS["TRAFFIC_MODE"]
toggle.bookmark_via_cancel_long = cancel_button_control_long == BUTTON_FUNCTIONS["BOOKMARK"]
self.set_favorite_button_flags(toggle, "cancel_long", cancel_button_control_long)
cancel_button_control_very_long = self.get_button_function("VeryLongCancelButtonControl", condition=toggle.remap_cancel_to_distance)
toggle.experimental_mode_via_cancel_very_long = toggle.openpilot_longitudinal and cancel_button_control_very_long == BUTTON_FUNCTIONS["EXPERIMENTAL_MODE"]
@@ -931,6 +944,7 @@ class StarPilotVariables:
toggle.switchback_mode_via_cancel_very_long = cancel_button_control_very_long == BUTTON_FUNCTIONS["SWITCHBACK_MODE"]
toggle.traffic_mode_via_cancel_very_long = toggle.openpilot_longitudinal and cancel_button_control_very_long == BUTTON_FUNCTIONS["TRAFFIC_MODE"]
toggle.bookmark_via_cancel_very_long = cancel_button_control_very_long == BUTTON_FUNCTIONS["BOOKMARK"]
self.set_favorite_button_flags(toggle, "cancel_very_long", cancel_button_control_very_long)
toggle.frogsgomoo_tweak = self.get_value("FrogsGoMoosTweak", condition=toggle.openpilot_longitudinal and toggle.car_make == "toyota")
toggle.stoppingDecelRate = 0.01 if toggle.frogsgomoo_tweak else toggle.stoppingDecelRate
@@ -986,6 +1000,7 @@ class StarPilotVariables:
toggle.switchback_mode_via_lkas = lkas_button_control == BUTTON_FUNCTIONS["SWITCHBACK_MODE"]
toggle.traffic_mode_via_lkas = toggle.openpilot_longitudinal and lkas_button_control == BUTTON_FUNCTIONS["TRAFFIC_MODE"]
toggle.bookmark_via_lkas = lkas_button_control == BUTTON_FUNCTIONS["BOOKMARK"]
self.set_favorite_button_flags(toggle, "lkas", lkas_button_control)
has_canfd_media_buttons = toggle.car_make == "hyundai" and bool(CP.flags & HyundaiFlags.CANFD)
mode_button_control = self.get_button_function("ModeButtonControl", condition=has_canfd_media_buttons)
@@ -998,6 +1013,7 @@ class StarPilotVariables:
toggle.switchback_mode_via_mode = mode_button_control == BUTTON_FUNCTIONS["SWITCHBACK_MODE"]
toggle.traffic_mode_via_mode = toggle.openpilot_longitudinal and mode_button_control == BUTTON_FUNCTIONS["TRAFFIC_MODE"]
toggle.bookmark_via_mode = mode_button_control == BUTTON_FUNCTIONS["BOOKMARK"]
self.set_favorite_button_flags(toggle, "mode", mode_button_control)
mode_button_control_long = self.get_button_function("LongModeButtonControl", condition=has_canfd_media_buttons)
toggle.experimental_mode_via_mode_long = toggle.openpilot_longitudinal and mode_button_control_long == BUTTON_FUNCTIONS["EXPERIMENTAL_MODE"]
@@ -1009,6 +1025,7 @@ class StarPilotVariables:
toggle.switchback_mode_via_mode_long = mode_button_control_long == BUTTON_FUNCTIONS["SWITCHBACK_MODE"]
toggle.traffic_mode_via_mode_long = toggle.openpilot_longitudinal and mode_button_control_long == BUTTON_FUNCTIONS["TRAFFIC_MODE"]
toggle.bookmark_via_mode_long = mode_button_control_long == BUTTON_FUNCTIONS["BOOKMARK"]
self.set_favorite_button_flags(toggle, "mode_long", mode_button_control_long)
mode_button_control_very_long = self.get_button_function("VeryLongModeButtonControl", condition=has_canfd_media_buttons)
toggle.experimental_mode_via_mode_very_long = toggle.openpilot_longitudinal and mode_button_control_very_long == BUTTON_FUNCTIONS["EXPERIMENTAL_MODE"]
@@ -1020,6 +1037,7 @@ class StarPilotVariables:
toggle.switchback_mode_via_mode_very_long = mode_button_control_very_long == BUTTON_FUNCTIONS["SWITCHBACK_MODE"]
toggle.traffic_mode_via_mode_very_long = toggle.openpilot_longitudinal and mode_button_control_very_long == BUTTON_FUNCTIONS["TRAFFIC_MODE"]
toggle.bookmark_via_mode_very_long = mode_button_control_very_long == BUTTON_FUNCTIONS["BOOKMARK"]
self.set_favorite_button_flags(toggle, "mode_very_long", mode_button_control_very_long)
star_button_control = self.get_button_function("StarButtonControl", condition=has_canfd_media_buttons)
toggle.experimental_mode_via_star = toggle.openpilot_longitudinal and star_button_control == BUTTON_FUNCTIONS["EXPERIMENTAL_MODE"]
@@ -1031,6 +1049,7 @@ class StarPilotVariables:
toggle.switchback_mode_via_star = star_button_control == BUTTON_FUNCTIONS["SWITCHBACK_MODE"]
toggle.traffic_mode_via_star = toggle.openpilot_longitudinal and star_button_control == BUTTON_FUNCTIONS["TRAFFIC_MODE"]
toggle.bookmark_via_star = star_button_control == BUTTON_FUNCTIONS["BOOKMARK"]
self.set_favorite_button_flags(toggle, "star", star_button_control)
star_button_control_long = self.get_button_function("LongStarButtonControl", condition=has_canfd_media_buttons)
toggle.experimental_mode_via_star_long = toggle.openpilot_longitudinal and star_button_control_long == BUTTON_FUNCTIONS["EXPERIMENTAL_MODE"]
@@ -1042,6 +1061,7 @@ class StarPilotVariables:
toggle.switchback_mode_via_star_long = star_button_control_long == BUTTON_FUNCTIONS["SWITCHBACK_MODE"]
toggle.traffic_mode_via_star_long = toggle.openpilot_longitudinal and star_button_control_long == BUTTON_FUNCTIONS["TRAFFIC_MODE"]
toggle.bookmark_via_star_long = star_button_control_long == BUTTON_FUNCTIONS["BOOKMARK"]
self.set_favorite_button_flags(toggle, "star_long", star_button_control_long)
star_button_control_very_long = self.get_button_function("VeryLongStarButtonControl", condition=has_canfd_media_buttons)
toggle.experimental_mode_via_star_very_long = toggle.openpilot_longitudinal and star_button_control_very_long == BUTTON_FUNCTIONS["EXPERIMENTAL_MODE"]
@@ -1053,6 +1073,7 @@ class StarPilotVariables:
toggle.switchback_mode_via_star_very_long = star_button_control_very_long == BUTTON_FUNCTIONS["SWITCHBACK_MODE"]
toggle.traffic_mode_via_star_very_long = toggle.openpilot_longitudinal and star_button_control_very_long == BUTTON_FUNCTIONS["TRAFFIC_MODE"]
toggle.bookmark_via_star_very_long = star_button_control_very_long == BUTTON_FUNCTIONS["BOOKMARK"]
self.set_favorite_button_flags(toggle, "star_very_long", star_button_control_very_long)
toggle.has_canfd_media_buttons = has_canfd_media_buttons
toggle.lock_doors_timer = self.get_value("LockDoorsTimer", cast=float, condition=(toggle.car_make == "toyota"))
@@ -0,0 +1,80 @@
from openpilot.common.params import ParamKeyType
from openpilot.starpilot.common.favorite_slots import (
FAVORITE_SLOTS_PARAM,
default_favorite_slots,
load_favorite_slots,
toggle_favorite_slot,
)
class FakeParams:
def __init__(self):
self.store = {}
self.types = {
FAVORITE_SLOTS_PARAM: ParamKeyType.JSON,
"RedneckCruise": ParamKeyType.BOOL,
"NotBool": ParamKeyType.INT,
}
def get(self, key):
return self.store.get(key)
def get_bool(self, key):
return bool(self.store.get(key, False))
def put(self, key, value):
self.store[key] = value
def put_bool(self, key, value):
self.store[key] = bool(value)
def put_bool_nonblocking(self, key, value):
self.put_bool(key, value)
def get_type(self, key):
return self.types.get(key, ParamKeyType.STRING)
def test_load_favorite_slots_defaults_on_empty_payload():
params = FakeParams()
assert load_favorite_slots(params) == default_favorite_slots()
def test_load_favorite_slots_filters_non_bool_keys():
params = FakeParams()
params.put(FAVORITE_SLOTS_PARAM, [
{"enabled": True, "show_onroad": True, "key": "NotBool", "label": "Bad"},
{"enabled": True, "show_onroad": False, "key": "RedneckCruise", "label": "Redneck Cruise"},
])
slots = load_favorite_slots(params)
assert slots[0]["key"] is None
assert slots[0]["enabled"] is True
assert slots[1]["key"] == "RedneckCruise"
assert slots[1]["show_onroad"] is False
def test_toggle_favorite_slot_ignores_disabled_slot():
params = FakeParams()
params.put("RedneckCruise", False)
params.put(FAVORITE_SLOTS_PARAM, [
{"enabled": False, "show_onroad": True, "key": "RedneckCruise", "label": "Redneck Cruise"},
])
assert toggle_favorite_slot(0, params, FakeParams()) is False
assert params.get_bool("RedneckCruise") is False
def test_toggle_favorite_slot_flips_bool_and_requests_refresh():
params = FakeParams()
memory = FakeParams()
params.put("RedneckCruise", False)
params.put(FAVORITE_SLOTS_PARAM, [
{"enabled": True, "show_onroad": False, "key": "RedneckCruise", "label": "Redneck Cruise"},
])
assert toggle_favorite_slot(0, params, memory) is True
assert params.get_bool("RedneckCruise") is True
assert memory.get_bool("StarPilotTogglesUpdated") is True
@@ -105,6 +105,16 @@ def test_button_function_ignores_tuning_level_gate():
assert variables.get_button_function("LKASButtonControl") == spv.BUTTON_FUNCTIONS["AOL_TOGGLE"]
def test_favorite_button_flags_map_to_three_slots():
toggle = SimpleNamespace()
spv.StarPilotVariables.set_favorite_button_flags(toggle, "lkas", spv.BUTTON_FUNCTIONS["FAVORITE_2"])
assert toggle.favorite_1_via_lkas is False
assert toggle.favorite_2_via_lkas is True
assert toggle.favorite_3_via_lkas is False
def test_set_speed_limit_available_on_openpilot_longitudinal():
assert spv.set_speed_limit_available(openpilot_longitudinal=True, has_cc_long=False, pcm_cruise_speed=True) is True
+6
View File
@@ -14,6 +14,7 @@ from openpilot.starpilot.common.experimental_state import (
sync_manual_cc_state,
sync_manual_ce_state,
)
from openpilot.starpilot.common.favorite_slots import toggle_favorite_slot
from openpilot.starpilot.common.starpilot_utilities import is_FrogsGoMoo
from openpilot.starpilot.common.starpilot_variables import ERROR_LOGS_PATH, GearShifter, NON_DRIVING_GEARS
@@ -82,6 +83,11 @@ class StarPilotCard:
self.params_memory.put_bool("SwitchbackModeEnabled", self.switchback_mode_enabled)
elif sm["carControl"].longActive and getattr(starpilot_toggles, f"traffic_mode_via_{key}"):
self.traffic_mode_enabled = not self.traffic_mode_enabled
else:
for slot_index in range(3):
if getattr(starpilot_toggles, f"favorite_{slot_index + 1}_via_{key}", False):
toggle_favorite_slot(slot_index, self.params, self.params_memory)
break
def handle_bookmark(self):
counter = self.params_memory.get_int("WheelButtonBookmarkCounter")
@@ -2,12 +2,21 @@ from types import SimpleNamespace
from opendbc.car.chrysler.values import CAR as CHRYSLER_CAR
from openpilot.common.params import ParamKeyType
from openpilot.starpilot.common.favorite_slots import FAVORITE_SLOTS_PARAM
from openpilot.starpilot.controls import starpilot_card as spc
class FakeParams:
def __init__(self, *args, **kwargs):
self._store = {}
self._types = {
FAVORITE_SLOTS_PARAM: ParamKeyType.JSON,
"RedneckCruise": ParamKeyType.BOOL,
}
def get(self, key):
return self._store.get(key)
def get_bool(self, key):
return bool(self._store.get(key, False))
@@ -15,6 +24,9 @@ class FakeParams:
def put_bool(self, key, value):
self._store[key] = bool(value)
def put(self, key, value):
self._store[key] = value
def get_int(self, key, default=0):
return int(self._store.get(key, default))
@@ -24,6 +36,9 @@ class FakeParams:
def put_bool_nonblocking(self, key, value):
self.put_bool(key, value)
def get_type(self, key):
return self._types.get(key, ParamKeyType.STRING)
class FakeSM(dict):
def __init__(self, *args, updated=None, **kwargs):
@@ -407,3 +422,20 @@ def test_cancel_button_short_press_can_run_independent_mapping(monkeypatch, tmp_
assert ret.cancelLongPressed is False
assert ret.cancelVeryLongPressed is False
assert card.params_memory.get_int("WheelButtonBookmarkCounter") == 1
def test_favorite_wheel_action_toggles_hidden_onroad_slot(monkeypatch, tmp_path):
monkeypatch.setattr(spc, "Params", FakeParams)
monkeypatch.setattr(spc, "is_FrogsGoMoo", lambda: False)
monkeypatch.setattr(spc, "ERROR_LOGS_PATH", tmp_path)
card = spc.StarPilotCard(SimpleNamespace(brand="gm"), SimpleNamespace(alternativeExperience=0))
card.params.put("RedneckCruise", False)
card.params.put(FAVORITE_SLOTS_PARAM, [
{"enabled": True, "show_onroad": False, "key": "RedneckCruise", "label": "Redneck Cruise"},
])
card.handle_button_event("lkas", make_sm(), make_toggles(favorite_1_via_lkas=True))
assert card.params.get_bool("RedneckCruise") is True
assert card.params_memory.get_bool("StarPilotTogglesUpdated") is True
@@ -1,7 +1,7 @@
import { html, reactive } from "/assets/vendor/arrow-core.js"
import { createBrowserHistory, createRouter } from "/assets/vendor/remix-router-1.3.1.js"
import { hideSidebar } from "/assets/js/utils.js"
import { DeviceSettings } from "/assets/components/tools/device_settings.js"
import { DeviceSettings } from "/assets/components/tools/device_settings.js?v=favorite-slots-2"
import { ErrorLogs } from "/assets/components/tools/error_logs.js"
import { VehicleFeatures } from "/assets/components/tools/vehicle_features.js"
import { GalaxyPairing } from "/assets/components/tools/galaxy.js"
@@ -470,6 +470,77 @@
opacity: var(--disabled-opacity);
}
/* ――― Favorite Slots ――― */
.ds-favorites-panel {
display: grid;
gap: var(--gap-base);
padding: var(--padding-base);
}
.ds-favorite-card {
background: var(--input-bg);
border: var(--border-style-main);
border-radius: var(--border-radius-base);
padding: var(--padding-base);
}
.ds-favorite-card-header,
.ds-favorite-controls,
.ds-favorite-live-row,
.ds-favorite-switch {
align-items: center;
display: flex;
}
.ds-favorite-card-header,
.ds-favorite-live-row {
justify-content: space-between;
gap: var(--gap-base);
}
.ds-favorite-controls {
flex-wrap: wrap;
gap: var(--gap-base);
margin-top: var(--margin-base);
}
.ds-favorite-field {
display: flex;
flex: 1;
flex-direction: column;
gap: 0.35rem;
min-width: 0;
}
.ds-favorite-field span,
.ds-favorite-switch span {
color: var(--text-muted);
font-size: var(--font-size-xs);
}
.ds-favorite-select {
max-width: none;
}
.ds-favorite-search {
max-width: none;
}
.ds-favorite-switch {
flex-shrink: 0;
gap: 0.6rem;
}
.ds-favorite-live-row {
border-top: 1px solid var(--sidebar-border-color);
margin-top: var(--margin-base);
padding-top: var(--padding-sm);
}
.ds-favorite-live-copy {
min-width: 0;
}
/* ――― Mobile ――― */
@media only screen and (max-width: 768px) and (orientation: portrait) {
.ds-wrapper {
@@ -495,4 +566,15 @@
.ds-tabs::-webkit-scrollbar {
display: none;
}
.ds-favorite-card-header,
.ds-favorite-controls,
.ds-favorite-live-row {
align-items: stretch;
flex-direction: column;
}
.ds-favorite-switch {
justify-content: space-between;
}
}
@@ -27,6 +27,12 @@ const state = reactive({
fetched: false,
activeSectionSlug: "",
numericUpdating: {},
favoriteLoading: false,
favoriteSaving: false,
favoriteOptions: [],
favoriteSlots: [],
favoriteFilters: ["", "", ""],
favoriteValues: {},
})
function slugifySectionName(name) {
@@ -218,6 +224,33 @@ function syncInputs() {
syncSelectValue(el, key)
}
}
const favoriteSlots = normalizeFavoriteSlots(state.favoriteSlots)
for (const el of document.querySelectorAll("[data-favorite-slot][data-favorite-field]")) {
const slotIndex = Number.parseInt(el.dataset.favoriteSlot, 10)
const field = el.dataset.favoriteField
const slot = favoriteSlots[slotIndex]
if (!slot) continue
if (el.tagName === "SELECT" && field === "key") {
el.value = slot.key || ""
el.disabled = !!state.favoriteSaving
} else if (el.tagName === "INPUT" && field === "search") {
el.value = state.favoriteFilters[slotIndex] || ""
el.disabled = !!state.favoriteSaving
} else if (el.tagName === "INPUT" && el.type === "checkbox") {
el.checked = !!slot[field]
if (field === "enabled") {
el.disabled = !!state.favoriteSaving
} else if (field === "show_onroad") {
el.disabled = !!state.favoriteSaving || !slot.enabled || !slot.key
}
}
}
for (const el of document.querySelectorAll("input[type='checkbox'][data-favorite-value-key]")) {
el.checked = !!state.values[el.dataset.favoriteValueKey]
}
}
async function fetchDefaultValues() {
@@ -252,7 +285,7 @@ async function fetchLayoutAndParams() {
state.loadingValues = true
try {
const layoutRes = await fetch("/assets/components/tools/device_settings_layout.json")
const layoutRes = await fetch("/assets/components/tools/device_settings_layout.json?v=favorite-slots-2", { cache: "no-store" })
const rawLayoutData = await layoutRes.json()
const layoutData = rawLayoutData
@@ -293,6 +326,9 @@ async function fetchLayoutAndParams() {
console.error("Failed to fetch param values:", e)
state.defaultValues = {}
}
await fetchFavoriteSlots()
state.loadingValues = false
// Resolve slug now that layout is available (uses stored route params)
@@ -396,6 +432,145 @@ function coerceValueByType(rawValue, dataType) {
return rawValue
}
function defaultFavoriteSlots() {
return [0, 1, 2].map(() => ({
enabled: false,
show_onroad: false,
key: null,
label: "",
}))
}
function normalizeFavoriteSlots(slots) {
const normalized = defaultFavoriteSlots()
if (!Array.isArray(slots)) return normalized
slots.slice(0, 3).forEach((slot, index) => {
if (!slot || typeof slot !== "object") return
const key = slot.key ? String(slot.key) : null
normalized[index] = {
enabled: !!slot.enabled,
show_onroad: !!slot.show_onroad,
key,
label: key ? String(slot.label || key) : "",
}
})
return normalized
}
async function fetchFavoriteSlots() {
state.favoriteLoading = true
try {
const res = await fetch("/api/favorites/slots")
const data = await res.json()
if (res.ok) {
state.favoriteOptions = Array.isArray(data.options) ? data.options : []
state.favoriteSlots = normalizeFavoriteSlots(data.slots)
state.favoriteValues = data.values || {}
state.values = { ...state.values, ...state.favoriteValues, StarPilotFavoriteSlots: state.favoriteSlots }
}
} catch (e) {
console.error("Failed to fetch favorite slots:", e)
}
state.favoriteLoading = false
}
async function saveFavoriteSlots(slots) {
if (state.favoriteSaving) return
const previousSlots = state.favoriteSlots
state.favoriteSlots = normalizeFavoriteSlots(slots)
state.favoriteSaving = true
try {
const res = await fetch("/api/favorites/slots", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slots: state.favoriteSlots }),
})
const data = await res.json()
if (res.ok) {
state.favoriteOptions = Array.isArray(data.options) ? data.options : state.favoriteOptions
state.favoriteSlots = normalizeFavoriteSlots(data.slots)
state.favoriteValues = data.values || {}
state.values = { ...state.values, ...state.favoriteValues, StarPilotFavoriteSlots: state.favoriteSlots }
showParamSnackbar(data.message || "Favorite slots saved.")
scheduleSyncInputs()
} else {
state.favoriteSlots = previousSlots
showParamSnackbar(data.error || "Failed to save favorite slots", "error")
}
} catch (e) {
state.favoriteSlots = previousSlots
showParamSnackbar("Network error — is the device reachable?", "error")
}
state.favoriteSaving = false
}
function updateFavoriteSlot(index, patch) {
const slots = normalizeFavoriteSlots(state.favoriteSlots)
const current = slots[index] || defaultFavoriteSlots()[0]
const nextSlot = { ...current, ...patch }
if (!nextSlot.key) {
nextSlot.label = ""
} else {
const option = state.favoriteOptions.find(opt => opt.key === nextSlot.key)
nextSlot.label = option?.label || nextSlot.key
}
slots[index] = nextSlot
saveFavoriteSlots(slots)
}
function updateFavoriteFilter(index, event) {
const filters = Array.isArray(state.favoriteFilters) ? [...state.favoriteFilters] : ["", "", ""]
filters[index] = getEventValue(event)
state.favoriteFilters = filters.slice(0, 3)
scheduleSyncInputs()
}
function favoriteOptionMatchesFilter(option, filter) {
if (!filter) return true
const q = filter.toLowerCase()
return [option.label, option.key, option.section, option.description]
.some(value => String(value || "").toLowerCase().includes(q))
}
async function updateFavoriteValue(key, checked) {
const current = state.values[key]
state.values = { ...state.values, [key]: checked }
state.favoriteValues = { ...state.favoriteValues, [key]: checked }
try {
const res = await fetch("/api/params", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key, value: checked }),
})
const data = await res.json()
if (res.ok) {
const updated = (data.updated && typeof data.updated === "object") ? data.updated : {}
state.values = { ...state.values, [key]: checked, ...updated }
state.favoriteValues = { ...state.favoriteValues, [key]: state.values[key] }
showParamSnackbar(data.message || `Parameter '${key}' updated.`)
scheduleSyncInputs()
} else {
state.values = { ...state.values, [key]: current }
state.favoriteValues = { ...state.favoriteValues, [key]: current }
showParamSnackbar(data.error || "Failed to update parameter", "error")
}
} catch (e) {
state.values = { ...state.values, [key]: current }
state.favoriteValues = { ...state.favoriteValues, [key]: current }
showParamSnackbar("Network error — is the device reachable?", "error")
}
}
function stepPrecision(step, explicitPrecision) {
if (explicitPrecision !== undefined && explicitPrecision !== null && explicitPrecision !== "") {
const parsed = Number.parseInt(explicitPrecision, 10)
@@ -598,6 +773,11 @@ async function resetNumericParam(param) {
}
async function updateParam(key, elType) {
if (String(key).toLowerCase() === "starpilotfavoriteslots") {
await saveFavoriteSlots(state.favoriteSlots)
return
}
const current = state.values[key]
const el = document.getElementById(`ds-${key}`)
if (!el) return
@@ -767,7 +947,113 @@ function handleSectionTabClick(sectionSlug, event) {
}
}
function renderFavoriteSlotsPanel() {
if (state.favoriteLoading) {
return html`<div class="ds-loading">Loading favorite slots...</div>`
}
const slots = normalizeFavoriteSlots(state.favoriteSlots)
const options = state.favoriteOptions || []
return html`
<div class="ds-favorites-panel">
${slots.map((slot, index) => {
const selectedOption = options.find(opt => opt.key === slot.key)
const selectedKey = slot.key || ""
const selectedValue = selectedKey ? !!state.values[selectedKey] : false
const favoriteFilter = state.favoriteFilters[index] || ""
let filteredOptions = options.filter(opt => favoriteOptionMatchesFilter(opt, favoriteFilter))
if (selectedOption && !filteredOptions.some(opt => opt.key === selectedOption.key)) {
filteredOptions = [selectedOption, ...filteredOptions]
}
return html`
<div class="ds-favorite-card">
<div class="ds-favorite-card-header">
<div>
<div class="ds-row-label">Favorite #${index + 1}</div>
<div class="ds-row-desc">${selectedOption?.section || "No toggle selected"}</div>
</div>
<label class="ds-favorite-switch">
<span>Enabled</span>
<input
type="checkbox"
class="ds-toggle"
data-favorite-slot="${index}"
data-favorite-field="enabled"
checked="${() => slot.enabled}"
disabled="${() => state.favoriteSaving}"
@change="${(e) => updateFavoriteSlot(index, { enabled: !!e.currentTarget.checked })}" />
</label>
</div>
<div class="ds-favorite-controls">
<label class="ds-favorite-field">
<span>Search</span>
<input
type="search"
class="ds-search ds-favorite-search"
data-favorite-slot="${index}"
data-favorite-field="search"
value="${() => favoriteFilter}"
disabled="${() => state.favoriteSaving}"
@input="${(e) => updateFavoriteFilter(index, e)}" />
</label>
<label class="ds-favorite-field">
<span>Toggle</span>
<select
class="ds-select ds-favorite-select"
data-favorite-slot="${index}"
data-favorite-field="key"
disabled="${() => state.favoriteSaving}"
@change="${(e) => updateFavoriteSlot(index, { key: e.currentTarget.value || null })}">
<option value="" selected="${() => selectedKey === ""}">Select a toggle...</option>
${filteredOptions.map(opt => html`
<option value="${opt.key}" selected="${() => selectedKey === opt.key}">${opt.label}</option>
`)}
</select>
</label>
<label class="ds-favorite-switch">
<span>Show On-Road Button</span>
<input
type="checkbox"
class="ds-toggle"
data-favorite-slot="${index}"
data-favorite-field="show_onroad"
checked="${() => slot.show_onroad}"
disabled="${() => state.favoriteSaving || !slot.enabled || !selectedKey}"
@change="${(e) => updateFavoriteSlot(index, { show_onroad: !!e.currentTarget.checked })}" />
</label>
</div>
${selectedKey ? html`
<div class="ds-favorite-live-row">
<div class="ds-favorite-live-copy">
<span class="ds-row-label">${selectedOption?.label || slot.label || selectedKey}</span>
${selectedOption?.description ? html`<div class="ds-row-desc">${selectedOption.description}</div>` : ""}
</div>
<input
type="checkbox"
class="ds-toggle"
data-favorite-value-key="${selectedKey}"
checked="${() => selectedValue}"
@change="${(e) => updateFavoriteValue(selectedKey, !!e.currentTarget.checked)}" />
</div>
` : ""}
</div>
`
})}
</div>
`
}
function renderSettingRow(p) {
if (p.ui_type === "favorites") {
return renderFavoriteSlotsPanel()
}
if (p.parent_key && !state.filter) {
if (!isParamEnabledForChildren(p.parent_key)) return ""
if (!state.expanded[p.parent_key]) return ""
@@ -1,4 +1,17 @@
[
{
"name": "Favorites",
"icon": "bi-star",
"params": [
{
"key": "StarPilotFavoriteSlots",
"label": "Favorite Slots",
"description": "Configure up to three favorite toggles for on-road buttons and steering-wheel bindings.",
"data_type": "json",
"ui_type": "favorites"
}
]
},
{
"name": "Lateral (Steering)",
"icon": "bi-arrows-move",
@@ -307,7 +320,7 @@
{
"key": "CustomAccelProfile0MPH",
"label": "0 mph",
"description": "Max acceleration in m/s\u00b2 at the fixed 0 mph breakpoint.",
"description": "Max acceleration in m/s² at the fixed 0 mph breakpoint.",
"data_type": "float",
"ui_type": "numeric",
"min": 0.0,
@@ -319,7 +332,7 @@
{
"key": "CustomAccelProfile11MPH",
"label": "11 mph",
"description": "Max acceleration in m/s\u00b2 at the fixed 11 mph breakpoint.",
"description": "Max acceleration in m/s² at the fixed 11 mph breakpoint.",
"data_type": "float",
"ui_type": "numeric",
"min": 0.0,
@@ -331,7 +344,7 @@
{
"key": "CustomAccelProfile22MPH",
"label": "22 mph",
"description": "Max acceleration in m/s\u00b2 at the fixed 22 mph breakpoint.",
"description": "Max acceleration in m/s² at the fixed 22 mph breakpoint.",
"data_type": "float",
"ui_type": "numeric",
"min": 0.0,
@@ -343,7 +356,7 @@
{
"key": "CustomAccelProfile34MPH",
"label": "34 mph",
"description": "Max acceleration in m/s\u00b2 at the fixed 34 mph breakpoint.",
"description": "Max acceleration in m/s² at the fixed 34 mph breakpoint.",
"data_type": "float",
"ui_type": "numeric",
"min": 0.0,
@@ -355,7 +368,7 @@
{
"key": "CustomAccelProfile45MPH",
"label": "45 mph",
"description": "Max acceleration in m/s\u00b2 at the fixed 45 mph breakpoint.",
"description": "Max acceleration in m/s² at the fixed 45 mph breakpoint.",
"data_type": "float",
"ui_type": "numeric",
"min": 0.0,
@@ -367,7 +380,7 @@
{
"key": "CustomAccelProfile56MPH",
"label": "56 mph",
"description": "Max acceleration in m/s\u00b2 at the fixed 56 mph breakpoint.",
"description": "Max acceleration in m/s² at the fixed 56 mph breakpoint.",
"data_type": "float",
"ui_type": "numeric",
"min": 0.0,
@@ -379,7 +392,7 @@
{
"key": "CustomAccelProfile89MPH",
"label": "89 mph",
"description": "Max acceleration in m/s\u00b2 at the fixed 89 mph breakpoint.",
"description": "Max acceleration in m/s² at the fixed 89 mph breakpoint.",
"data_type": "float",
"ui_type": "numeric",
"min": 0.0,
@@ -1053,7 +1066,7 @@
{
"key": "CustomCruise",
"label": "Cruise Interval",
"description": "How much the set speed increases or decreases for each + or \u2013 cruise control button press.",
"description": "How much the set speed increases or decreases for each + or cruise control button press.",
"data_type": "float",
"ui_type": "numeric",
"min": 1.0,
@@ -1063,7 +1076,7 @@
{
"key": "CustomCruiseLong",
"label": "Cruise Interval (Hold)",
"description": "How much the set speed increases or decreases while holding the + or \u2013 cruise control buttons.",
"description": "How much the set speed increases or decreases while holding the + or cruise control buttons.",
"data_type": "float",
"ui_type": "numeric",
"min": 1.0,
@@ -1562,7 +1575,7 @@
},
{
"key": "Offset1",
"label": "Speed Offset (0\u201324 mph)",
"label": "Speed Offset (024 mph)",
"description": "How much to offset posted speed-limits between 0 and 24 mph.",
"data_type": "float",
"ui_type": "numeric",
@@ -1572,7 +1585,7 @@
},
{
"key": "Offset2",
"label": "Speed Offset (25\u201334 mph)",
"label": "Speed Offset (2534 mph)",
"description": "How much to offset posted speed-limits between 25 and 34 mph.",
"data_type": "float",
"ui_type": "numeric",
@@ -1582,7 +1595,7 @@
},
{
"key": "Offset3",
"label": "Speed Offset (35\u201344 mph)",
"label": "Speed Offset (3544 mph)",
"description": "How much to offset posted speed-limits between 35 and 44 mph.",
"data_type": "float",
"ui_type": "numeric",
@@ -1592,7 +1605,7 @@
},
{
"key": "Offset4",
"label": "Speed Offset (45\u201354 mph)",
"label": "Speed Offset (4554 mph)",
"description": "How much to offset posted speed-limits between 45 and 54 mph.",
"data_type": "float",
"ui_type": "numeric",
@@ -1602,7 +1615,7 @@
},
{
"key": "Offset5",
"label": "Speed Offset (55\u201364 mph)",
"label": "Speed Offset (5564 mph)",
"description": "How much to offset posted speed-limits between 55 and 64 mph.",
"data_type": "float",
"ui_type": "numeric",
@@ -1612,7 +1625,7 @@
},
{
"key": "Offset6",
"label": "Speed Offset (65\u201374 mph)",
"label": "Speed Offset (6574 mph)",
"description": "How much to offset posted speed-limits between 65 and 74 mph.",
"data_type": "float",
"ui_type": "numeric",
@@ -1622,7 +1635,7 @@
},
{
"key": "Offset7",
"label": "Speed Offset (75\u201399 mph)",
"label": "Speed Offset (7599 mph)",
"description": "How much to offset posted speed-limits between 75 and 99 mph.",
"data_type": "float",
"ui_type": "numeric",
@@ -2236,7 +2249,7 @@
{
"key": "WarningImmediateVolume",
"label": "Warning Immediate Volume",
"description": "Set the volume for the loudest warnings that require urgent attention.\n\nExamples include: \"DISENGAGE IMMEDIATELY \u2014 Driver Distracted\", \"DISENGAGE IMMEDIATELY \u2014 Driver Unresponsive\".",
"description": "Set the volume for the loudest warnings that require urgent attention.\n\nExamples include: \"DISENGAGE IMMEDIATELY Driver Distracted\", \"DISENGAGE IMMEDIATELY Driver Unresponsive\".",
"data_type": "int",
"ui_type": "numeric",
"min": 25.0,
@@ -2594,6 +2607,18 @@
{
"value": 8,
"label": "Create Bookmark"
},
{
"value": 11,
"label": "Favorite #1"
},
{
"value": 12,
"label": "Favorite #2"
},
{
"value": 13,
"label": "Favorite #3"
}
]
},
@@ -2639,6 +2664,18 @@
{
"value": 8,
"label": "Create Bookmark"
},
{
"value": 11,
"label": "Favorite #1"
},
{
"value": 12,
"label": "Favorite #2"
},
{
"value": 13,
"label": "Favorite #3"
}
]
},
@@ -2684,6 +2721,18 @@
{
"value": 8,
"label": "Create Bookmark"
},
{
"value": 11,
"label": "Favorite #1"
},
{
"value": 12,
"label": "Favorite #2"
},
{
"value": 13,
"label": "Favorite #3"
}
]
},
@@ -2729,6 +2778,18 @@
{
"value": 8,
"label": "Create Bookmark"
},
{
"value": 11,
"label": "Favorite #1"
},
{
"value": 12,
"label": "Favorite #2"
},
{
"value": 13,
"label": "Favorite #3"
}
]
},
@@ -2774,6 +2835,18 @@
{
"value": 8,
"label": "Create Bookmark"
},
{
"value": 11,
"label": "Favorite #1"
},
{
"value": 12,
"label": "Favorite #2"
},
{
"value": 13,
"label": "Favorite #3"
}
]
},
@@ -2819,6 +2892,18 @@
{
"value": 8,
"label": "Create Bookmark"
},
{
"value": 11,
"label": "Favorite #1"
},
{
"value": 12,
"label": "Favorite #2"
},
{
"value": 13,
"label": "Favorite #3"
}
]
},
@@ -2868,6 +2953,18 @@
{
"value": 9,
"label": "Toggle Always On Lateral"
},
{
"value": 11,
"label": "Favorite #1"
},
{
"value": 12,
"label": "Favorite #2"
},
{
"value": 13,
"label": "Favorite #3"
}
]
},
@@ -2889,6 +2986,18 @@
{
"value": 10,
"label": "Adopt Current Speed Limit"
},
{
"value": 11,
"label": "Favorite #1"
},
{
"value": 12,
"label": "Favorite #2"
},
{
"value": 13,
"label": "Favorite #3"
}
]
},
@@ -2934,6 +3043,18 @@
{
"value": 8,
"label": "Create Bookmark"
},
{
"value": 11,
"label": "Favorite #1"
},
{
"value": 12,
"label": "Favorite #2"
},
{
"value": 13,
"label": "Favorite #3"
}
]
},
@@ -2979,6 +3100,18 @@
{
"value": 8,
"label": "Create Bookmark"
},
{
"value": 11,
"label": "Favorite #1"
},
{
"value": 12,
"label": "Favorite #2"
},
{
"value": 13,
"label": "Favorite #3"
}
]
},
@@ -3024,6 +3157,18 @@
{
"value": 8,
"label": "Create Bookmark"
},
{
"value": 11,
"label": "Favorite #1"
},
{
"value": 12,
"label": "Favorite #2"
},
{
"value": 13,
"label": "Favorite #3"
}
]
},
@@ -3069,6 +3214,18 @@
{
"value": 8,
"label": "Create Bookmark"
},
{
"value": 11,
"label": "Favorite #1"
},
{
"value": 12,
"label": "Favorite #2"
},
{
"value": 13,
"label": "Favorite #3"
}
]
},
@@ -3114,6 +3271,18 @@
{
"value": 8,
"label": "Create Bookmark"
},
{
"value": 11,
"label": "Favorite #1"
},
{
"value": 12,
"label": "Favorite #2"
},
{
"value": 13,
"label": "Favorite #3"
}
]
},
@@ -3159,6 +3328,18 @@
{
"value": 8,
"label": "Create Bookmark"
},
{
"value": 11,
"label": "Favorite #1"
},
{
"value": 12,
"label": "Favorite #2"
},
{
"value": 13,
"label": "Favorite #3"
}
]
}
@@ -44,7 +44,7 @@
<link rel="stylesheet" href="/assets/components/tools/tsk_manager.css">
<script type="module">
import("/assets/components/router.js").catch((err) => {
import("/assets/components/router.js?v=favorite-slots-2").catch((err) => {
console.error("[the_pond] bootstrap failed", err);
const target = document.getElementById("app") || document.body;
const pre = document.createElement("pre");
+132
View File
@@ -67,6 +67,7 @@ from openpilot.starpilot.common.model_versions import (
uses_split_off_policy_artifacts,
)
from openpilot.starpilot.common.experimental_state import sync_persist_chill_state, sync_persist_experimental_state
from openpilot.starpilot.common.favorite_slots import FAVORITE_SLOTS_PARAM, normalize_favorite_slots
from openpilot.starpilot.common.starpilot_utilities import delete_file, get_lock_status, run_cmd
from openpilot.starpilot.common.starpilot_variables import ACTIVE_THEME_PATH, ERROR_LOGS_PATH, EXCLUDED_KEYS, LEGACY_STARPILOT_PARAM_RENAMES, MAPS_PATH, MODELS_PATH, RESOURCES_REPO, SCREEN_RECORDINGS_PATH, STOCK_THEME_PATH, THEME_SAVE_PATH,\
default_ev_tuning_enabled, migrate_cancel_button_controls, update_starpilot_toggles
@@ -1985,6 +1986,7 @@ def write_legacy_param_file(key, value):
_layout_type_overrides = None
_layout_param_metadata = None
_favorite_slot_options = None
def _get_layout_param_metadata():
global _layout_param_metadata
@@ -2014,6 +2016,50 @@ def _get_layout_type_overrides():
}
return _layout_type_overrides
def _get_favorite_slot_options():
global _favorite_slot_options
if _favorite_slot_options is not None:
return _favorite_slot_options
allowed_keys, value_types = _get_param_type_info()
options = []
try:
layout_path = os.path.join(os.path.dirname(__file__), "assets", "components", "tools", "device_settings_layout.json")
with open(layout_path) as f:
layout_data = json.load(f)
seen = set()
for section in layout_data:
section_name = section.get("name", "")
for param_data in section.get("params", []):
key = str(param_data.get("key") or "").strip()
if not key or key in seen:
continue
if key not in allowed_keys or value_types.get(key) is not bool:
continue
if param_data.get("ui_type") != "toggle" or param_data.get("data_type") != "bool":
continue
seen.add(key)
options.append({
"key": key,
"label": str(param_data.get("label") or key),
"description": str(param_data.get("description") or ""),
"section": section_name,
})
except Exception:
options = []
_favorite_slot_options = options
return _favorite_slot_options
def _favorite_slot_values(options):
return {
option["key"]: _safe_params_get_bool(option["key"])
for option in options
if option.get("key")
}
_cached_allowed_keys = None
_cached_param_types = None
_cached_default_values = None
@@ -3433,6 +3479,18 @@ def setup(app):
"last_empty_catalog_log_time": 0.0,
}
@app.after_request
def disable_device_settings_asset_cache(response):
if request.path in {
"/assets/components/router.js",
"/assets/components/tools/device_settings.js",
"/assets/components/tools/device_settings_layout.json",
}:
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
return response
@app.errorhandler(404)
def not_found(_):
response = make_response(render_template("index.html"))
@@ -3728,6 +3786,56 @@ def setup(app):
return jsonify(models), 200
@app.route("/api/favorites/slots", methods=["GET", "PUT"])
def favorite_slots():
options = _get_favorite_slot_options()
option_by_key = {option["key"]: option for option in options}
eligible_keys = set(option_by_key)
if request.method == "PUT":
data = request.get_json() or {}
raw_slots = data.get("slots", data) if isinstance(data, dict) else data
if isinstance(raw_slots, dict):
raw_slots = raw_slots.get("slots", [])
if not isinstance(raw_slots, list):
return jsonify(error="Favorite slots payload must be a list."), 400
for idx, raw_slot in enumerate(raw_slots[:3]):
if not isinstance(raw_slot, dict):
continue
key = str(raw_slot.get("key") or "").strip()
if key and key not in eligible_keys:
return jsonify(error=f"Favorite #{idx + 1} must use a Galaxy-exposed boolean toggle."), 400
slots = normalize_favorite_slots(raw_slots, params=params, eligible_keys=eligible_keys)
for slot in slots:
key = slot.get("key")
if not key:
continue
slot["label"] = option_by_key[key]["label"]
params.put(FAVORITE_SLOTS_PARAM, slots)
update_starpilot_toggles()
return jsonify({
"message": "Favorite slots saved.",
"slots": slots,
"options": options,
"values": _favorite_slot_values(options),
}), 200
slots = normalize_favorite_slots(params.get(FAVORITE_SLOTS_PARAM), params=params, eligible_keys=eligible_keys)
for slot in slots:
key = slot.get("key")
if key in option_by_key:
slot["label"] = option_by_key[key]["label"]
return jsonify({
"slots": slots,
"options": options,
"values": _favorite_slot_values(options),
}), 200
@app.route("/api/params", methods=["GET", "PUT"])
def get_param():
if request.method == "PUT":
@@ -3736,6 +3844,30 @@ def setup(app):
return jsonify({"error": "Missing 'key' or 'value' in request body."}), 400
key = str(data["key"]).strip()
if key.lower() == FAVORITE_SLOTS_PARAM.lower():
key = FAVORITE_SLOTS_PARAM
raw_slots = data["value"]
if isinstance(raw_slots, dict):
raw_slots = raw_slots.get("slots", raw_slots)
if not isinstance(raw_slots, list):
return jsonify({"error": "Favorite slots must be configured with the Favorites editor."}), 400
options = _get_favorite_slot_options()
option_by_key = {option["key"]: option for option in options}
eligible_keys = set(option_by_key)
slots = normalize_favorite_slots(raw_slots, params=params, eligible_keys=eligible_keys)
for slot in slots:
slot_key = slot.get("key")
if slot_key in option_by_key:
slot["label"] = option_by_key[slot_key]["label"]
params.put(FAVORITE_SLOTS_PARAM, slots)
update_starpilot_toggles()
return jsonify({
"message": "Favorite slots saved.",
"updated": {FAVORITE_SLOTS_PARAM: slots},
}), 200
key = {
"model": "Model",
"modelversion": "ModelVersion",
@@ -8,6 +8,9 @@ QMap<int, QString> getWheelFunctionsMap() {
{3, QObject::tr("Pause Steering")},
{7, QObject::tr("Toggle \"Switchback Mode\" On/Off")},
{8, QObject::tr("Create Bookmark")},
{11, QObject::tr("Favorite #1")},
{12, QObject::tr("Favorite #2")},
{13, QObject::tr("Favorite #3")},
};
}
@@ -35,6 +38,9 @@ QMap<int, QString> getMainCruiseFunctionsMap() {
{0, QObject::tr("No Action")},
{9, QObject::tr("Toggle Always On Lateral")},
{10, QObject::tr("Adopt Current Speed Limit")},
{11, QObject::tr("Favorite #1")},
{12, QObject::tr("Favorite #2")},
{13, QObject::tr("Favorite #3")},
};
}
+140
View File
@@ -1,5 +1,26 @@
#include "starpilot/ui/qt/onroad/starpilot_buttons.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QPainter>
#include <QPainterPath>
namespace {
constexpr int favorite_btn_size = btn_size;
constexpr int favorite_indicator_size = 22;
constexpr int favorite_slots_count = 3;
QJsonArray parseFavoriteSlots(const std::string &raw_slots) {
QJsonParseError error;
const QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(raw_slots), &error);
if (error.error != QJsonParseError::NoError || !doc.isArray()) {
return QJsonArray();
}
return doc.array();
}
} // namespace
DrivingPersonalityButton::DrivingPersonalityButton(QWidget *parent) : QPushButton(parent) {
setFixedSize(btn_size + UI_BORDER_SIZE, btn_size);
@@ -74,3 +95,122 @@ void DrivingPersonalityButton::paintEvent(QPaintEvent *event) {
drawIcon(p, rect().center() + QPoint(UI_BORDER_SIZE / 2, 0), currentGif ? currentGif->currentPixmap() : currentImg, Qt::transparent, 1.0);
}
FavoriteButton::FavoriteButton(int slot_index, QWidget *parent) : QPushButton(parent), slot_index(slot_index) {
setFixedSize(favorite_btn_size, favorite_btn_size);
QObject::connect(this, &QPushButton::clicked, this, &FavoriteButton::toggleFavorite);
}
FavoriteSlotState FavoriteButton::currentSlot() {
FavoriteSlotState next_slot;
if (slot_index < 0 || slot_index >= favorite_slots_count) {
return next_slot;
}
const QJsonArray favorite_slots = parseFavoriteSlots(params.get("StarPilotFavoriteSlots"));
if (slot_index >= favorite_slots.size() || !favorite_slots.at(slot_index).isObject()) {
return next_slot;
}
const QJsonObject slot_obj = favorite_slots.at(slot_index).toObject();
next_slot.enabled = slot_obj.value("enabled").toBool(false);
next_slot.show_onroad = slot_obj.value("show_onroad").toBool(false);
next_slot.key = slot_obj.value("key").toString().trimmed();
next_slot.label = slot_obj.value("label").toString().trimmed();
if (next_slot.key.isEmpty() || !params.checkKey(next_slot.key.toStdString()) || params.getKeyType(next_slot.key.toStdString()) != ParamKeyType::BOOL) {
next_slot.enabled = false;
next_slot.show_onroad = false;
next_slot.key.clear();
next_slot.label.clear();
return next_slot;
}
if (next_slot.label.isEmpty()) {
next_slot.label = next_slot.key;
}
next_slot.value = params.getBool(next_slot.key.toStdString());
return next_slot;
}
void FavoriteButton::updateState() {
const FavoriteSlotState next_slot = currentSlot();
const bool changed = next_slot.enabled != slot.enabled ||
next_slot.show_onroad != slot.show_onroad ||
next_slot.value != slot.value ||
next_slot.key != slot.key ||
next_slot.label != slot.label;
slot = next_slot;
setVisible(shouldShow());
if (changed) {
update();
}
}
bool FavoriteButton::shouldShow() const {
return slot.enabled && slot.show_onroad && !slot.key.isEmpty();
}
void FavoriteButton::toggleFavorite() {
slot = currentSlot();
if (!slot.enabled || slot.key.isEmpty()) {
return;
}
const bool next_value = !params.getBool(slot.key.toStdString());
params.putBool(slot.key.toStdString(), next_value);
params_memory.putBool("StarPilotTogglesUpdated", true);
slot.value = next_value;
update();
}
QFont FavoriteButton::fittedLabelFont(QPainter &p, const QString &label, const QRect &text_rect) const {
QFont font = p.font();
font.setWeight(QFont::DemiBold);
font.setLetterSpacing(QFont::AbsoluteSpacing, 0);
for (int font_size = 28; font_size >= 18; --font_size) {
font.setPixelSize(font_size);
const QFontMetrics metrics(font);
const QRect bounds = metrics.boundingRect(text_rect, Qt::AlignCenter | Qt::TextWordWrap, label);
if (bounds.width() <= text_rect.width() && bounds.height() <= text_rect.height()) {
return font;
}
}
font.setPixelSize(18);
return font;
}
void FavoriteButton::paintEvent(QPaintEvent *event) {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
const QRectF bg_rect = rect().adjusted(0, 0, -1, -1);
QPainterPath bg_path;
bg_path.addRoundedRect(bg_rect, 12, 12);
p.fillPath(bg_path, QColor(0, 0, 0, isDown() ? 190 : 166));
p.setPen(QPen(QColor(255, 255, 255, isDown() ? 130 : 95), 2));
p.drawPath(bg_path);
const QColor indicator_color = slot.value ? QColor(48, 255, 156) : QColor(135, 135, 135);
const int indicator_x = width() - 20 - favorite_indicator_size;
const int indicator_y = 20;
p.setPen(Qt::NoPen);
p.setBrush(indicator_color);
p.drawEllipse(indicator_x, indicator_y, favorite_indicator_size, favorite_indicator_size);
QFont slot_font = p.font();
slot_font.setPixelSize(20);
slot_font.setWeight(QFont::DemiBold);
slot_font.setLetterSpacing(QFont::AbsoluteSpacing, 0);
p.setFont(slot_font);
p.setPen(QColor(255, 255, 255, 175));
p.drawText(QRect(18, 14, 54, 32), Qt::AlignLeft | Qt::AlignVCenter, QString("#%1").arg(slot_index + 1));
const QString label = slot.label.isEmpty() ? QStringLiteral("Favorite") : slot.label;
const QRect text_rect(14, 50, width() - 28, height() - 64);
p.setFont(fittedLabelFont(p, label, text_rect));
p.setPen(QColor(255, 255, 255, 245));
p.drawText(text_rect, Qt::AlignCenter | Qt::TextWordWrap, label);
}
@@ -1,7 +1,17 @@
#pragma once
#include <array>
#include "selfdrive/ui/qt/onroad/buttons.h"
struct FavoriteSlotState {
bool enabled = false;
bool show_onroad = false;
bool value = false;
QString key;
QString label;
};
class DrivingPersonalityButton : public QPushButton {
Q_OBJECT
@@ -28,3 +38,24 @@ private:
QPixmap currentImg;
};
class FavoriteButton : public QPushButton {
public:
explicit FavoriteButton(int slot_index, QWidget *parent = 0);
void updateState();
bool shouldShow() const;
private:
void paintEvent(QPaintEvent *event) override;
void toggleFavorite();
FavoriteSlotState currentSlot();
QFont fittedLabelFont(QPainter &p, const QString &label, const QRect &text_rect) const;
int slot_index;
FavoriteSlotState slot;
Params params;
Params params_memory{"", true};
};