diff --git a/.gitignore b/.gitignore index 2f0373a..d84edce 100644 --- a/.gitignore +++ b/.gitignore @@ -93,3 +93,4 @@ ENV/ *.swp *.~undo-tree~ +*.glade~ diff --git a/Makefile b/Makefile index 3148408..f21b446 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,10 @@ install-icons: cp icons/qappmenu-pause.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/qappmenu-pause.svg cp icons/qappmenu-start.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/qappmenu-start.svg cp icons/qappmenu-shutdown.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/qappmenu-shutdown.svg + cp icons/qappmenu-top-left.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/qappmenu-top-left.svg + cp icons/qappmenu-top-right.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/qappmenu-top-right.svg + cp icons/qappmenu-bottom-left.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/qappmenu-bottom-left.svg + cp icons/qappmenu-bottom-right.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/qappmenu-bottom-right.svg cp icons/settings-*.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/ install-autostart: diff --git a/icons/qappmenu-bottom-left.svg b/icons/qappmenu-bottom-left.svg new file mode 100644 index 0000000..0b3f621 --- /dev/null +++ b/icons/qappmenu-bottom-left.svg @@ -0,0 +1,84 @@ + + + Qubes Arrow + + + + + + + + + Ali Mirjamali + + + + + CC-BY-SA + + + 2024-11-02 + Qubes Arrow + + + Qubes + Arrow + + + Arrows designed for Qubes OS project + + + + + + + + + + diff --git a/icons/qappmenu-bottom-right.svg b/icons/qappmenu-bottom-right.svg new file mode 100644 index 0000000..0ec4350 --- /dev/null +++ b/icons/qappmenu-bottom-right.svg @@ -0,0 +1,84 @@ + + + Qubes Arrow + + + + + + + + + Ali Mirjamali + + + + + CC-BY-SA + + + 2024-11-02 + Qubes Arrow + + + Qubes + Arrow + + + Arrows designed for Qubes OS project + + + + + + + + + + diff --git a/icons/qappmenu-top-left.svg b/icons/qappmenu-top-left.svg new file mode 100644 index 0000000..0259ccb --- /dev/null +++ b/icons/qappmenu-top-left.svg @@ -0,0 +1,84 @@ + + + Qubes Arrow + + + + + + + + + Ali Mirjamali + + + + + CC-BY-SA + + + 2024-11-02 + Bottom Left Arrow + + + Qubes + Arrow + + + Arrows designed for Qubes OS project + + + + + + + + + + diff --git a/icons/qappmenu-top-right.svg b/icons/qappmenu-top-right.svg new file mode 100644 index 0000000..658ad12 --- /dev/null +++ b/icons/qappmenu-top-right.svg @@ -0,0 +1,84 @@ + + + Qubes Arrow + + + + + + + + + Ali Mirjamali + + + + + CC-BY-SA + + + 2024-11-02 + Qubes Arrow + + + Qubes + Arrow + + + Arrows designed for Qubes OS project + + + + + + + + + + diff --git a/qubes_menu/appmenu.py b/qubes_menu/appmenu.py index 7389ea1..3ee603e 100644 --- a/qubes_menu/appmenu.py +++ b/qubes_menu/appmenu.py @@ -23,7 +23,8 @@ from .custom_widgets import SelfAwareMenu from .vm_manager import VMManager from .page_handler import MenuPage -from .constants import INITIAL_PAGE_FEATURE, SORT_RUNNING_FEATURE +from .constants import INITIAL_PAGE_FEATURE, SORT_RUNNING_FEATURE, \ + POSITION_FEATURE import gi gi.require_version('Gtk', '3.0') @@ -36,6 +37,10 @@ "search_page", "app_page", "favorites_page", "settings_page" ] +POSITION_LIST = [ + "mouse", "top-left", "top-right", "bottom-left", "bottom-right" +] + logger = logging.getLogger('qubes-appmenu') def load_theme(widget: Gtk.Widget, light_theme_path: str, @@ -108,6 +113,7 @@ def __init__(self, qapp, dispatcher): self.highlight_tag: Optional[str] = None self.tasks = [] + self.appmenu_position: str = 'mouse' def _add_cli_options(self): self.add_main_option( @@ -190,6 +196,29 @@ def _do_power_button(_widget): else: subprocess.Popen('xfce4-session-logout', stdin=subprocess.DEVNULL) + def reposition(self): + """ + Helper function to reposition Appmenu based on 'menu_position' feature + """ + assert self.main_window + match self.appmenu_position: + case 'top-left': + self.main_window.move(0, 0) + case 'top-right': + self.main_window.move( + self.main_window.get_screen().get_width() - \ + self.main_window.get_size().width, 0) + case 'bottom-left': + self.main_window.move(0, + self.main_window.get_screen().get_height() - \ + self.main_window.get_size().height) + case 'bottom-right': + self.main_window.move( + self.main_window.get_screen().get_width() - \ + self.main_window.get_size().width, + self.main_window.get_screen().get_height() - \ + self.main_window.get_size().height) + def do_activate(self, *args, **kwargs): """ Method called whenever this program is run; it executes actual setup @@ -202,6 +231,7 @@ def do_activate(self, *args, **kwargs): assert self.main_window assert self.main_notebook if not self.start_in_background: + self.reposition() self.main_window.show_all() self.initialize_state() # set size if too big @@ -231,6 +261,7 @@ def do_activate(self, *args, **kwargs): if self.main_window.is_visible() and not self.keep_visible: self.main_window.hide() else: + self.reposition() self.main_window.present() def hide_menu(self): @@ -330,7 +361,8 @@ def perform_setup(self): self.load_settings() # monitor for settings changes - for feature in [INITIAL_PAGE_FEATURE, SORT_RUNNING_FEATURE]: + for feature in [INITIAL_PAGE_FEATURE, SORT_RUNNING_FEATURE, \ + POSITION_FEATURE]: self.dispatcher.add_handler( 'domain-feature-set:' + feature, self._update_settings) @@ -375,6 +407,11 @@ def load_settings(self): self.sort_running = \ bool(local_vm.features.get(SORT_RUNNING_FEATURE, False)) + position = local_vm.features.get(POSITION_FEATURE, "mouse") + if position not in POSITION_LIST: + position = "mouse" + self.appmenu_position = position + for handler in self.handlers.values(): handler.set_sorting_order(self.sort_running) diff --git a/qubes_menu/constants.py b/qubes_menu/constants.py index 820e90a..d78c601 100644 --- a/qubes_menu/constants.py +++ b/qubes_menu/constants.py @@ -35,6 +35,7 @@ INITIAL_PAGE_FEATURE = 'menu-initial-page' SORT_RUNNING_FEATURE = 'menu-sort-running' +POSITION_FEATURE = 'menu-position' FAVORITES_FEATURE = 'menu-favorites' DISPOSABLE_PREFIX = '@disp:' diff --git a/qubes_menu/tests/test_appmenu.py b/qubes_menu/tests/test_appmenu.py index 247fab1..766582e 100644 --- a/qubes_menu/tests/test_appmenu.py +++ b/qubes_menu/tests/test_appmenu.py @@ -28,6 +28,7 @@ def test_app_menu_conffeatures(): features={'menu-favorites': ''}) qapp._qubes['dom0'].features['menu-initial-page'] = 'favorites_page' qapp._qubes['dom0'].features['menu-sort-running'] = '1' + qapp._qubes['dom0'].features['menu-position'] = '' qapp.update_vm_calls() dispatcher = MockDispatcher(qapp) @@ -38,6 +39,7 @@ def test_app_menu_conffeatures(): # check that initial page is correct assert app_menu.initial_page == "favorites_page" assert app_menu.sort_running + assert app_menu.appmenu_position == "mouse" def test_app_menu_conffeatures_default(): @@ -48,7 +50,8 @@ def test_app_menu_conffeatures_default(): name="test-vm2", qapp=qapp, features={'menu-favorites': '', 'menu-initial-page': 'fake', - 'menu-sort-running': 'fake'}) + 'menu-sort-running': 'fake', + 'menu-position': 'fake'}) qapp.update_vm_calls() dispatcher = MockDispatcher(qapp) @@ -59,6 +62,7 @@ def test_app_menu_conffeatures_default(): # check that default configuration is correct assert app_menu.initial_page == "app_page" assert not app_menu.sort_running + assert app_menu.appmenu_position == "mouse" def test_appmenu_options(): @@ -68,6 +72,7 @@ def test_appmenu_options(): features={'menu-favorites': ''}) qapp._qubes['dom0'].features['menu-initial-page'] = 'app_page' qapp._qubes['dom0'].features['menu-sort-running'] = '1' + qapp._qubes['dom0'].features['menu-position'] = 'top-left' qapp.update_vm_calls() dispatcher = MockDispatcher(qapp) @@ -86,3 +91,44 @@ def test_appmenu_options(): assert app_menu.initial_page == "favorites_page" assert app_menu.keep_visible + assert app_menu.appmenu_position == "top-left" + +def test_appmenu_positioning(): + qapp = MockQubesComplete() + + qapp._qubes['test-vm2'] = MockQube(name="test-vm2", qapp=qapp, + features={'menu-favorites': ''}) + qapp._qubes['dom0'].features['menu-initial-page'] = 'app_page' + qapp._qubes['dom0'].features['menu-sort-running'] = '1' + qapp._qubes['dom0'].features['menu-position'] = '' + qapp.update_vm_calls() + + dispatcher = MockDispatcher(qapp) + app_menu = AppMenu(qapp, dispatcher) + + app_menu.perform_setup() + + # Note: Relying on gravity is to assert changes is useless here. + assert app_menu.main_window + app_menu.appmenu_position = "mouse" + app_menu.reposition() + app_menu.appmenu_position = "top-left" + app_menu.reposition() + assert app_menu.main_window.get_position() == (0, 0) + app_menu.appmenu_position = "top-right" + app_menu.reposition() + assert app_menu.main_window.get_position() == ( \ + app_menu.main_window.get_screen().get_width() - \ + app_menu.main_window.get_size().width, 0) + app_menu.appmenu_position = "bottom-left" + app_menu.reposition() + assert app_menu.main_window.get_position() == (0, \ + app_menu.main_window.get_screen().get_height() - \ + app_menu.main_window.get_size().height) + app_menu.appmenu_position = "bottom-right" + app_menu.reposition() + assert app_menu.main_window.get_position() == ( \ + app_menu.main_window.get_screen().get_width() - \ + app_menu.main_window.get_size().width, \ + app_menu.main_window.get_screen().get_height() - \ + app_menu.main_window.get_size().height) diff --git a/qubes_menu_settings/menu_settings.glade b/qubes_menu_settings/menu_settings.glade index 5151f48..80268ae 100644 --- a/qubes_menu_settings/menu_settings.glade +++ b/qubes_menu_settings/menu_settings.glade @@ -85,6 +85,61 @@ 1 + + + True + False + 3 + + + True + False + Default menu position: + + + False + True + 0 + + + + + True + False + Where to show Appmenu (useful for Hotkeys). + 0.5 + 0.6000000238418579 + 20 + qubes-question + + + False + True + 3 + 1 + + + + + menu_position_combo + True + False + 10 + 10 + + + False + True + 2 + + + + + False + True + 2 + + True @@ -128,7 +183,7 @@ False True - 2 + 3 @@ -195,7 +250,7 @@ False True - 3 + 4 diff --git a/qubes_menu_settings/menu_settings.glade~ b/qubes_menu_settings/menu_settings.glade~ deleted file mode 100644 index 17e047a..0000000 --- a/qubes_menu_settings/menu_settings.glade~ +++ /dev/null @@ -1,204 +0,0 @@ - - - - - - False - Qubes App Menu Settings - center - qappmenu-settings - center - - - True - False - 10 - 10 - 10 - 10 - vertical - 20 - - - True - False - The settings below apply to default Qubes App Menu. - 0 - - - False - True - 0 - - - - - True - False - 3 - - - True - False - Starting menu page: - - - False - True - 0 - - - - - True - False - This page will be shown when you open the menu. - 0.5 - 0.6000000238418579 - 20 - qubes-question - - - False - True - 3 - 1 - - - - - starting_page_combo - True - False - 10 - - - False - True - 2 - - - - - False - True - 1 - - - - - True - False - - - Show running qubes at the top - sort_running_to_top_check - True - True - False - True - - - False - True - 0 - - - - - True - False - In the qube list in search and in the app page, the running qubes will be sorted to top. - 0.5 - 0.6000000238418579 - 20 - qubes-question - - - False - True - 3 - 1 - - - - - - - - False - True - 2 - - - - - True - False - end - 20 - 20 - start - - - Cancel - True - True - True - - - - True - True - 0 - - - - - Apply - True - True - True - - - - True - True - 1 - - - - - Confirm - True - True - True - - - - True - True - 2 - - - - - False - True - 3 - - - - - - diff --git a/qubes_menu_settings/menu_settings.py b/qubes_menu_settings/menu_settings.py index 3b2877c..cfd07bf 100644 --- a/qubes_menu_settings/menu_settings.py +++ b/qubes_menu_settings/menu_settings.py @@ -32,7 +32,8 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk -from qubes_menu.constants import INITIAL_PAGE_FEATURE, SORT_RUNNING_FEATURE +from qubes_menu.constants import INITIAL_PAGE_FEATURE, SORT_RUNNING_FEATURE, \ + POSITION_FEATURE MENU_PAGES = { @@ -46,6 +47,20 @@ "Applications": {"icon": "qappmenu-qube", "object": "app_page"}, "Favorites": {"icon": "qappmenu-favorites", "object": "favorites_page"}} +MENU_POSITIONS = { + "top-left": "Top Left", + "top-right": "Top Right", + "bottom-left": "Bottom Left", + "bottom-right": "Bottom Right", + "mouse": "Mouse" +} + +MENU_POSITIONS_DICT = { + "Top Left": {"icon": "qappmenu-top-left", "object": "top-left"}, + "Top Right": {"icon": "qappmenu-top-right", "object": "top-right"}, + "Bottom Left": {"icon": "qappmenu-bottom-left", "object": "bottom-left"}, + "Bottom Right": {"icon": "qappmenu-bottom-right", "object": "bottom-right"}, + "Mouse": {"icon": "input-mouse-symbolic", "object": "mouse"}} class AppMenuSettings(Gtk.Application): """ @@ -94,6 +109,9 @@ def perform_setup(self): self.starting_page_combo: Gtk.ComboBox = \ self.builder.get_object("starting_page_combo") + self.menu_position_combo: Gtk.ComboBox = \ + self.builder.get_object("menu_position_combo") + self.sort_running_check: Gtk.CheckButton = \ self.builder.get_object("sort_running_to_top_check") @@ -115,6 +133,9 @@ def perform_setup(self): self.initial_page_model = ImageListModeler( self.starting_page_combo, MENU_PAGES_DICT) + self.menu_position_model = ImageListModeler( + self.menu_position_combo, MENU_POSITIONS_DICT) + self.load_state() def load_state(self): @@ -128,6 +149,13 @@ def load_state(self): self.initial_page_model.select_name(MENU_PAGES[initial_page]) self.initial_page_model.update_initial() + menu_position = self.vm.features.get(POSITION_FEATURE, "mouse") + if menu_position not in MENU_POSITIONS: + menu_position = "mouse" + + self.menu_position_model.select_name(MENU_POSITIONS[menu_position]) + self.menu_position_model.update_initial() + # this can sometimes be None, thus, the "or False) sort_running = \ bool(self.vm.features.get(SORT_RUNNING_FEATURE, False)) @@ -154,6 +182,13 @@ def _save(self, *_args): self.vm.features[INITIAL_PAGE_FEATURE] = \ self.initial_page_model.get_selected() + old_menu_position = self.vm.features.get(POSITION_FEATURE, + "mouse") + + if self.menu_position_model.get_selected() != old_menu_position: + self.vm.features[POSITION_FEATURE] = \ + self.menu_position_model.get_selected() + def _save_exit(self, *_args): self._save() self._quit() diff --git a/qubes_menu_settings/test_menu_settings.py b/qubes_menu_settings/test_menu_settings.py index e98e9c2..daf5bc0 100644 --- a/qubes_menu_settings/test_menu_settings.py +++ b/qubes_menu_settings/test_menu_settings.py @@ -27,6 +27,7 @@ def test_menu_settings_load(): qapp._qubes['dom0'].features['menu-initial-page'] = 'favorites_page' qapp._qubes['dom0'].features['menu-sort-running'] = '1' qapp._qubes['dom0'].features['menu-favorites'] = '' + qapp._qubes['dom0'].features['menu-position'] = '' qapp.update_vm_calls() @@ -35,6 +36,7 @@ def test_menu_settings_load(): app.perform_setup() assert app.initial_page_model.get_selected() == "favorites_page" + assert app.menu_position_model.get_selected() == "mouse" assert app.sort_running_check.get_active() @@ -43,6 +45,7 @@ def test_menu_settings_change(): qapp._qubes['dom0'].features['menu-initial-page'] = 'app_page' qapp._qubes['dom0'].features['menu-sort-running'] = '' qapp._qubes['dom0'].features['menu-favorites'] = '' + qapp._qubes['dom0'].features['menu-position'] = 'mouse' qapp.update_vm_calls() @@ -51,13 +54,16 @@ def test_menu_settings_change(): app.perform_setup() assert app.initial_page_model.get_selected() == "app_page" + assert app.menu_position_model.get_selected() == "mouse" assert not app.sort_running_check.get_active() app.starting_page_combo.set_active_id("Search") # the first option is search + app.menu_position_combo.set_active_id("Top Left") # the first option is Top Left app.sort_running_check.set_active(True) qapp.expected_calls[('dom0', 'admin.vm.feature.Set', 'menu-sort-running', b'1')] = b'0\0' qapp.expected_calls[('dom0', 'admin.vm.feature.Set', 'menu-initial-page', b'search_page')] = b'0\0' + qapp.expected_calls[('dom0', 'admin.vm.feature.Set', 'menu-position', b'top-left')] = b'0\0' app._save() @@ -67,6 +73,7 @@ def test_menu_settings_change2(): qapp._qubes['dom0'].features['menu-initial-page'] = 'app_page' qapp._qubes['dom0'].features['menu-sort-running'] = '' qapp._qubes['dom0'].features['menu-favorites'] = '' + qapp._qubes['dom0'].features['menu-position'] = 'mouse' qapp.update_vm_calls() diff --git a/rpm_spec/qubes-desktop-linux-menu.spec.in b/rpm_spec/qubes-desktop-linux-menu.spec.in index d5c3ddc..a5cbe15 100644 --- a/rpm_spec/qubes-desktop-linux-menu.spec.in +++ b/rpm_spec/qubes-desktop-linux-menu.spec.in @@ -133,6 +133,10 @@ gtk-update-icon-cache %{_datadir}/icons/hicolor &>/dev/null || : /usr/share/icons/hicolor/scalable/apps/qappmenu-pause.svg /usr/share/icons/hicolor/scalable/apps/qappmenu-shutdown.svg /usr/share/icons/hicolor/scalable/apps/qappmenu-start.svg +/usr/share/icons/hicolor/scalable/apps/qappmenu-top-left.svg +/usr/share/icons/hicolor/scalable/apps/qappmenu-top-right.svg +/usr/share/icons/hicolor/scalable/apps/qappmenu-bottom-left.svg +/usr/share/icons/hicolor/scalable/apps/qappmenu-bottom-right.svg /usr/share/icons/hicolor/scalable/apps/appmenu-settings-program-icon.svg /usr/share/icons/hicolor/scalable/apps/settings-black.svg /usr/share/icons/hicolor/scalable/apps/settings-blue.svg