ui: refactor ListView for generic widget support and simplified item architecture (#35536)

refactor list view

apply reviews
This commit is contained in:
Dean Lee
2025-06-12 23:55:13 +08:00
committed by GitHub
parent 723a52626d
commit 32ae9efb3d
8 changed files with 299 additions and 396 deletions
+16 -17
View File
@@ -1,7 +1,7 @@
from openpilot.system.ui.lib.list_view import ListView, toggle_item
from openpilot.system.ui.lib.widget import Widget
from openpilot.system.ui.lib.list_view import ListView, ToggleItem
from openpilot.common.params import Params
from openpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item
from openpilot.selfdrive.ui.widgets.ssh_key import SshKeyItem
from openpilot.system.ui.lib.widget import Widget
# Description constants
DESCRIPTIONS = {
@@ -16,34 +16,33 @@ DESCRIPTIONS = {
),
}
class DeveloperLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
items = [
toggle_item(
ToggleItem(
"Enable ADB",
description=DESCRIPTIONS["enable_adb"],
DESCRIPTIONS["enable_adb"],
initial_state=self._params.get_bool("AdbEnabled"),
callback=self._on_enable_adb,
),
ssh_key_item("SSH Key", description=DESCRIPTIONS["ssh_key"]),
toggle_item(
SshKeyItem("SSH Key", description=DESCRIPTIONS["ssh_key"]),
ToggleItem(
"Joystick Debug Mode",
description=DESCRIPTIONS["joystick_debug_mode"],
DESCRIPTIONS["joystick_debug_mode"],
initial_state=self._params.get_bool("JoystickDebugMode"),
callback=self._on_joystick_debug_mode,
),
toggle_item(
ToggleItem(
"Longitudinal Maneuver Mode",
description="",
"",
initial_state=self._params.get_bool("LongitudinalManeuverMode"),
callback=self._on_long_maneuver_mode,
),
toggle_item(
ToggleItem(
"openpilot Longitudinal Control (Alpha)",
description="",
"",
initial_state=self._params.get_bool("AlphaLongitudinalEnabled"),
callback=self._on_alpha_long_enabled,
),
@@ -54,7 +53,7 @@ class DeveloperLayout(Widget):
def _render(self, rect):
self._list_widget.render(rect)
def _on_enable_adb(self): pass
def _on_joystick_debug_mode(self): pass
def _on_long_maneuver_mode(self): pass
def _on_alpha_long_enabled(self): pass
def _on_enable_adb(self, state): pass
def _on_joystick_debug_mode(self, state): pass
def _on_long_maneuver_mode(self, state): pass
def _on_alpha_long_enabled(self, state): pass
+10 -10
View File
@@ -7,7 +7,7 @@ from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialo
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.hardware import TICI
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.list_view import ListView, text_item, button_item, dual_button_item
from openpilot.system.ui.lib.list_view import ListView, TextItem, ButtonItem, DualButtonItem
from openpilot.system.ui.lib.widget import Widget, DialogResult
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
@@ -44,15 +44,15 @@ class DeviceLayout(Widget):
serial = self._params.get("HardwareSerial") or "N/A"
items = [
text_item("Dongle ID", dongle_id),
text_item("Serial", serial),
button_item("Pair Device", "PAIR", DESCRIPTIONS['pair_device'], callback=self._pair_device),
button_item("Driver Camera", "PREVIEW", DESCRIPTIONS['driver_camera'], callback=self._show_driver_camera, enabled=ui_state.is_offroad),
button_item("Reset Calibration", "RESET", DESCRIPTIONS['reset_calibration'], callback=self._reset_calibration_prompt),
button_item("Regulatory", "VIEW", callback=self._on_regulatory, visible=TICI),
button_item("Review Training Guide", "REVIEW", DESCRIPTIONS['review_guide'], self._on_review_training_guide),
button_item("Change Language", "CHANGE", callback=self._show_language_selection, enabled=ui_state.is_offroad),
dual_button_item("Reboot", "Power Off", left_callback=self._reboot_prompt, right_callback=self._power_off_prompt),
TextItem("Dongle ID", dongle_id),
TextItem("Serial", serial),
ButtonItem("Pair Device", "PAIR", DESCRIPTIONS['pair_device'], callback=self._pair_device),
ButtonItem("Driver Camera", "PREVIEW", DESCRIPTIONS['driver_camera'], callback=self._show_driver_camera, enabled=ui_state.is_offroad),
ButtonItem("Reset Calibration", "RESET", DESCRIPTIONS['reset_calibration'], callback=self._reset_calibration_prompt),
ButtonItem("Regulatory", "VIEW", callback=self._on_regulatory, visible=TICI),
ButtonItem("Review Training Guide", "REVIEW", DESCRIPTIONS['review_guide'], self._on_review_training_guide),
ButtonItem("Change Language", "CHANGE", callback=self._show_language_selection, enabled=ui_state.is_offroad),
DualButtonItem("Reboot", "Power Off", left_callback=self._reboot_prompt, right_callback=self._power_off_prompt),
]
return items
+1 -2
View File
@@ -115,13 +115,12 @@ class SettingsLayout(Widget):
# Draw button text (right-aligned)
text_size = measure_text_cached(self._font_medium, panel_info.name, 65)
text_pos = rl.Vector2(
button_rect.x + button_rect.width - text_size.x, button_rect.y + (button_rect.height - text_size.y) / 2
button_rect.x + button_rect.width - text_size.x, y + (button_rect.height - text_size.y) / 2
)
rl.draw_text_ex(self._font_medium, panel_info.name, text_pos, 65, 0, text_color)
# Store button rect for click detection
panel_info.button_rect = button_rect
y += NAV_BTN_HEIGHT + button_spacing
def _draw_current_panel(self, rect: rl.Rectangle):
+6 -6
View File
@@ -1,6 +1,6 @@
from openpilot.common.params import Params
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.list_view import ListView, button_item, text_item
from openpilot.system.ui.lib.list_view import ListView, ButtonItem, TextItem
from openpilot.system.ui.lib.widget import Widget, DialogResult
from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog
@@ -15,11 +15,11 @@ class SoftwareLayout(Widget):
def _init_items(self):
items = [
text_item("Current Version", ""),
button_item("Download", "CHECK", callback=self._on_download_update),
button_item("Install Update", "INSTALL", callback=self._on_install_update),
button_item("Target Branch", "SELECT", callback=self._on_select_branch),
button_item("Uninstall", "UNINSTALL", callback=self._on_uninstall),
TextItem("Current Version", ""),
ButtonItem("Download", "CHECK", callback=self._on_download_update),
ButtonItem("Install Update", "INSTALL", callback=self._on_install_update),
ButtonItem("Target Branch", "SELECT", callback=self._on_select_branch),
ButtonItem("Uninstall", "UNINSTALL", callback=self._on_uninstall),
]
return items
+9 -9
View File
@@ -1,4 +1,4 @@
from openpilot.system.ui.lib.list_view import ListView, multiple_button_item, toggle_item
from openpilot.system.ui.lib.list_view import ListView, MultipleButtonItem, ToggleItem
from openpilot.system.ui.lib.widget import Widget
from openpilot.common.params import Params
@@ -29,24 +29,24 @@ class TogglesLayout(Widget):
super().__init__()
self._params = Params()
items = [
toggle_item(
ToggleItem(
"Enable openpilot",
DESCRIPTIONS["OpenpilotEnabledToggle"],
self._params.get_bool("OpenpilotEnabledToggle"),
icon="chffr_wheel.png",
),
toggle_item(
ToggleItem(
"Experimental Mode",
initial_state=self._params.get_bool("ExperimentalMode"),
icon="experimental_white.png",
),
toggle_item(
ToggleItem(
"Disengage on Accelerator Pedal",
DESCRIPTIONS["DisengageOnAccelerator"],
self._params.get_bool("DisengageOnAccelerator"),
icon="disengage_on_accelerator.png",
),
multiple_button_item(
MultipleButtonItem(
"Driving Personality",
DESCRIPTIONS["LongitudinalPersonality"],
buttons=["Aggressive", "Standard", "Relaxed"],
@@ -55,25 +55,25 @@ class TogglesLayout(Widget):
selected_index=int(self._params.get("LongitudinalPersonality") or 0),
icon="speed_limit.png"
),
toggle_item(
ToggleItem(
"Enable Lane Departure Warnings",
DESCRIPTIONS["IsLdwEnabled"],
self._params.get_bool("IsLdwEnabled"),
icon="warning.png",
),
toggle_item(
ToggleItem(
"Always-On Driver Monitoring",
DESCRIPTIONS["AlwaysOnDM"],
self._params.get_bool("AlwaysOnDM"),
icon="monitoring.png",
),
toggle_item(
ToggleItem(
"Record and Upload Driver Camera",
DESCRIPTIONS["RecordFront"],
self._params.get_bool("RecordFront"),
icon="monitoring.png",
),
toggle_item(
ToggleItem(
"Use Metric System", DESCRIPTIONS["IsMetric"], self._params.get_bool("IsMetric"), icon="monitoring.png"
),
]
+17 -20
View File
@@ -8,10 +8,8 @@ from openpilot.common.params import Params
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.button import gui_button, ButtonStyle
from openpilot.system.ui.lib.list_view import (
ItemAction,
ListItem,
BUTTON_HEIGHT,
BUTTON_BORDER_RADIUS,
BUTTON_FONT_SIZE,
BUTTON_WIDTH,
)
@@ -21,18 +19,18 @@ from openpilot.system.ui.widgets.confirm_dialog import alert_dialog
from openpilot.system.ui.widgets.keyboard import Keyboard
class SshKeyActionState(Enum):
class SshKeyState(Enum):
LOADING = "LOADING"
ADD = "ADD"
REMOVE = "REMOVE"
class SshKeyAction(ItemAction):
class SshKeyItem(ListItem):
HTTP_TIMEOUT = 15 # seconds
MAX_WIDTH = 500
def __init__(self):
super().__init__(self.MAX_WIDTH, True)
def __init__(self, title: str, description: str):
super().__init__(title, description=description)
self._keyboard = Keyboard()
self._params = Params()
@@ -41,11 +39,14 @@ class SshKeyAction(ItemAction):
self._refresh_state()
def get_action_width(self) -> int:
return self.MAX_WIDTH
def _refresh_state(self):
self._username = self._params.get("GithubUsername", "")
self._state = SshKeyActionState.REMOVE if self._params.get("GithubSshKeys") else SshKeyActionState.ADD
self._state = SshKeyState.REMOVE if self._params.get("GithubSshKeys") else SshKeyState.ADD
def _render(self, rect: rl.Rectangle) -> bool:
def render_action(self, rect: rl.Rectangle) -> bool:
# Show error dialog if there's an error
if self._error_message:
message = copy.copy(self._error_message)
@@ -71,8 +72,8 @@ class SshKeyAction(ItemAction):
rect.x + rect.width - BUTTON_WIDTH, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT
),
self._state.value,
is_enabled=self._state != SshKeyActionState.LOADING,
border_radius=BUTTON_BORDER_RADIUS,
is_enabled=self._state != SshKeyState.LOADING,
border_radius=BUTTON_HEIGHT // 2,
font_size=BUTTON_FONT_SIZE,
button_style=ButtonStyle.LIST_ACTION,
):
@@ -81,11 +82,11 @@ class SshKeyAction(ItemAction):
return False
def _handle_button_click(self):
if self._state == SshKeyActionState.ADD:
if self._state == SshKeyState.ADD:
self._keyboard.clear()
self._keyboard.set_title("Enter your GitHub username")
gui_app.set_modal_overlay(self._keyboard, callback=self._on_username_submit)
elif self._state == SshKeyActionState.REMOVE:
elif self._state == SshKeyState.REMOVE:
self._params.remove("GithubUsername")
self._params.remove("GithubSshKeys")
self._refresh_state()
@@ -98,7 +99,7 @@ class SshKeyAction(ItemAction):
if not username:
return
self._state = SshKeyActionState.LOADING
self._state = SshKeyState.LOADING
threading.Thread(target=lambda: self._fetch_ssh_key(username), daemon=True).start()
def _fetch_ssh_key(self, username: str):
@@ -113,16 +114,12 @@ class SshKeyAction(ItemAction):
# Success - save keys
self._params.put("GithubUsername", username)
self._params.put("GithubSshKeys", keys)
self._state = SshKeyActionState.REMOVE
self._state = SshKeyState.REMOVE
self._username = username
except requests.exceptions.Timeout:
self._error_message = "Request timed out"
self._state = SshKeyActionState.ADD
self._state = SshKeyState.ADD
except Exception:
self._error_message = f"No SSH keys found for user '{username}'"
self._state = SshKeyActionState.ADD
def ssh_key_item(title: str, description: str):
return ListItem(title=title, description=description, action_item=SshKeyAction())
self._state = SshKeyState.ADD