mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-09-18 10:23:43 +08:00
ui: add descriptions for settings (#38867)
This commit is contained in:
@@ -23,8 +23,6 @@ class AlphaLongConfirmPage(NavScroller):
|
||||
GreyBigButton("", "WARNING: alpha longitudinal control may disable Automatic Emergency Braking (AEB)"),
|
||||
GreyBigButton("", "On this car, openpilot defaults to the stock system's built-in ACC."),
|
||||
GreyBigButton("", "Enabling this will switch to openpilot longitudinal control."),
|
||||
GreyBigButton("", "Using Experimental mode is recommended with openpilot longitudinal control alpha."),
|
||||
GreyBigButton("", "Changing this setting will restart openpilot if the car is powered on."),
|
||||
accept,
|
||||
])
|
||||
|
||||
@@ -63,28 +61,31 @@ class DeveloperLayoutMici(NavScroller):
|
||||
|
||||
txt_ssh = gui_app.texture("icons_mici/settings/developer/ssh.png", 56, 64)
|
||||
github_username = ui_state.params.get("GithubUsername") or ""
|
||||
self._ssh_keys_btn = BigButton("SSH keys", "Not set" if not github_username else github_username, icon=txt_ssh)
|
||||
self._ssh_keys_btn = BigButton("SSH keys", "Not set" if not github_username else github_username, icon=txt_ssh,
|
||||
description="Grant SSH access to all public keys in your GitHub settings. Only enter your own username.")
|
||||
self._ssh_keys_btn.set_click_callback(ssh_keys_callback)
|
||||
|
||||
# adb, ssh, ssh keys, debug mode, joystick debug mode, longitudinal maneuver mode, ip address
|
||||
# ******** Main Scroller ********
|
||||
self._adb_toggle = BigCircleParamControl(gui_app.texture("icons_mici/adb_short.png", 82, 82), "AdbEnabled", icon_offset=(0, 12))
|
||||
self._ssh_toggle = BigCircleParamControl(gui_app.texture("icons_mici/ssh_short.png", 82, 82), "SshEnabled", icon_offset=(0, 12))
|
||||
self._joystick_toggle = BigToggle("joystick debug mode",
|
||||
initial_state=ui_state.params.get_bool("JoystickDebugMode"),
|
||||
toggle_callback=self._on_joystick_debug_mode)
|
||||
self._long_maneuver_toggle = BigToggle("longitudinal maneuver mode",
|
||||
initial_state=ui_state.params.get_bool("LongitudinalManeuverMode"),
|
||||
toggle_callback=self._on_long_maneuver_mode)
|
||||
self._lat_maneuver_toggle = BigToggle("lateral maneuver mode",
|
||||
initial_state=ui_state.params.get_bool("LateralManeuverMode"),
|
||||
toggle_callback=self._on_lat_maneuver_mode)
|
||||
self._alpha_long_toggle = BigToggle("alpha longitudinal",
|
||||
initial_state=ui_state.params.get_bool("AlphaLongitudinalEnabled"),
|
||||
toggle_callback=self._on_alpha_long_enabled)
|
||||
self._adb_toggle = BigCircleParamControl(gui_app.texture("icons_mici/adb_short.png", 82, 82), "AdbEnabled", icon_offset=(0, 12),
|
||||
description="Use Android Debug Bridge (ADB) over USB or the network.", title="enable ADB")
|
||||
self._ssh_toggle = BigCircleParamControl(gui_app.texture("icons_mici/ssh_short.png", 82, 82), "SshEnabled", icon_offset=(0, 12),
|
||||
description="Access the device remotely using your SSH keys.", title="enable SSH")
|
||||
self._joystick_toggle = BigToggle("joystick debug\nmode", initial_state=ui_state.params.get_bool("JoystickDebugMode"),
|
||||
toggle_callback=self._on_joystick_debug_mode, description="Control the car with a joystick for debugging.")
|
||||
self._long_maneuver_toggle = BigToggle("longitudinal maneuver mode", initial_state=ui_state.params.get_bool("LongitudinalManeuverMode"),
|
||||
toggle_callback=self._on_long_maneuver_mode,
|
||||
description="Run longitudinal maneuvers for testing gas and brake control.")
|
||||
self._lat_maneuver_toggle = BigToggle("lateral maneuver mode", initial_state=ui_state.params.get_bool("LateralManeuverMode"),
|
||||
toggle_callback=self._on_lat_maneuver_mode,
|
||||
description="Run lateral maneuvers for testing steering control.")
|
||||
self._alpha_long_toggle = BigToggle("alpha longitudinal", initial_state=ui_state.params.get_bool("AlphaLongitudinalEnabled"),
|
||||
toggle_callback=self._on_alpha_long_enabled,
|
||||
description="Use alpha openpilot longitudinal control instead of stock ACC. This may disable Automatic Emergency " +
|
||||
"Braking (AEB).")
|
||||
self._debug_mode_toggle = BigParamControl("ui debug mode", "ShowDebugInfo",
|
||||
toggle_callback=lambda checked: (gui_app.set_show_touches(checked),
|
||||
gui_app.set_show_fps(checked)))
|
||||
toggle_callback=lambda checked: (gui_app.set_show_touches(checked), gui_app.set_show_fps(checked)),
|
||||
description="Show touch locations and the UI frame rate.")
|
||||
|
||||
self._scroller.add_widgets([
|
||||
self._adb_toggle,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
from typing import Union
|
||||
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.params import Params
|
||||
@@ -77,15 +78,16 @@ def _engaged_confirmation_click(callback: Callable, action_text: str, icon: rl.T
|
||||
|
||||
class EngagedConfirmationCircleButton(BigCircleButton):
|
||||
def __init__(self, title: str, icon: rl.Texture, callback: Callable[[], None], exit_on_confirm: bool = True,
|
||||
red: bool = False, icon_offset: tuple[int, int] = (0, 0)):
|
||||
super().__init__(icon, red, icon_offset)
|
||||
red: bool = False, icon_offset: tuple[int, int] = (0, 0), *, description: str = ""):
|
||||
super().__init__(icon, red, icon_offset, description=description, title=title)
|
||||
self.set_click_callback(lambda: _engaged_confirmation_click(callback, title, icon, exit_on_confirm=exit_on_confirm, red=red))
|
||||
|
||||
|
||||
class EngagedConfirmationButton(BigButton):
|
||||
def __init__(self, text: str, action_text: str, icon: rl.Texture, callback: Callable[[], None],
|
||||
exit_on_confirm: bool = True, red: bool = False):
|
||||
super().__init__(text, "", icon)
|
||||
exit_on_confirm: bool = True, red: bool = False, *, description: str = "",
|
||||
description_icon: Union[rl.Texture, None] = None):
|
||||
super().__init__(text, "", icon, description=description, description_icon=description_icon)
|
||||
self.set_click_callback(lambda: _engaged_confirmation_click(callback, action_text, icon, exit_on_confirm=exit_on_confirm, red=red))
|
||||
|
||||
|
||||
@@ -177,7 +179,9 @@ class DeviceLayoutMici(NavScroller):
|
||||
params.put_bool("OnroadCycleRequested", True, block=True)
|
||||
|
||||
reset_calibration_btn = EngagedConfirmationButton("reset calibration", "reset", gui_app.texture("icons_mici/settings/device/lkas.png", 122, 64),
|
||||
reset_calibration_callback)
|
||||
reset_calibration_callback,
|
||||
description="Mount the device within 4° left or right and 5° up or 9° down. openpilot calibrates " +
|
||||
"continuously; resetting is rarely needed. Resetting clears learned calibration.")
|
||||
|
||||
reboot_btn = EngagedConfirmationCircleButton("reboot", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70),
|
||||
reboot_callback, exit_on_confirm=False)
|
||||
@@ -189,7 +193,8 @@ class DeviceLayoutMici(NavScroller):
|
||||
regulatory_btn = BigButton("regulatory info", "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64))
|
||||
regulatory_btn.set_click_callback(self._on_regulatory)
|
||||
|
||||
cabin_cam_btn = BigButton("driver\ncamera preview", "", gui_app.texture("icons_mici/settings/device/cameras.png", 64, 64))
|
||||
cabin_cam_btn = BigButton("driver\ncamera preview", "", gui_app.texture("icons_mici/settings/device/cameras.png", 64, 64),
|
||||
description="Preview the cabin camera to check driver monitoring visibility. The vehicle must be off.")
|
||||
cabin_cam_btn.set_click_callback(lambda: gui_app.push_widget(CabinCameraDialog()))
|
||||
cabin_cam_btn.set_enabled(lambda: ui_state.is_offroad())
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ NetworkType = log.DeviceState.NetworkType
|
||||
|
||||
|
||||
class EsimNetworkButton(BigButton):
|
||||
def __init__(self, cellular_manager: CellularManager):
|
||||
def __init__(self, cellular_manager: CellularManager, *, description: str = ""):
|
||||
self._cellular_manager = cellular_manager
|
||||
self._cell_icons = {
|
||||
NetworkStrength.unknown: gui_app.texture("icons_mici/settings/network/cell_strength_none.png", 64, 47),
|
||||
@@ -22,7 +22,7 @@ class EsimNetworkButton(BigButton):
|
||||
NetworkStrength.good: gui_app.texture("icons_mici/settings/network/cell_strength_high.png", 64, 47),
|
||||
NetworkStrength.great: gui_app.texture("icons_mici/settings/network/cell_strength_full.png", 64, 47),
|
||||
}
|
||||
super().__init__("esim", "loading...", self._cell_icons[NetworkStrength.unknown], scroll=True)
|
||||
super().__init__("esim", "loading...", self._cell_icons[NetworkStrength.unknown], scroll=True, description=description)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
@@ -54,7 +54,7 @@ class EsimNetworkButton(BigButton):
|
||||
|
||||
|
||||
class WifiNetworkButton(BigButton):
|
||||
def __init__(self, wifi_manager: WifiManager):
|
||||
def __init__(self, wifi_manager: WifiManager, *, description: str = ""):
|
||||
self._wifi_manager = wifi_manager
|
||||
self._lock_txt = gui_app.texture("icons_mici/settings/network/new/lock.png", 28, 36)
|
||||
self._draw_lock = False
|
||||
@@ -64,7 +64,7 @@ class WifiNetworkButton(BigButton):
|
||||
self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 64, 47)
|
||||
self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 64, 47)
|
||||
|
||||
super().__init__("wi-fi", "not connected", self._wifi_slash_txt, scroll=True)
|
||||
super().__init__("wi-fi", "not connected", self._wifi_slash_txt, scroll=True, description=description)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
@@ -29,7 +29,8 @@ class NetworkLayoutMici(NavScroller):
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
self._wifi_manager.set_tethering_active(checked)
|
||||
|
||||
self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback)
|
||||
self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback,
|
||||
description="Share the device’s internet connection through a Wi-Fi hotspot.")
|
||||
|
||||
def tethering_password_callback(password: str):
|
||||
if password:
|
||||
@@ -59,7 +60,9 @@ class NetworkLayoutMici(NavScroller):
|
||||
|
||||
# TODO: signal for current network metered type when changing networks, this is wrong until you press it once
|
||||
# TODO: disable when not connected
|
||||
self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback)
|
||||
self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback,
|
||||
description="Metered prevents large uploads on this Wi-Fi connection. Default uses the network’s detected " +
|
||||
"setting.")
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
|
||||
self._wifi_button = WifiNetworkButton(self._wifi_manager)
|
||||
@@ -76,14 +79,15 @@ class NetworkLayoutMici(NavScroller):
|
||||
|
||||
# ******** Advanced settings ********
|
||||
# ******** Roaming toggle ********
|
||||
self._roaming_btn = BigParamControl("enable roaming", "GsmRoaming")
|
||||
self._roaming_btn = BigParamControl("enable roaming", "GsmRoaming", description="Allow cellular data roaming.")
|
||||
|
||||
# ******** APN settings ********
|
||||
self._apn_btn = BigButton("apn settings", "edit")
|
||||
self._apn_btn = BigButton("apn settings", "edit",
|
||||
description="Set the access point name required by your cellular carrier. Leave blank for automatic configuration.")
|
||||
self._apn_btn.set_click_callback(self._edit_apn)
|
||||
|
||||
# ******** Cellular metered toggle ********
|
||||
self._cellular_metered_btn = BigParamControl("cellular metered", "GsmMetered")
|
||||
self._cellular_metered_btn = BigParamControl("cellular metered", "GsmMetered", description="Prevent large uploads over the cellular connection.")
|
||||
|
||||
# Main scroller ----------------------------------
|
||||
self._scroller.add_widgets([
|
||||
|
||||
@@ -242,7 +242,8 @@ class BranchSelectPage(NavScroller):
|
||||
|
||||
class TargetBranchButton(BigButton):
|
||||
def __init__(self, check_update_btn: CheckUpdateButton):
|
||||
super().__init__("target branch", ui_state.params.get("UpdaterTargetBranch") or "")
|
||||
super().__init__("target branch", ui_state.params.get("UpdaterTargetBranch") or "",
|
||||
description="Select the software branch to download on the next update check.")
|
||||
self._check_update_btn = check_update_btn
|
||||
self.set_click_callback(self._on_click)
|
||||
self.set_visible(not ui_state.params.get_bool("IsTestedBranch"))
|
||||
@@ -276,7 +277,9 @@ class SoftwareLayoutMici(NavScroller):
|
||||
|
||||
uninstall_openpilot_btn = EngagedConfirmationButton("uninstall openpilot", "uninstall",
|
||||
gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64),
|
||||
uninstall_openpilot_callback, exit_on_confirm=False)
|
||||
uninstall_openpilot_callback, exit_on_confirm=False,
|
||||
description="Remove openpilot from this device.",
|
||||
description_icon=gui_app.texture("icons_mici/setup/factory_reset.png", 64, 64))
|
||||
|
||||
check_update_btn = CheckUpdateButton()
|
||||
self._scroller.add_widgets([
|
||||
|
||||
@@ -41,15 +41,33 @@ class TogglesLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._personality_toggle = BigMultiParamToggle("driving personality", "LongitudinalPersonality", ["aggressive", "standard", "relaxed"])
|
||||
self._experimental_btn = BigToggle("experimental mode", initial_state=ui_state.params.get_bool("ExperimentalMode"),
|
||||
toggle_callback=self._on_experimental_mode)
|
||||
self._personality_toggle = BigMultiParamToggle("driving personality", "LongitudinalPersonality", ["aggressive", "standard", "relaxed"],
|
||||
description="Standard is recommended.\n" +
|
||||
"Aggressive follows closer, with firmer gas and braking.\n" +
|
||||
"Relaxed leaves more space.\n" +
|
||||
"Use the steering wheel distance button on supported cars.")
|
||||
self._experimental_btn = BigToggle("experimental mode", description_icon=gui_app.texture("icons_mici/experimental_mode.png", 64, 64),
|
||||
initial_state=ui_state.params.get_bool("ExperimentalMode"), toggle_callback=self._on_experimental_mode,
|
||||
description="Let the driving model control gas and brakes.\n" +
|
||||
"Includes stopping for red lights and stop signs.\n" +
|
||||
"Set speed is a maximum, not a target.\n" +
|
||||
"These are alpha features. Expect mistakes.\n" +
|
||||
"The path colors show acceleration and braking.")
|
||||
is_metric_toggle = BigParamControl("use metric units", "IsMetric")
|
||||
ldw_toggle = BigParamControl("lane departure warnings", "IsLdwEnabled")
|
||||
always_on_dm_toggle = BigParamControl("always-on driver monitor", "AlwaysOnDM")
|
||||
record_front = BigParamControl("record & upload cabin camera", "RecordFront", toggle_callback=restart_needed_callback)
|
||||
record_mic = BigParamControl("record & upload mic audio", "RecordAudio", toggle_callback=restart_needed_callback)
|
||||
enable_openpilot = BigParamControl("enable openpilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback)
|
||||
ldw_toggle = BigParamControl("lane departure warnings", "IsLdwEnabled",
|
||||
description="Warn when you drift across a detected lane line.\n" +
|
||||
"Only above 31 mph (50 km/h), with no turn signal.")
|
||||
always_on_dm_toggle = BigParamControl("always-on driver monitor", "AlwaysOnDM", description="Monitor the driver even when openpilot is not engaged.")
|
||||
record_front = BigParamControl("record & upload cabin camera", "RecordFront",
|
||||
description_icon=gui_app.texture("icons_mici/settings/device/cameras.png", 64, 64),
|
||||
toggle_callback=restart_needed_callback, description="Upload cabin camera data to help improve driver monitoring.")
|
||||
record_mic = BigParamControl("record & upload mic audio", "RecordAudio", description_icon=gui_app.texture("icons_mici/microphone.png", 64, 64),
|
||||
toggle_callback=restart_needed_callback,
|
||||
description="Record microphone audio while driving.\n" +
|
||||
"Audio is included in dashcam videos in comma connect.")
|
||||
enable_openpilot = BigParamControl("enable openpilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback,
|
||||
description="Enable to use openpilot driver assistance.\n" +
|
||||
"Disable to use your car's stock driver assistance.")
|
||||
|
||||
self._scroller.add_widgets([
|
||||
self._personality_toggle,
|
||||
|
||||
@@ -29,9 +29,41 @@ class ScrollState(Enum):
|
||||
POST_SCROLL = 2
|
||||
|
||||
|
||||
class BigCircleButton(Widget):
|
||||
def __init__(self, icon: rl.Texture, red: bool = False, icon_offset: tuple[int, int] = (0, 0)):
|
||||
class BaseButton(Widget):
|
||||
def __init__(self, description: str, title: str, icon: Union[rl.Texture, None] = None):
|
||||
super().__init__()
|
||||
self._shake_start: float | None = None
|
||||
if description:
|
||||
# Dialogs also use buttons; import lazily to avoid a circular import.
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import SettingDescriptionDialog
|
||||
self.set_long_press_callback(lambda: gui_app.push_widget(SettingDescriptionDialog(title, description, icon)))
|
||||
else:
|
||||
self.set_long_press_callback(self.trigger_shake)
|
||||
|
||||
def trigger_shake(self):
|
||||
self._shake_start = rl.get_time()
|
||||
|
||||
@property
|
||||
def _shake_offset(self) -> float:
|
||||
SHAKE_DURATION = 0.5
|
||||
SHAKE_AMPLITUDE = 24.0
|
||||
SHAKE_FREQUENCY = 32.0
|
||||
if self._shake_start is None:
|
||||
return 0.0
|
||||
t = rl.get_time() - self._shake_start
|
||||
if t > SHAKE_DURATION:
|
||||
return 0.0
|
||||
decay = 1.0 - t / SHAKE_DURATION
|
||||
return decay * SHAKE_AMPLITUDE * math.sin(t * SHAKE_FREQUENCY)
|
||||
|
||||
def set_position(self, x: float, y: float) -> None:
|
||||
super().set_position(x + self._shake_offset, y)
|
||||
|
||||
class BigCircleButton(BaseButton):
|
||||
def __init__(self, icon: rl.Texture, red: bool = False, icon_offset: tuple[int, int] = (0, 0),
|
||||
*, description: str = "",
|
||||
description_icon: Union[rl.Texture, None] = None, title: str = ""):
|
||||
super().__init__(description, title, description_icon or icon)
|
||||
self._red = red
|
||||
self._icon_offset = icon_offset
|
||||
|
||||
@@ -73,8 +105,9 @@ class BigCircleButton(Widget):
|
||||
|
||||
|
||||
class BigCircleToggle(BigCircleButton):
|
||||
def __init__(self, icon: rl.Texture, toggle_callback: Callable | None = None, icon_offset: tuple[int, int] = (0, 0)):
|
||||
super().__init__(icon, False, icon_offset=icon_offset)
|
||||
def __init__(self, icon: rl.Texture, toggle_callback: Callable | None = None, icon_offset: tuple[int, int] = (0, 0),
|
||||
*, description: str = "", description_icon: Union[rl.Texture, None] = None, title: str = ""):
|
||||
super().__init__(icon, False, icon_offset=icon_offset, description=description, description_icon=description_icon, title=title)
|
||||
self._toggle_callback = toggle_callback
|
||||
|
||||
# State
|
||||
@@ -103,14 +136,16 @@ class BigCircleToggle(BigCircleButton):
|
||||
0, 1.0, rl.WHITE)
|
||||
|
||||
|
||||
class BigButton(Widget):
|
||||
class BigButton(BaseButton):
|
||||
LABEL_HORIZONTAL_PADDING = 40
|
||||
LABEL_VERTICAL_PADDING = 23 # visually matches 30 in figma
|
||||
|
||||
"""A lightweight stand-in for the Qt BigButton, drawn & updated each frame."""
|
||||
|
||||
def __init__(self, text: str, value: str = "", icon: Union[rl.Texture, None] = None, scroll: bool = False):
|
||||
super().__init__()
|
||||
def __init__(self, text: str, value: str = "", icon: Union[rl.Texture, None] = None, scroll: bool = False,
|
||||
*, description: str = "",
|
||||
description_icon: Union[rl.Texture, None] = None):
|
||||
super().__init__(description, text, description_icon or icon or None)
|
||||
self.set_rect(rl.Rectangle(0, 0, 402, 180))
|
||||
self.text = text
|
||||
self.value = value
|
||||
@@ -119,7 +154,6 @@ class BigButton(Widget):
|
||||
|
||||
self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps)
|
||||
self._click_delay = 0.075
|
||||
self._shake_start: float | None = None
|
||||
self._grow_animation_until: float | None = None
|
||||
|
||||
self._rotate_icon_t: float | None = None
|
||||
@@ -187,28 +221,9 @@ class BigButton(Widget):
|
||||
def get_text(self):
|
||||
return self.text
|
||||
|
||||
def trigger_shake(self):
|
||||
self._shake_start = rl.get_time()
|
||||
|
||||
def trigger_grow_animation(self, duration: float = 0.65):
|
||||
self._grow_animation_until = rl.get_time() + duration
|
||||
|
||||
@property
|
||||
def _shake_offset(self) -> float:
|
||||
SHAKE_DURATION = 0.5
|
||||
SHAKE_AMPLITUDE = 24.0
|
||||
SHAKE_FREQUENCY = 32.0
|
||||
if self._shake_start is None:
|
||||
return 0.0
|
||||
t = rl.get_time() - self._shake_start
|
||||
if t > SHAKE_DURATION:
|
||||
return 0.0
|
||||
decay = 1.0 - t / SHAKE_DURATION
|
||||
return decay * SHAKE_AMPLITUDE * math.sin(t * SHAKE_FREQUENCY)
|
||||
|
||||
def set_position(self, x: float, y: float) -> None:
|
||||
super().set_position(x + self._shake_offset, y)
|
||||
|
||||
def _handle_background(self) -> tuple[rl.Texture, float, float, float]:
|
||||
if self._grow_animation_until is not None:
|
||||
if rl.get_time() >= self._grow_animation_until:
|
||||
@@ -272,8 +287,10 @@ class BigButton(Widget):
|
||||
|
||||
|
||||
class BigToggle(BigButton):
|
||||
def __init__(self, text: str, value: str = "", initial_state: bool = False, toggle_callback: Callable | None = None):
|
||||
super().__init__(text, value, "")
|
||||
def __init__(self, text: str, value: str = "", initial_state: bool = False, toggle_callback: Callable | None = None,
|
||||
*, description: str = "",
|
||||
description_icon: Union[rl.Texture, None] = None):
|
||||
super().__init__(text, value, "", description=description, description_icon=description_icon)
|
||||
self._checked = initial_state
|
||||
self._toggle_callback = toggle_callback
|
||||
|
||||
@@ -308,8 +325,8 @@ class BigToggle(BigButton):
|
||||
|
||||
class BigMultiToggle(BigToggle):
|
||||
def __init__(self, text: str, options: list[str], toggle_callback: Callable | None = None,
|
||||
select_callback: Callable | None = None):
|
||||
super().__init__(text, "", toggle_callback=toggle_callback)
|
||||
select_callback: Callable | None = None, *, description: str = "", description_icon: Union[rl.Texture, None] = None):
|
||||
super().__init__(text, "", toggle_callback=toggle_callback, description=description, description_icon=description_icon)
|
||||
assert len(options) > 0
|
||||
self._options = options
|
||||
self._select_callback = select_callback
|
||||
@@ -374,9 +391,9 @@ class GreyBigButton(BigButton):
|
||||
|
||||
class BigMultiParamToggle(BigMultiToggle):
|
||||
def __init__(self, text: str, param: str, options: list[str], toggle_callback: Callable | None = None,
|
||||
select_callback: Callable | None = None):
|
||||
select_callback: Callable | None = None, *, description: str = "", description_icon: Union[rl.Texture, None] = None):
|
||||
assert Params is not None
|
||||
super().__init__(text, options, toggle_callback, select_callback)
|
||||
super().__init__(text, options, toggle_callback, select_callback, description=description, description_icon=description_icon)
|
||||
self._param = param
|
||||
|
||||
self._params = Params()
|
||||
@@ -392,9 +409,10 @@ class BigMultiParamToggle(BigMultiToggle):
|
||||
|
||||
|
||||
class BigParamControl(BigToggle):
|
||||
def __init__(self, text: str, param: str, toggle_callback: Callable | None = None):
|
||||
def __init__(self, text: str, param: str, toggle_callback: Callable | None = None, *, description: str = "",
|
||||
description_icon: Union[rl.Texture, None] = None):
|
||||
assert Params is not None
|
||||
super().__init__(text, "", toggle_callback=toggle_callback)
|
||||
super().__init__(text, "", toggle_callback=toggle_callback, description=description, description_icon=description_icon)
|
||||
self.param = param
|
||||
self.params = Params()
|
||||
self.set_checked(self.params.get_bool(self.param, False))
|
||||
@@ -410,9 +428,9 @@ class BigParamControl(BigToggle):
|
||||
# TODO: param control base class
|
||||
class BigCircleParamControl(BigCircleToggle):
|
||||
def __init__(self, icon: rl.Texture, param: str, toggle_callback: Callable | None = None,
|
||||
icon_offset: tuple[int, int] = (0, 0)):
|
||||
icon_offset: tuple[int, int] = (0, 0), *, description: str = "", description_icon: Union[rl.Texture, None] = None, title: str = ""):
|
||||
assert Params is not None
|
||||
super().__init__(icon, toggle_callback, icon_offset=icon_offset)
|
||||
super().__init__(icon, toggle_callback, icon_offset=icon_offset, description=description, description_icon=description_icon, title=title)
|
||||
self._param = param
|
||||
self.params = Params()
|
||||
self.set_checked(self.params.get_bool(self._param, False))
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import abc
|
||||
import math
|
||||
import re
|
||||
import pyray as rl
|
||||
from typing import Union
|
||||
from collections.abc import Callable
|
||||
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.mici_keyboard import MiciKeyboard
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
@@ -37,6 +39,31 @@ class BigDialog(BigDialogBase):
|
||||
))
|
||||
|
||||
|
||||
class SettingDescriptionDialog(NavScroller):
|
||||
def __init__(self, title: str, description: str, icon: Union[rl.Texture, None] = None):
|
||||
super().__init__()
|
||||
cards = [GreyBigButton(title, "scroll for details", icon or gui_app.texture("icons_mici/setup/green_info.png", 64, 64))]
|
||||
# Explicit lines are authored cards; otherwise prefer sentence boundaries.
|
||||
paragraphs = description.splitlines() if "\n" in description else re.split(r"(?<=[.!?])\s+", description.strip())
|
||||
# Measure each card so longer text still fits with the actual font and padding.
|
||||
for sentence in paragraphs:
|
||||
card = GreyBigButton("", "")
|
||||
words: list[str] = []
|
||||
for word in sentence.split():
|
||||
card.set_value(" ".join([*words, word]))
|
||||
height = card._sub_label.get_content_height(card._subtitle_width_hint())
|
||||
if words and height > card.rect.height - 2 * card.LABEL_VERTICAL_PADDING:
|
||||
card.set_value(" ".join(words))
|
||||
cards.append(card)
|
||||
card = GreyBigButton("", "")
|
||||
words = []
|
||||
words.append(word)
|
||||
if words:
|
||||
card.set_value(" ".join(words))
|
||||
cards.append(card)
|
||||
self._scroller.add_widgets(cards)
|
||||
|
||||
|
||||
class BigConfirmationDialog(BigDialogBase):
|
||||
def __init__(self, title: str, icon: rl.Texture, confirm_callback: Callable[[], None],
|
||||
exit_on_confirm: bool = True, red: bool = False):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import time
|
||||
import pyray as rl
|
||||
from enum import IntEnum
|
||||
from typing import Protocol, TypeVar
|
||||
@@ -51,6 +52,8 @@ class Widget(abc.ABC):
|
||||
self._click_delay: float | None = None # seconds to hold is_pressed after release
|
||||
self._click_release_time: float | None = None
|
||||
self._click_callback: Callable[[], None] | None = None
|
||||
self._long_press_callback: Callable[[], None] | None = None
|
||||
self._press_started: list[float | None] = [None] * MAX_TOUCH_SLOTS
|
||||
self._multi_touch = False
|
||||
self.__was_awake = True
|
||||
|
||||
@@ -92,6 +95,9 @@ class Widget(abc.ABC):
|
||||
"""Set a callback to be called when the widget is clicked."""
|
||||
self._click_callback = click_callback
|
||||
|
||||
def set_long_press_callback(self, callback: Callable[[], None]) -> None:
|
||||
self._long_press_callback = callback
|
||||
|
||||
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
|
||||
"""Set a callback to determine if the widget can be clicked."""
|
||||
self._touch_valid_callback = touch_callback
|
||||
@@ -136,6 +142,7 @@ class Widget(abc.ABC):
|
||||
self._process_mouse_events()
|
||||
else:
|
||||
# TODO: ideally we emit release events when going disabled
|
||||
self._press_started = [None] * MAX_TOUCH_SLOTS
|
||||
self.__is_pressed = [False] * MAX_TOUCH_SLOTS
|
||||
self.__tracking_is_pressed = [False] * MAX_TOUCH_SLOTS
|
||||
|
||||
@@ -160,6 +167,7 @@ class Widget(abc.ABC):
|
||||
# Allows touch to leave the rect and come back in focus if mouse did not release
|
||||
if mouse_event.left_pressed and touch_valid:
|
||||
if mouse_in_rect:
|
||||
self._press_started[mouse_event.slot] = mouse_event.t
|
||||
self._handle_mouse_press(mouse_event.pos)
|
||||
self.__is_pressed[mouse_event.slot] = True
|
||||
self.__tracking_is_pressed[mouse_event.slot] = True
|
||||
@@ -185,9 +193,20 @@ class Widget(abc.ABC):
|
||||
|
||||
# Mouse/touch left our rect but may come back into focus later
|
||||
elif not mouse_in_rect:
|
||||
self._press_started[mouse_event.slot] = None
|
||||
self.__is_pressed[mouse_event.slot] = False
|
||||
self._handle_mouse_event(mouse_event)
|
||||
|
||||
if self._long_press_callback is not None and touch_valid:
|
||||
for slot, started in enumerate(self._press_started):
|
||||
if started is not None and self.__is_pressed[slot] and time.monotonic() - started >= 0.45:
|
||||
# Clear tracking before opening help so release cannot activate a toggle or action.
|
||||
self._press_started[slot] = None
|
||||
self.__is_pressed[slot] = False
|
||||
self.__tracking_is_pressed[slot] = False
|
||||
self._long_press_callback()
|
||||
break
|
||||
|
||||
def _layout(self) -> None:
|
||||
"""Optionally lay out child widgets separately. This is called before rendering."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user