diff --git a/selfdrive/ui/sunnypilot/layouts/settings/settings.py b/selfdrive/ui/sunnypilot/layouts/settings/settings.py index b2f7ceb41d..0d30fdb6dd 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 @@ -115,7 +115,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 0000000000..b890dd5b5b --- /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) diff --git a/system/ui/sunnypilot/widgets/tree_dialog.py b/system/ui/sunnypilot/widgets/tree_dialog.py index cfc0a5caf1..2dd5e3a2dc 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,23 @@ class TreeOptionDialog(MultiOptionDialog): self.search_dialog = None self._search_pressed = False + 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() def _on_search_confirm(self, result, text): @@ -142,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: @@ -152,10 +180,15 @@ 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), 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