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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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
+
+
+
+ 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