mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-06-27 17:42:04 +08:00
Raindrops on Roses & Whiskers on Kittens
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ public:
|
||||
|
||||
StarPilotAnnotatedCameraWidget *starpilot_nvg;
|
||||
|
||||
bool onroad_distance_btn_enabled;
|
||||
int onroad_controls_width = 0;
|
||||
|
||||
QJsonObject starpilot_toggles;
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 (0–24 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 (25–34 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 (35–44 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 (45–54 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 (55–64 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 (65–74 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 (75–99 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");
|
||||
|
||||
@@ -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")},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user