From b44889ed05f2615973e92eb68f62f0e889838e16 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Thu, 2 Oct 2025 15:03:05 +0200 Subject: [PATCH] Skip confusing buttons from dom0 and disp template - Dom0: hide shutdown and pause, technically, shutdown can be used by GUIVMs in the future if the action was modified, currently, qubesd logs a failure. - Disposables templates have the "Start qube" button in the "TEMPLATES" tab and the "Search" page, but not in the "APPS" tab. Fixes: https://github.com/QubesOS/qubes-issues/issues/10288 For: https://github.com/QubesOS/qubes-issues/issues/1512 --- qubes_menu/app_widgets.py | 2 +- qubes_menu/application_page.py | 4 +- qubes_menu/custom_widgets.py | 58 ++++++++++----- qubes_menu/search_page.py | 2 +- qubes_menu/tests/test_app_page.py | 117 +++++++++++++++++++++++------- 5 files changed, 133 insertions(+), 50 deletions(-) diff --git a/qubes_menu/app_widgets.py b/qubes_menu/app_widgets.py index 0580d72..aa88be0 100644 --- a/qubes_menu/app_widgets.py +++ b/qubes_menu/app_widgets.py @@ -45,7 +45,7 @@ logger = logging.getLogger("qubes-appmenu") -DISP_TEXT = "new Disposable Qube from " +DISP_TEXT = "New disposable qube from " class AppEntry(Gtk.ListBoxRow): diff --git a/qubes_menu/application_page.py b/qubes_menu/application_page.py index 4799f43..a31082a 100644 --- a/qubes_menu/application_page.py +++ b/qubes_menu/application_page.py @@ -342,7 +342,9 @@ def _selection_changed(self, _widget, row: Optional[VMRow]): self.selected_vm_entry = row self._set_right_visibility(True) self.network_indicator.set_network_state(row.vm_entry.has_network) - self.control_list.update_visibility(row.vm_entry.power_state) + self.control_list.update_visibility( + row.vm_entry, self.toggle_buttons.apps_toggle.get_active() + ) self.control_list.unselect_all() self.app_list.ephemeral_vm = bool(self.selected_vm_entry.vm_entry.parent_vm) self.app_list.invalidate_filter() diff --git a/qubes_menu/custom_widgets.py b/qubes_menu/custom_widgets.py index d4bb16c..ee41d9b 100644 --- a/qubes_menu/custom_widgets.py +++ b/qubes_menu/custom_widgets.py @@ -269,8 +269,9 @@ def get_appinfo(self) -> ApplicationInfo: vm_entry.settings_desktop_file_name ) - def update_state(self, state): # pylint: disable=unused-argument + def update_state(self, vm_entry: VMEntry, apps_tab: bool = False): """Update state: should be always visible.""" + # pylint: disable=unused-argument self.show_all() def show_menu(self, _widget, event): @@ -449,7 +450,7 @@ def __init__(self): self.show_all() self.command = None - def update_state(self, state): + def update_state(self, vm_entry: VMEntry): """ Update own state (visibility/text/sensitivity) based on provided VM state. @@ -497,35 +498,49 @@ def show_menu(self, _widget, event): if event.button == 3: self.menu.popup_at_pointer(None) # None means current event - def update_state(self, state): + def update_state(self, vm_entry: VMEntry, apps_tab: bool = False): """ Update own state (visibility/text/sensitivity) based on provided VM state. """ - self.state = state - if state == "Running": - self.row_label.set_label("Shutdown qube") - self.command = "qvm-shutdown" + vm_name = vm_entry.vm_name + is_dispvm_template = vm_entry.is_dispvm_template + self.state = vm_entry.power_state + + if ( + vm_name == "dom0" + or (apps_tab and is_dispvm_template and self.state != "Running") + ): + self.row_label.set_label(" ") + self.set_sensitive(False) + self.command = None + self.icon.hide() + return + + self.set_sensitive(True) + self.icon.show() + if self.state == "Halted" and not (is_dispvm_template and apps_tab): + self.row_label.set_label("Start qube") + self.command = "qvm-start" self.icon.set_from_pixbuf( - load_icon("qappmenu-shutdown", size=None, pixel_size=15) + load_icon("qappmenu-start", size=None, pixel_size=15) ) return - if state == "Transient": + if self.state == "Transient": self.row_label.set_label("Kill qube") self.command = "qvm-kill" self.icon.set_from_pixbuf( load_icon("qappmenu-shutdown", size=None, pixel_size=15) ) return - if state == "Halted": - self.row_label.set_label("Start qube") - self.command = "qvm-start" + if self.state == "Running": + self.row_label.set_label("Shutdown qube") + self.command = "qvm-shutdown" self.icon.set_from_pixbuf( - load_icon("qappmenu-start", size=None, pixel_size=15) + load_icon("qappmenu-shutdown", size=None, pixel_size=15) ) - return - if state == "Paused": + if self.state == "Paused": self.row_label.set_label("Unpause qube") self.command = "qvm-unpause" self.icon.set_from_pixbuf( @@ -544,13 +559,16 @@ def __init__(self): self.icon.set_from_pixbuf(load_icon("qappmenu-pause", size=None, pixel_size=15)) self.state = None - def update_state(self, state): + def update_state(self, vm_entry: VMEntry, apps_tab: bool = False): """ Update own state (visibility/text/sensitivity) based on provided VM state. """ - self.state = state - if state == "Running": + # pylint: disable=unused-argument + vm_name = vm_entry.vm_name + self.state = vm_entry.power_state + + if self.state == "Running" and vm_name != "dom0": self.row_label.set_label("Pause qube") self.set_sensitive(True) self.command = "qvm-pause" @@ -581,12 +599,12 @@ def __init__(self, app_page): self.add(self.start_item) self.add(self.pause_item) - def update_visibility(self, state): + def update_visibility(self, vm_entry: VMEntry, apps_tab: bool = False): """ Update children's state based on provided VM state. """ for row in self.get_children(): - row.update_state(state) + row.update_state(vm_entry, apps_tab) class KeynavController: diff --git a/qubes_menu/search_page.py b/qubes_menu/search_page.py index 5e9cec7..d42b9f6 100644 --- a/qubes_menu/search_page.py +++ b/qubes_menu/search_page.py @@ -511,7 +511,7 @@ def _selection_changed(self, _widget, row: Optional[SearchVMRow]): else: self.selected_vm_row = row self.control_list.show() - self.control_list.update_visibility(row.vm_entry.power_state) + self.control_list.update_visibility(row.vm_entry, apps_tab=False) self.control_list.unselect_all() self.app_list.invalidate_filter() self.app_list.select_row(None) diff --git a/qubes_menu/tests/test_app_page.py b/qubes_menu/tests/test_app_page.py index fd25524..7a5fadb 100644 --- a/qubes_menu/tests/test_app_page.py +++ b/qubes_menu/tests/test_app_page.py @@ -30,43 +30,104 @@ def test_app_page_vm_state(test_desktop_file_path, test_qapp, test_builder): dispatcher = MockDispatcher(test_qapp) vm_manager = VMManager(test_qapp, dispatcher) - with mock.patch.object(DesktopFileManager, 'desktop_dirs', - [test_desktop_file_path]): + with mock.patch.object( + DesktopFileManager, "desktop_dirs", [test_desktop_file_path] + ): desktop_file_manager = DesktopFileManager(test_qapp) app_page = AppPage(vm_manager, test_builder, desktop_file_manager) - # select a turned off vm - app_page.vm_list.select_row([row for row in app_page.vm_list.get_children() - if row.vm_name == 'test-red'][0]) + # For some reason it defaults to the system tab. + app_page.toggle_buttons.apps_toggle.set_active(True) - assert app_page.control_list.start_item.row_label.get_label() == \ - "Start qube" - assert app_page.control_list.pause_item.row_label.get_label() == \ - " " + # select dom0 + app_page.vm_list.select_row( + [ + row + for row in app_page.vm_list.get_children() + if row.vm_name == "dom0" + ][0] + ) + assert app_page.control_list.start_item.row_label.get_label() == " " + assert app_page.control_list.pause_item.row_label.get_label() == " " - # select a turned on vm - app_page.vm_list.select_row([row for row in app_page.vm_list.get_children() - if row.vm_name == 'sys-usb'][0]) + # select a turned off vm + app_page.vm_list.select_row( + [ + row + for row in app_page.vm_list.get_children() + if row.vm_name == "test-red" + ][0] + ) + + assert ( + app_page.control_list.start_item.row_label.get_label() == "Start qube" + ) + assert app_page.control_list.pause_item.row_label.get_label() == " " - assert app_page.control_list.start_item.row_label.get_label() == \ - "Shutdown qube" - assert app_page.control_list.pause_item.row_label.get_label() == \ - "Pause qube" + # select a turned on vm + app_page.vm_list.select_row( + [ + row + for row in app_page.vm_list.get_children() + if row.vm_name == "sys-usb" + ][0] + ) + + assert ( + app_page.control_list.start_item.row_label.get_label() + == "Shutdown qube" + ) + assert ( + app_page.control_list.pause_item.row_label.get_label() == "Pause qube" + ) + + # select a turned off disposable template + app_page.vm_list.select_row( + [ + row + for row in app_page.vm_list.get_children() + if row.vm_name == "test-alt-dvm" + ][0] + ) + assert app_page.control_list.start_item.row_label.get_label() == " " + assert app_page.control_list.pause_item.row_label.get_label() == " " + + # select a turned on disposable template + app_page.vm_list.select_row( + [ + row + for row in app_page.vm_list.get_children() + if row.vm_name == "test-alt-dvm-running" + ][0] + ) + assert ( + app_page.control_list.start_item.row_label.get_label() + == "Shutdown qube" + ) + assert ( + app_page.control_list.pause_item.row_label.get_label() == "Pause qube" + ) def test_dispvm_parent_sorting(test_desktop_file_path, test_qapp, test_builder): # check if dispvm child is sorted after the parent - test_qapp._qubes['disp1233'] = MockQube( - name="disp1233", qapp=test_qapp, klass='DispVM', - template_for_dispvms='True', template='default-dvm', auto_cleanup=True) + test_qapp._qubes["disp1233"] = MockQube( + name="disp1233", + qapp=test_qapp, + klass="DispVM", + template_for_dispvms="True", + template="default-dvm", + auto_cleanup=True, + ) test_qapp.update_vm_calls() dispatcher = MockDispatcher(test_qapp) vm_manager = VMManager(test_qapp, dispatcher) - with mock.patch.object(DesktopFileManager, 'desktop_dirs', - [test_desktop_file_path]): + with mock.patch.object( + DesktopFileManager, "desktop_dirs", [test_desktop_file_path] + ): desktop_file_manager = DesktopFileManager(test_qapp) app_page = AppPage(vm_manager, test_builder, desktop_file_manager) @@ -75,11 +136,11 @@ def test_dispvm_parent_sorting(test_desktop_file_path, test_qapp, test_builder): for row in app_page.vm_list.get_children(): if found_dvm: - if row.vm_name == 'disp1233' and row.vm_entry.parent_vm: + if row.vm_name == "disp1233" and row.vm_entry.parent_vm: break found_dvm = False continue - if row.vm_name == 'default-dvm' and row.vm_entry._is_dispvm_template: + if row.vm_entry.is_dispvm_template: found_dvm = True continue found_dvm = False @@ -92,12 +153,14 @@ def test_settings_app_page(test_desktop_file_path, test_qapp, test_builder): dispatcher = MockDispatcher(test_qapp) vm_manager = VMManager(test_qapp, dispatcher) - with mock.patch.object(DesktopFileManager, 'desktop_dirs', - [test_desktop_file_path]): + with mock.patch.object( + DesktopFileManager, "desktop_dirs", [test_desktop_file_path] + ): desktop_file_manager = DesktopFileManager(test_qapp) - settings_page = SettingsPage(test_qapp, test_builder, - desktop_file_manager, dispatcher) + settings_page = SettingsPage( + test_qapp, test_builder, desktop_file_manager, dispatcher + ) for row in settings_page.app_list.get_children(): assert not row.app_info.vm