From d382cd08e5fd46e63b54400e6f74f284316ee494 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 5 Dec 2025 16:49:29 -0500 Subject: [PATCH 1/6] ui: adjust Sponsor and Pairing button behaviors in sunnylink panel (#1557) ui: adjust sponsor and pairing button behaviors in sunnyLink panel --- selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py | 5 ++--- system/ui/sunnypilot/widgets/sunnylink_pairing_dialog.py | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py b/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py index 7125fdf3f..316d4d224 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py @@ -328,12 +328,11 @@ class SunnylinkLayout(Widget): tier_name = ui_state.sunnylink_state.get_sponsor_tier().name.capitalize() or tr("Not Sponsor") self._sponsor_btn.action_item.set_text(sponsor_btn_text) self._sponsor_btn.action_item.set_value(tier_name, ui_state.sunnylink_state.get_sponsor_tier_color()) - self._sponsor_btn.action_item.set_enabled(self._sunnylink_enabled and not ui_state.sunnylink_state.is_sponsor()) + self._sponsor_btn.action_item.set_enabled(self._sunnylink_enabled) pair_btn_text = tr("Paired") if ui_state.sunnylink_state.is_paired() else tr("Not Paired") self._pair_btn.action_item.set_text(pair_btn_text) - self._pair_btn.set_visible(lambda: self._sunnylink_enabled and not ui_state.sunnylink_state.is_paired()) - self._pair_btn.action_item.set_enabled(self._sunnylink_enabled and not ui_state.sunnylink_state.is_paired()) + self._pair_btn.action_item.set_enabled(self._sunnylink_enabled) def _render(self, rect): self._scroller.render(rect) diff --git a/system/ui/sunnypilot/widgets/sunnylink_pairing_dialog.py b/system/ui/sunnypilot/widgets/sunnylink_pairing_dialog.py index 4fab79caf..af6b9cf45 100644 --- a/system/ui/sunnypilot/widgets/sunnylink_pairing_dialog.py +++ b/system/ui/sunnypilot/widgets/sunnylink_pairing_dialog.py @@ -25,6 +25,7 @@ class SunnylinkPairingDialog(PairingDialog): def __init__(self, sponsor_pairing: bool = False): PairingDialog.__init__(self) self._sponsor_pairing = sponsor_pairing + self._is_paired_prev = ui_state.sunnylink_state.is_paired() def _get_pairing_url(self) -> str: qr_string = "https://github.com/sponsors/sunnyhaibin" @@ -42,7 +43,8 @@ class SunnylinkPairingDialog(PairingDialog): return qr_string def _update_state(self): - if ui_state.sunnylink_state.is_paired(): + is_paired = ui_state.sunnylink_state.is_paired() + if not self._is_paired_prev and is_paired: gui_app.set_modal_overlay(None) def _render(self, rect: rl.Rectangle) -> int: From 93c1c713a9ad4302520829c561698e6c2626254f Mon Sep 17 00:00:00 2001 From: Nayan Date: Fri, 5 Dec 2025 22:52:41 -0500 Subject: [PATCH 2/6] ui: toggle should only toggle toggle - don't toggle description with toggle. Yeah, that! (#1559) @sunnyhaibin, please! Co-authored-by: Jason Wen --- system/ui/sunnypilot/widgets/list_view.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/system/ui/sunnypilot/widgets/list_view.py b/system/ui/sunnypilot/widgets/list_view.py index bd68d35a4..ac646df5b 100644 --- a/system/ui/sunnypilot/widgets/list_view.py +++ b/system/ui/sunnypilot/widgets/list_view.py @@ -165,8 +165,10 @@ class ListItemSP(ListItem): content_width = item_rect.width - (style.ITEM_PADDING * 2) title_width = measure_text_cached(self._font, self.title, style.ITEM_TEXT_FONT_SIZE).x right_width = min(content_width - title_width, right_width) - - action_x = item_rect.x + item_rect.width - right_width + if isinstance(self.action_item, ToggleAction): + action_x = item_rect.x + else: + action_x = item_rect.x + item_rect.width - right_width action_y = item_rect.y return rl.Rectangle(action_x, action_y, right_width, style.ITEM_BASE_HEIGHT) From 5007437969ca25ea566d390a2853a022f7ac756e Mon Sep 17 00:00:00 2001 From: Nayan Date: Fri, 5 Dec 2025 22:58:53 -0500 Subject: [PATCH 3/6] ui: enforce fixed dimensions for `ToggleSP` (#1558) fix rect Co-authored-by: Jason Wen --- system/ui/sunnypilot/widgets/toggle.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system/ui/sunnypilot/widgets/toggle.py b/system/ui/sunnypilot/widgets/toggle.py index 46b390f77..2924aec2b 100644 --- a/system/ui/sunnypilot/widgets/toggle.py +++ b/system/ui/sunnypilot/widgets/toggle.py @@ -24,6 +24,9 @@ class ToggleSP(Toggle): initial_state = self.params.get_bool(self.param_key) Toggle.__init__(self, initial_state, callback) + def set_rect(self, rect: rl.Rectangle): + self._rect = rl.Rectangle(rect.x, rect.y, style.TOGGLE_WIDTH, style.TOGGLE_HEIGHT) + def _handle_mouse_release(self, mouse_pos: MousePos): super()._handle_mouse_release(mouse_pos) if self._enabled and self.param_key: From 1807b193fafeca3c446560a7f9ddf95db66b1f74 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 7 Dec 2025 00:58:33 -0500 Subject: [PATCH 4/6] ui: preserve and update `current_ref` in `TreeOptionDialog` init (#1562) * ui: preserve and update `current_ref` in `TreeOptionDialog` init * do it in init instead --- system/ui/sunnypilot/widgets/tree_dialog.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/system/ui/sunnypilot/widgets/tree_dialog.py b/system/ui/sunnypilot/widgets/tree_dialog.py index cfc0a5caf..27a978153 100644 --- a/system/ui/sunnypilot/widgets/tree_dialog.py +++ b/system/ui/sunnypilot/widgets/tree_dialog.py @@ -78,7 +78,7 @@ class TreeItemWidget(Button): class TreeOptionDialog(MultiOptionDialog): def __init__(self, title, folders, current_ref="", fav_param="", option_font_weight=FontWeight.MEDIUM, search_prompt=None, get_folders_fn=None, on_exit=None, display_func=None, search_funcs=None, search_title=None, search_subtitle=None): - super().__init__(title, [], "", option_font_weight) + super().__init__(title, [], current_ref, option_font_weight) self.folders = folders self.selection_ref = current_ref self.fav_param = fav_param @@ -101,6 +101,19 @@ class TreeOptionDialog(MultiOptionDialog): self.search_dialog = None self._search_pressed = False + if current_ref: + found = False + for folder in folders: + for node in folder.nodes: + if node.ref == current_ref: + display = self.display_func(node) + self.selection = display + self.current = display + found = True + break + if found: + break + self._build_visible_items() def _on_search_confirm(self, result, text): @@ -156,6 +169,7 @@ class TreeOptionDialog(MultiOptionDialog): self.visible_items.append(TreeItemWidget(self.display_func(node), node.ref, False, 1 if folder.folder else 0, lambda node_ref=node: self._select_node(node_ref), favorite_cb, node.ref in self.favorites, is_expanded=expanded)) + self.option_buttons = self.visible_items self.options = [item.text for item in self.visible_items] self.scroller._items = self.visible_items From 96c2650ac4a43175214a6d23739c013b8bb92a89 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 7 Dec 2025 01:31:00 -0500 Subject: [PATCH 5/6] ui: improve `TreeOptionDialog` node selection and item handling (#1563) --- system/ui/sunnypilot/widgets/tree_dialog.py | 41 +++++++++++++++------ 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/system/ui/sunnypilot/widgets/tree_dialog.py b/system/ui/sunnypilot/widgets/tree_dialog.py index 27a978153..2dd5e3a2d 100644 --- a/system/ui/sunnypilot/widgets/tree_dialog.py +++ b/system/ui/sunnypilot/widgets/tree_dialog.py @@ -101,18 +101,22 @@ class TreeOptionDialog(MultiOptionDialog): self.search_dialog = None self._search_pressed = False - if current_ref: - found = False - for folder in folders: - for node in folder.nodes: - if node.ref == current_ref: - display = self.display_func(node) - self.selection = display - self.current = display - found = True - break - if found: + self.selection_node = None + # Try to match by ref, by display text, or fall back to "Default" when no ref is set + for folder in self.folders: + for node in folder.nodes: + display = self.display_func(node) + if ( + node.ref == current_ref or + display == current_ref or + (not current_ref and node.ref == "Default") + ): + self.selection = display + self.current = display + self.selection_node = node break + if self.selection_node is not None: + break self._build_visible_items() @@ -155,6 +159,17 @@ class TreeOptionDialog(MultiOptionDialog): def _build_visible_items(self, reset_scroll=True): self.visible_items = [] + + # Pinned selected item at the very top (if any) + if getattr(self, "selection_node", None) is not None: + node = self.selection_node + display = self.display_func(node) + self.selection = self.current = display + favorite_cb = (lambda node_ref=node: self._toggle_favorite(node_ref)) if self.fav_param and node.ref != "Default" else None + self.visible_items.append(TreeItemWidget(self.display_func(node), node.ref, False, 0, + lambda node_ref=node: self._select_node(node_ref), + favorite_cb, node.ref in self.favorites, is_expanded=True)) + for folder in self.folders: nodes = [node for node in folder.nodes if not self.query or search_from_list(self.query, [search_func(node) for search_func in self.search_funcs])] if not nodes and self.query: @@ -165,6 +180,10 @@ class TreeOptionDialog(MultiOptionDialog): lambda folder_ref=folder: self._toggle_folder(folder_ref))) if expanded: for node in nodes: + # Skip duplicate root-level item for the selected node + if self.selection_node is not None and node.ref == self.selection_node.ref and not folder.folder: + continue + favorite_cb = (lambda node_ref=node: self._toggle_favorite(node_ref)) if self.fav_param and node.ref != "Default" else None self.visible_items.append(TreeItemWidget(self.display_func(node), node.ref, False, 1 if folder.folder else 0, lambda node_ref=node: self._select_node(node_ref), From 323b793a832d605a8e64f51235b713d295ed0c58 Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Sat, 6 Dec 2025 22:36:02 -0800 Subject: [PATCH 6/6] ui: software panel (#1518) * param to control stock vs sp ui * init styles * SP Toggles * Lint * optimizations * sp raylib preview * fix callback * fix ui preview * dialog txt * compare vs what used to be done before InputDialog * introducing ui_state_sp for py * raylib: input dialog * raylib: SP Toggles * raylib: SP Panels * raylib: Option Control * init * param to control stock vs sp ui * better * tree dialog, progress bar widget cool stuff * merge origin raylib toggles * tree dialog * less trees for the planet * the heck * add ui_update callback * save the trees we got icons * Update process.py * yesssssssssss * utilize ON_COLOR constant form op system * Revert "add ui_update callback" This reverts commit 4da32cc0097434aab0aa6a3c35465eabb23c8958. * # Conflicts: # system/ui/sunnypilot/widgets/list_view.py # system/ui/sunnypilot/widgets/option_control.py * Merge remote-tracking branch 'openpilot/master' into nov-19-sync * ui: `GuiApplicationExt` * add to readme * scroller_tici :) * use gui_app.sunnypilot_ui() * # Conflicts: # selfdrive/ui/layouts/main.py # selfdrive/ui/sunnypilot/layouts/settings/cruise.py # selfdrive/ui/sunnypilot/layouts/settings/display.py # selfdrive/ui/sunnypilot/layouts/settings/models.py # selfdrive/ui/sunnypilot/layouts/settings/navigation.py # selfdrive/ui/sunnypilot/layouts/settings/osm.py # selfdrive/ui/sunnypilot/layouts/settings/steering.py # selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py # selfdrive/ui/sunnypilot/layouts/settings/trips.py # selfdrive/ui/sunnypilot/layouts/settings/vehicle.py # selfdrive/ui/sunnypilot/layouts/settings/visuals.py # system/ui/sunnypilot/widgets/option_control.py * init value * Remove 'sunnypilot_ui' Removed 'sunnypilot_ui' parameter from params_keys.h * Update raylib_screenshots.py Removed the parameter setting for 'sunnypilot_ui' in the test. * easier to see * Update progress_bar.py * try something * adjust placement * more simple * smoothing updating components * ui: fuzzy search helper * ui_state_sp * description! * fuzzy af searching * better tree. fully dynamic and stuff * rm * rearrange * license * idk how maybe the merge * more indent * more indent * cleanup * temporaily revert ui_state_sp * only show if fav_param is used in the call * conditional for mypy * mypy * conditional for mypy * str concatenation to reduce line len * level * sunny's new x,y makes this even easier! * refreshing half a second seems legit. * software stuffs * rm * add * loathing loathing, unadulterated loathing, i loathe it all * loathing loathing, unadulterated loathing, i loathe it all * # Conflicts: # system/ui/sunnypilot/lib/styles.py # system/ui/sunnypilot/widgets/tree_dialog.py * search * ds * hide on advanced controls * some * handle toggle confirmation * sunny, NO * nayan, NO !! * easier * move * move it! * add more * need to show current branch --------- Co-authored-by: nayan Co-authored-by: Jason Wen --- .../sunnypilot/layouts/settings/settings.py | 4 +- .../sunnypilot/layouts/settings/software.py | 96 +++++++++++++++++++ 2 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/software.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/settings.py b/selfdrive/ui/sunnypilot/layouts/settings/settings.py index 45c9b9348..4de2cacaf 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/settings.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/settings.py @@ -12,7 +12,7 @@ from openpilot.selfdrive.ui.layouts.settings import settings as OP from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout from openpilot.selfdrive.ui.sunnypilot.layouts.settings.device import DeviceLayoutSP from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout -from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout +from openpilot.selfdrive.ui.sunnypilot.layouts.settings.software import SoftwareLayoutSP from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout from openpilot.system.ui.lib.application import gui_app, MousePos from openpilot.system.ui.lib.multilang import tr_noop @@ -114,7 +114,7 @@ class SettingsLayoutSP(OP.SettingsLayout): OP.PanelType.NETWORK: PanelInfo(tr_noop("Network"), NetworkUISP(wifi_manager), icon="icons/network.png"), OP.PanelType.SUNNYLINK: PanelInfo(tr_noop("sunnylink"), SunnylinkLayout(), icon="icons/wifi_strength_full.png"), OP.PanelType.TOGGLES: PanelInfo(tr_noop("Toggles"), TogglesLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"), - OP.PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_software.png"), + OP.PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayoutSP(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_software.png"), OP.PanelType.MODELS: PanelInfo(tr_noop("Models"), ModelsLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_models.png"), OP.PanelType.STEERING: PanelInfo(tr_noop("Steering"), SteeringLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_lateral.png"), OP.PanelType.CRUISE: PanelInfo(tr_noop("Cruise"), CruiseLayout(), icon="icons/speed_limit.png"), diff --git a/selfdrive/ui/sunnypilot/layouts/settings/software.py b/selfdrive/ui/sunnypilot/layouts/settings/software.py new file mode 100644 index 000000000..b890dd5b5 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/software.py @@ -0,0 +1,96 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import os + +from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.hardware import HARDWARE +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.multilang import tr, tr_noop +from openpilot.system.ui.widgets import DialogResult +from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog + +from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp +from openpilot.system.ui.sunnypilot.widgets.tree_dialog import TreeOptionDialog, TreeNode, TreeFolder + + +DESCRIPTIONS = { + 'disable_updates_offroad': tr_noop( + "When enabled, automatic software updates will be off.
This requires a reboot to take effect." + ), + 'disable_updates_onroad': tr_noop( + "Please enable \"Always Offroad\" mode or turn off the vehicle to adjust these toggles." + ) +} + + +class SoftwareLayoutSP(SoftwareLayout): + def __init__(self): + super().__init__() + self.disable_updates_toggle = toggle_item_sp( + lambda: tr("Disable Updates"), + description="", + initial_state=ui_state.params.get_bool("DisableUpdates"), + callback=self._on_disable_updates_toggled, + ) + self._scroller.add_widget(self.disable_updates_toggle) + + def _handle_reboot(self, result): + if result == DialogResult.CONFIRM: + ui_state.params.put_bool("DisableUpdates", self.disable_updates_toggle.action_item.get_state()) + ui_state.params.put_bool("DoReboot", True) + else: + self.disable_updates_toggle.action_item.set_state(ui_state.params.get_bool("DisableUpdates")) + + def _on_disable_updates_toggled(self, enabled): + dialog = ConfirmDialog(tr("System reboot required for changes to take effect. Reboot now?"), tr("Reboot")) + gui_app.set_modal_overlay(dialog, callback=self._handle_reboot) + + def _on_select_branch(self): + current_git_branch = ui_state.params.get("GitBranch") or "" + branches_str = ui_state.params.get("UpdaterAvailableBranches") or "" + branches = [b for b in branches_str.split(",") if b] + current_target = ui_state.params.get("UpdaterTargetBranch") or "" + top_level_branches = [current_git_branch, "release-mici", "release-tizi", "staging", "dev", "master"] + + if HARDWARE.get_device_type() == "tici": + top_level_branches = ["release-tici", "staging-tici"] + branches = [b for b in branches if b.endswith("-tici")] + + top_level_nodes = [TreeNode(b, {'display_name': b}) for b in top_level_branches if b in branches] + remaining_branches = [b for b in branches if b not in top_level_branches] + prebuilt_nodes = [TreeNode(b, {'display_name': b}) for b in remaining_branches if b.endswith("-prebuilt")] + non_prebuilt_nodes = [TreeNode(b, {'display_name': b}) for b in remaining_branches if not b.endswith("-prebuilt")] + + folders = [ + TreeFolder("", top_level_nodes), + TreeFolder("Prebuilt Branches", prebuilt_nodes), + TreeFolder("Non-Prebuilt Branches", non_prebuilt_nodes), + ] + + def _on_branch_selected(result): + if result == DialogResult.CONFIRM and self._branch_dialog is not None: + selection = self._branch_dialog.selection_ref + if selection: + ui_state.params.put("UpdaterTargetBranch", selection) + self._branch_btn.action_item.set_value(selection) + os.system("pkill -SIGUSR1 -f system.updated.updated") + self._branch_dialog = None + + self._branch_dialog = TreeOptionDialog(tr("Select a branch"), folders, current_target, "", + on_exit=_on_branch_selected) + + gui_app.set_modal_overlay(self._branch_dialog, callback=_on_branch_selected) + + def _update_state(self): + super()._update_state() + show_advanced = ui_state.params.get_bool("ShowAdvancedControls") + self.disable_updates_toggle.action_item.set_enabled(ui_state.is_offroad()) + self.disable_updates_toggle.set_visible(show_advanced) + + disable_updates_desc = tr(DESCRIPTIONS["disable_updates_offroad"] if ui_state.is_offroad() else DESCRIPTIONS["disable_updates_onroad"]) + self.disable_updates_toggle.set_description(disable_updates_desc)