From 9768109ec11416648076163af230d82563666628 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 16 Dec 2025 16:19:53 -0800 Subject: [PATCH] ui: generic hold gesture (#36893) * generic * fix * use in home * clean up * rm * clean up --- selfdrive/ui/mici/layouts/home.py | 33 ++++++++----------------------- system/ui/widgets/__init__.py | 30 ++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/selfdrive/ui/mici/layouts/home.py b/selfdrive/ui/mici/layouts/home.py index 9152bdc7fa..af97de92f4 100644 --- a/selfdrive/ui/mici/layouts/home.py +++ b/selfdrive/ui/mici/layouts/home.py @@ -1,5 +1,3 @@ -import time - from cereal import log import pyray as rl from collections.abc import Callable @@ -83,9 +81,6 @@ class MiciHomeLayout(Widget): self._on_settings_click: Callable | None = None self._last_refresh = 0 - self._mouse_down_t: None | float = None - self._did_long_press = False - self._is_pressed_prev = False self._version_text = None self._experimental_mode = False @@ -124,23 +119,13 @@ class MiciHomeLayout(Widget): def _update_params(self): self._experimental_mode = ui_state.params.get_bool("ExperimentalMode") + def _handle_long_press(self, _): + # long gating for experimental mode - only allow toggle if longitudinal control is available + if ui_state.has_longitudinal_control: + self._experimental_mode = not self._experimental_mode + ui_state.params.put("ExperimentalMode", self._experimental_mode) + def _update_state(self): - if self.is_pressed and not self._is_pressed_prev: - self._mouse_down_t = time.monotonic() - elif not self.is_pressed and self._is_pressed_prev: - self._mouse_down_t = None - self._did_long_press = False - self._is_pressed_prev = self.is_pressed - - if self._mouse_down_t is not None: - if time.monotonic() - self._mouse_down_t > 0.5: - # long gating for experimental mode - only allow toggle if longitudinal control is available - if ui_state.has_longitudinal_control: - self._experimental_mode = not self._experimental_mode - ui_state.params.put("ExperimentalMode", self._experimental_mode) - self._mouse_down_t = None - self._did_long_press = True - if rl.get_time() - self._last_refresh > 5.0: device_state = ui_state.sm['deviceState'] self._update_network_status(device_state) @@ -159,10 +144,8 @@ class MiciHomeLayout(Widget): self._on_settings_click = on_settings def _handle_mouse_release(self, mouse_pos: MousePos): - if not self._did_long_press: - if self._on_settings_click: - self._on_settings_click() - self._did_long_press = False + if self._on_settings_click: + self._on_settings_click() def _get_version_text(self) -> tuple[str, str, str, str] | None: description = ui_state.params.get("UpdaterCurrentDescription") diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index a3fed6d962..5b3b4a691c 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -20,6 +20,8 @@ class DialogResult(IntEnum): class Widget(abc.ABC): + LONG_PRESS_TIME = 0.5 + def __init__(self): self._rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0) self._parent_rect: rl.Rectangle | None = None @@ -33,6 +35,10 @@ class Widget(abc.ABC): self._multi_touch = False self.__was_awake = True + # Long press state (single touch only, slot 0) + self._long_press_start_t: float | None = None + self._long_press_fired: bool = False + @property def rect(self) -> rl.Rectangle: return self._rect @@ -127,19 +133,28 @@ class Widget(abc.ABC): self._handle_mouse_press(mouse_event.pos) self.__is_pressed[mouse_event.slot] = True self.__tracking_is_pressed[mouse_event.slot] = True + if mouse_event.slot == 0: + self._long_press_start_t = rl.get_time() + self._long_press_fired = False self._handle_mouse_event(mouse_event) # Callback such as scroll panel signifies user is scrolling elif not touch_valid: self.__is_pressed[mouse_event.slot] = False self.__tracking_is_pressed[mouse_event.slot] = False + if mouse_event.slot == 0: + self._long_press_start_t = None + self._long_press_fired = False elif mouse_event.left_released: self._handle_mouse_event(mouse_event) - if self.__is_pressed[mouse_event.slot] and mouse_in_rect: + if self.__is_pressed[mouse_event.slot] and mouse_in_rect and not (mouse_event.slot == 0 and self._long_press_fired): self._handle_mouse_release(mouse_event.pos) self.__is_pressed[mouse_event.slot] = False self.__tracking_is_pressed[mouse_event.slot] = False + if mouse_event.slot == 0: + self._long_press_start_t = None + self._long_press_fired = False # Mouse/touch is still within our rect elif mouse_in_rect: @@ -150,8 +165,17 @@ class Widget(abc.ABC): # Mouse/touch left our rect but may come back into focus later elif not mouse_in_rect: self.__is_pressed[mouse_event.slot] = False + if mouse_event.slot == 0: + self._long_press_start_t = None + self._long_press_fired = False self._handle_mouse_event(mouse_event) + # Long press detection + if self._long_press_start_t is not None and not self._long_press_fired: + if (rl.get_time() - self._long_press_start_t) >= self.LONG_PRESS_TIME: + self._long_press_fired = True + self._handle_long_press(gui_app.last_mouse_event.pos) + def _layout(self) -> None: """Optionally lay out child widgets separately. This is called before rendering.""" @@ -175,9 +199,11 @@ class Widget(abc.ABC): self._click_callback() return False + def _handle_long_press(self, mouse_pos: MousePos) -> None: + """Optionally handle a long-press gesture.""" + def _handle_mouse_event(self, mouse_event: MouseEvent) -> None: """Optionally handle mouse events. This is called before rendering.""" - # Default implementation does nothing, can be overridden by subclasses def show_event(self): """Optionally handle show event. Parent must manually call this"""