diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 51328de..64063aa 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -10,7 +10,7 @@ checks:pylint:
stage: checks
before_script:
- sudo dnf install -y python3-gobject gtk3 xorg-x11-server-Xvfb
- python3-pip python3-mypy python3-pyxdg gtk-layer-shell
+ python3-pip python3-mypy python3-pyxdg gtk-layer-shell cairo-devel
- pip3 install --quiet -r ci/requirements.txt
- git clone https://github.com/QubesOS/qubes-core-admin-client ~/core-admin-client
script:
@@ -25,7 +25,7 @@ checks:tests:
- "PATH=$PATH:$HOME/.local/bin"
- sudo dnf install -y python3-gobject gtk3 python3-pytest python3-pytest-asyncio
python3-coverage xorg-x11-server-Xvfb python3-inotify sequoia-sqv
- python3-pip python3-pyxdg gtk-layer-shell
+ python3-pip python3-pyxdg gtk-layer-shell cairo-devel
- pip3 install --quiet -r ci/requirements.txt
- git clone https://github.com/QubesOS/qubes-core-admin-client ~/core-admin-client
- git clone https://github.com/QubesOS/qubes-desktop-linux-manager ~/desktop-linux-manager
diff --git a/.pylintrc b/.pylintrc
index 3fccc23..f229cb4 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -80,7 +80,7 @@ notes=FIXME,FIX,XXX,TODO
[FORMAT]
# Maximum number of characters on a single line.
-max-line-length=80
+max-line-length=88
# Maximum number of lines in a module
max-module-lines=3000
diff --git a/Makefile b/Makefile
index f21b446..22d9e95 100644
--- a/Makefile
+++ b/Makefile
@@ -28,6 +28,10 @@ install-icons:
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/qappmenu-az.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/qappmenu-az.svg
+ cp icons/qappmenu-za.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/qappmenu-za.svg
+ cp icons/qappmenu-qube-az.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/qappmenu-qube-az.svg
+ cp icons/qappmenu-qube-za.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/qappmenu-qube-za.svg
cp icons/settings-*.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/
install-autostart:
diff --git a/icons/qappmenu-az.svg b/icons/qappmenu-az.svg
new file mode 100644
index 0000000..ab46560
--- /dev/null
+++ b/icons/qappmenu-az.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/qappmenu-qube-az.svg b/icons/qappmenu-qube-az.svg
new file mode 100644
index 0000000..96d7a0b
--- /dev/null
+++ b/icons/qappmenu-qube-az.svg
@@ -0,0 +1,33 @@
+
+
+
+
diff --git a/icons/qappmenu-qube-za.svg b/icons/qappmenu-qube-za.svg
new file mode 100644
index 0000000..15ac0a3
--- /dev/null
+++ b/icons/qappmenu-qube-za.svg
@@ -0,0 +1,33 @@
+
+
+
+
diff --git a/icons/qappmenu-za.svg b/icons/qappmenu-za.svg
new file mode 100644
index 0000000..960075f
--- /dev/null
+++ b/icons/qappmenu-za.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/qubes_menu/app_widgets.py b/qubes_menu/app_widgets.py
index 540a7e0..0580d72 100644
--- a/qubes_menu/app_widgets.py
+++ b/qubes_menu/app_widgets.py
@@ -26,21 +26,26 @@
from typing import Optional, List
from functools import reduce
-from .custom_widgets import (LimitedWidthLabel, SelfAwareMenu, HoverEventBox,
- FavoritesMenu)
+from .custom_widgets import (
+ LimitedWidthLabel,
+ SelfAwareMenu,
+ HoverEventBox,
+ FavoritesMenu,
+)
from .desktop_file_manager import ApplicationInfo
from .vm_manager import VMManager, VMEntry
from .utils import load_icon, text_search, highlight_words, remove_from_feature
from . import constants
import gi
-gi.require_version('Gtk', '3.0')
+
+gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk
-logger = logging.getLogger('qubes-appmenu')
+logger = logging.getLogger("qubes-appmenu")
-DISP_TEXT = 'new Disposable Qube from '
+DISP_TEXT = "new Disposable Qube from "
class AppEntry(Gtk.ListBoxRow):
@@ -53,6 +58,7 @@ class AppEntry(Gtk.ListBoxRow):
- supports running an application on click; after click signals to the
complete menu it might need hiding
"""
+
def __init__(self, app_info: ApplicationInfo, **properties):
"""
:param app_info: ApplicationInfo obj with data about related app file
@@ -61,23 +67,21 @@ def __init__(self, app_info: ApplicationInfo, **properties):
super().__init__(**properties)
self.app_info = app_info
self.app_info.entries.append(self)
- self.vm_name = app_info.vm.name if app_info.vm else 'dom0'
+ self.vm_name = app_info.vm.name if app_info.vm else "dom0"
self.menu = SelfAwareMenu()
self.event_box = HoverEventBox(focus_widget=self)
self.add(self.event_box)
self.event_box.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
- self.event_box.connect('button-press-event', self.show_menu)
+ self.event_box.connect("button-press-event", self.show_menu)
- self.drag_source_set(
- Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY)
+ self.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY)
self.drag_source_add_uri_targets()
self.connect("drag-data-get", self._on_drag_data_get)
def _on_drag_data_get(self, _widget, _drag_context, data, _info, _time):
- data.set_uris(['file://' +
- urllib.parse.quote(str(self.app_info.file_path))])
+ data.set_uris(["file://" + urllib.parse.quote(str(self.app_info.file_path))])
def show_menu(self, _widget, event):
"""
@@ -100,6 +104,9 @@ def run_app(self, vm):
# pylint: disable=consider-using-with
command = self.app_info.get_command_for_vm(vm)
subprocess.Popen(command, stdin=subprocess.DEVNULL)
+ self.get_toplevel().get_application().emit(
+ "app-started", self.app_info.file_path.name
+ )
self.get_toplevel().get_application().hide_menu()
@@ -107,6 +114,7 @@ class BaseAppEntry(AppEntry):
"""
A 'normal' Application row, used by main applications menu and system tools.
"""
+
def __init__(self, app_info: ApplicationInfo, **properties):
"""
:param app_info: ApplicationInfo obj with data about related app file
@@ -115,7 +123,7 @@ def __init__(self, app_info: ApplicationInfo, **properties):
super().__init__(app_info, **properties)
self.box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
self.event_box.add(self.box)
- self.get_style_context().add_class('app_entry')
+ self.get_style_context().add_class("app_entry")
self.menu = FavoritesMenu(lambda: self.app_info)
self.icon = Gtk.Image()
@@ -137,13 +145,15 @@ def show_menu(self, widget, event):
def update_contents(self):
"""Update icon and app name."""
self.icon.set_from_pixbuf(
- load_icon(self.app_info.app_icon, Gtk.IconSize.LARGE_TOOLBAR))
+ load_icon(self.app_info.app_icon, Gtk.IconSize.LARGE_TOOLBAR)
+ )
self.label.set_label(self.app_info.app_name)
self.show_all()
class VMIcon(Gtk.Image):
"""Helper class for displaying and auto-updating"""
+
def __init__(self, vm_entry: Optional[VMEntry]):
super().__init__()
self.vm_entry = vm_entry
@@ -151,11 +161,13 @@ def __init__(self, vm_entry: Optional[VMEntry]):
self.vm_entry.entries.append(self)
self.update_contents(update_label=True)
- def update_contents(self,
- update_power_state=False,
- update_label=False,
- update_has_network=False,
- update_type=False):
+ def update_contents(
+ self,
+ update_power_state=False,
+ update_label=False,
+ update_has_network=False,
+ update_type=False,
+ ):
# pylint: disable=unused-argument
"""
Update own contents (or related widgets, if applicable) based on state
@@ -168,8 +180,7 @@ def update_contents(self,
:return:
"""
if update_label and self.vm_entry:
- vm_icon = load_icon(self.vm_entry.vm_icon_name,
- Gtk.IconSize.LARGE_TOOLBAR)
+ vm_icon = load_icon(self.vm_entry.vm_icon_name, Gtk.IconSize.LARGE_TOOLBAR)
self.set_from_pixbuf(vm_icon)
self.show_all()
@@ -177,10 +188,10 @@ def update_contents(self,
class AppEntryWithVM(AppEntry):
"""Application Gtk.ListBoxRow with VM description underneath; to be
used in Search and Favorites."""
- def __init__(self, app_info: ApplicationInfo, vm_manager: VMManager,
- **properties):
+
+ def __init__(self, app_info: ApplicationInfo, vm_manager: VMManager, **properties):
super().__init__(app_info, **properties)
- self.get_style_context().add_class('favorite_entry')
+ self.get_style_context().add_class("favorite_entry")
self.grid = Gtk.Grid()
self.event_box.add(self.grid)
@@ -192,8 +203,8 @@ def __init__(self, app_info: ApplicationInfo, vm_manager: VMManager,
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
box.pack_start(self.vm_icon, False, False, 5)
box.pack_start(self.vm_label, False, False, 5)
- self.vm_label.get_style_context().add_class('favorite_vm_name')
- self.app_label.get_style_context().add_class('favorite_app_name')
+ self.vm_label.get_style_context().add_class("favorite_vm_name")
+ self.app_label.get_style_context().add_class("favorite_app_name")
self.app_label.set_halign(Gtk.Align.START)
self.grid.attach(self.app_icon, 0, 0, 1, 2)
@@ -210,8 +221,7 @@ def update_contents(self):
self.app_icon.set_from_pixbuf(app_icon)
if self.app_info.disposable:
- self.vm_label.set_text(
- DISP_TEXT + str(self.app_info.vm))
+ self.vm_label.set_text(DISP_TEXT + str(self.app_info.vm))
elif self.app_info.vm:
self.vm_label.set_text(str(self.app_info.vm))
else:
@@ -228,11 +238,11 @@ class FavoritesAppEntry(AppEntryWithVM):
constants.py, as a space-separated list containing a subset of menu-items
feature.
"""
- def __init__(self, app_info: ApplicationInfo, vm_manager: VMManager,
- **properties):
+
+ def __init__(self, app_info: ApplicationInfo, vm_manager: VMManager, **properties):
super().__init__(app_info, vm_manager, **properties)
- self.remove_item = Gtk.MenuItem(label='Remove from favorites')
- self.remove_item.connect('activate', self._remove_from_favorites)
+ self.remove_item = Gtk.MenuItem(label="Remove from favorites")
+ self.remove_item.connect("activate", self._remove_from_favorites)
self.menu.add(self.remove_item)
self.menu.show_all()
@@ -241,16 +251,17 @@ def _remove_from_favorites(self, *_args, **_kwargs):
feature"""
if not self.app_info.entry_name:
return # there is nothing to remove
- vm = self.app_info.vm or self.app_info.qapp.domains[
- self.app_info.qapp.local_name]
- remove_from_feature(vm, constants.FAVORITES_FEATURE,
- self.app_info.entry_name)
+ vm = (
+ self.app_info.vm
+ or self.app_info.qapp.domains[self.app_info.qapp.local_name]
+ )
+ remove_from_feature(vm, constants.FAVORITES_FEATURE, self.app_info.entry_name)
class SearchAppEntry(AppEntryWithVM):
"""Entry for apps listed on the Search tab."""
- def __init__(self, app_info: ApplicationInfo, vm_manager: VMManager,
- **properties):
+
+ def __init__(self, app_info: ApplicationInfo, vm_manager: VMManager, **properties):
super().__init__(app_info, vm_manager, **properties)
self.menu = FavoritesMenu(lambda: self.app_info)
@@ -269,17 +280,21 @@ def __init__(self, app_info: ApplicationInfo, vm_manager: VMManager,
if self.app_info.vm:
self.search_words.extend(
- self.app_info.vm.name.lower().replace('_', '-').split('-'))
+ self.app_info.vm.name.lower().replace("_", "-").split("-")
+ )
else:
- self.search_words.append('dom0')
+ self.search_words.append("dom0")
if self.app_info.disposable:
self.search_words.extend(DISP_TEXT.lower().split())
if self.app_info.app_name:
self.search_words.extend(
- self.app_info.app_name.lower().replace(
- '_', ' ').replace('-', ' ').split())
+ self.app_info.app_name.lower()
+ .replace("_", " ")
+ .replace("-", " ")
+ .split()
+ )
if self.app_info.keywords:
self.search_words.extend(k.lower() for k in self.app_info.keywords)
@@ -293,9 +308,10 @@ def find_text(self, search_words: List[str]):
return self.last_search_result
if search_words:
- result = reduce(lambda x, y: x*y,
- [text_search(word, self.search_words)
- for word in search_words])
+ result = reduce(
+ lambda x, y: x * y,
+ [text_search(word, self.search_words) for word in search_words],
+ )
else:
result = 0
diff --git a/qubes_menu/application_page.py b/qubes_menu/application_page.py
index 6c9ba5b..4799f43 100644
--- a/qubes_menu/application_page.py
+++ b/qubes_menu/application_page.py
@@ -23,15 +23,15 @@
from typing import Optional
from .desktop_file_manager import DesktopFileManager
-from .custom_widgets import NetworkIndicator, \
- VMRow, ControlList, KeynavController
+from .custom_widgets import NetworkIndicator, VMRow, ControlList, KeynavController
from .app_widgets import AppEntry, BaseAppEntry
from .vm_manager import VMEntry, VMManager
from .page_handler import MenuPage
from .utils import get_visible_child
import gi
-gi.require_version('Gtk', '3.0')
+
+gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk
@@ -40,20 +40,18 @@ class VMTypeToggle:
A class controlling a set of radio buttons for toggling
which VMs are shown.
"""
+
def __init__(self, builder: Gtk.Builder):
"""
:param builder: Gtk.Builder, containing loaded glade data
"""
- self.apps_toggle: Gtk.RadioButton = builder.get_object('apps_toggle')
- self.templates_toggle: Gtk.RadioButton = \
- builder.get_object('templates_toggle')
- self.system_toggle: Gtk.RadioButton = \
- builder.get_object('system_toggle')
- self.vm_list: Gtk.ListBox = builder.get_object('vm_list')
- self.app_list: Gtk.ListBox = builder.get_object('app_list')
+ self.apps_toggle: Gtk.RadioButton = builder.get_object("apps_toggle")
+ self.templates_toggle: Gtk.RadioButton = builder.get_object("templates_toggle")
+ self.system_toggle: Gtk.RadioButton = builder.get_object("system_toggle")
+ self.vm_list: Gtk.ListBox = builder.get_object("vm_list")
+ self.app_list: Gtk.ListBox = builder.get_object("app_list")
- self.buttons = [self.apps_toggle, self.templates_toggle,
- self.system_toggle]
+ self.buttons = [self.apps_toggle, self.templates_toggle, self.system_toggle]
for button in self.buttons:
button.set_relief(Gtk.ReliefStyle.NONE)
@@ -61,7 +59,7 @@ def __init__(self, builder: Gtk.Builder):
button.set_can_focus(True)
# the below is necessary to make sure keyboard navigation
# behaves correctly
- button.connect('focus', self._activate_button)
+ button.connect("focus", self._activate_button)
def initialize_state(self):
"""
@@ -76,7 +74,7 @@ def initialize_state(self):
for button in self.buttons:
if button.get_size_request() == (-1, -1):
- button.set_size_request(button.get_allocated_width()*1.2, -1)
+ button.set_size_request(button.get_allocated_width() * 1.2, -1)
def grab_focus(self):
"""Simulates other grab_focus type functions: grabs keyboard focus
@@ -95,7 +93,7 @@ def _activate_button(widget, _event):
def connect_to_toggle(self, func):
"""Connect a function to toggling of all buttons"""
for button in self.buttons:
- button.connect('toggled', func)
+ button.connect("toggled", func)
def filter_function(self, row):
"""Filter function calculated based on currently selected VM toggle
@@ -124,7 +122,7 @@ def _filter_templatevms(vm_entry: VMEntry):
Filter function for template VMEntries. Returns VMs that
are a templateVM or a template for DispVMs.
"""
- if vm_entry.vm_klass == 'TemplateVM':
+ if vm_entry.vm_klass == "TemplateVM":
return True
return vm_entry.is_dispvm_template
@@ -141,8 +139,13 @@ class AppPage(MenuPage):
"""
Helper class for managing the entirety of Applications menu page.
"""
- def __init__(self, vm_manager: VMManager, builder: Gtk.Builder,
- desktop_file_manager: DesktopFileManager):
+
+ def __init__(
+ self,
+ vm_manager: VMManager,
+ builder: Gtk.Builder,
+ desktop_file_manager: DesktopFileManager,
+ ):
"""
:param vm_manager: VM Manager object
:param builder: Gtk.Builder with loaded glade object
@@ -154,10 +157,10 @@ def __init__(self, vm_manager: VMManager, builder: Gtk.Builder,
self.page_widget: Gtk.Box = builder.get_object("app_page")
- self.vm_list: Gtk.ListBox = builder.get_object('vm_list')
- self.app_list: Gtk.ListBox = builder.get_object('app_list')
- self.vm_right_pane: Gtk.Box = builder.get_object('vm_right_pane')
- self.separator_bottom = builder.get_object('separator_bottom')
+ self.vm_list: Gtk.ListBox = builder.get_object("vm_list")
+ self.app_list: Gtk.ListBox = builder.get_object("app_list")
+ self.vm_right_pane: Gtk.Box = builder.get_object("vm_right_pane")
+ self.separator_bottom = builder.get_object("separator_bottom")
self.network_indicator = NetworkIndicator()
self.vm_right_pane.pack_start(self.network_indicator, False, False, 0)
@@ -168,20 +171,20 @@ def __init__(self, vm_manager: VMManager, builder: Gtk.Builder,
self.toggle_buttons.connect_to_toggle(self._button_toggled)
self.app_list.set_filter_func(self._is_app_fitting)
- self.app_list.connect('row-activated', self._app_clicked)
+ self.app_list.connect("row-activated", self._app_clicked)
self.app_list.set_sort_func(
- lambda x, y:
- x.app_info.sort_name > y.app_info.sort_name)
+ lambda x, y: x.app_info.sort_name > y.app_info.sort_name
+ )
self.app_list.invalidate_sort()
vm_manager.register_new_vm_callback(self._vm_callback)
self.vm_list.set_sort_func(self._sort_vms)
self.vm_list.set_filter_func(self.toggle_buttons.filter_function)
- self.vm_list.connect('row-selected', self._selection_changed)
+ self.vm_list.connect("row-selected", self._selection_changed)
self.control_list = ControlList(self)
- self.control_list.connect('row-activated', self._app_clicked)
+ self.control_list.connect("row-activated", self._app_clicked)
self.vm_right_pane.pack_end(self.control_list, False, False, 0)
self.setup_keynav()
@@ -190,15 +193,15 @@ def __init__(self, vm_manager: VMManager, builder: Gtk.Builder,
self.control_list.set_selection_mode(Gtk.SelectionMode.NONE)
self.keynav_manager = KeynavController(
- widgets_in_order=[self.app_list, self.control_list])
+ widgets_in_order=[self.app_list, self.control_list]
+ )
- self.widget_order = [self.app_list,
- self.control_list]
+ self.widget_order = [self.app_list, self.control_list]
self.vm_list.select_row(None)
self._selection_changed(None, None)
- self.vm_list.connect('map', self._on_map_vm_list)
+ self.vm_list.connect("map", self._on_map_vm_list)
def _on_map_vm_list(self, *_args):
# workaround for https://gitlab.gnome.org/GNOME/gtk/-/issues/4977
@@ -216,11 +219,16 @@ def _sort_vms(self, vmentry: VMRow, other_entry: VMRow):
my_sort_name = vmentry.sort_order
other_sort_name = other_entry.sort_order
if self.sort_running:
- my_sort_name = "0" if (vmentry.vm_entry.power_state == "Running") \
+ my_sort_name = (
+ "0"
+ if (vmentry.vm_entry.power_state == "Running")
else "1" + my_sort_name
- other_sort_name = "0" if \
- (other_entry.vm_entry.power_state == "Running") \
+ )
+ other_sort_name = (
+ "0"
+ if (other_entry.vm_entry.power_state == "Running")
else "1" + other_sort_name
+ )
return my_sort_name > other_sort_name
def set_sorting_order(self, sort_running: bool = False):
@@ -234,12 +242,12 @@ def set_sorting_order(self, sort_running: bool = False):
def setup_keynav(self):
"""Do all the required faffing about to convince Gtk to have
reasonable keyboard nav"""
- self.vm_list.connect('keynav-failed', self._vm_keynav_failed)
+ self.vm_list.connect("keynav-failed", self._vm_keynav_failed)
- self.app_list.connect('key-press-event', self._focus_vm_list)
- self.control_list.connect('key-press-event', self._focus_vm_list)
+ self.app_list.connect("key-press-event", self._focus_vm_list)
+ self.control_list.connect("key-press-event", self._focus_vm_list)
- self.vm_list.connect('key-press-event', self._vm_key_pressed)
+ self.vm_list.connect("key-press-event", self._vm_key_pressed)
def _vm_key_pressed(self, _widget, event):
if event.keyval == Gdk.KEY_Right:
@@ -262,8 +270,7 @@ def _vm_callback(self, vm_entry: VMEntry):
Callback to be performed on all newly loaded VMEntry instances.
"""
if vm_entry:
- vm_row = VMRow(vm_entry,
- show_dispvm_inheritance=not self.sort_running)
+ vm_row = VMRow(vm_entry, show_dispvm_inheritance=not self.sort_running)
vm_row.show_all()
vm_entry.entries.append(vm_row)
self.vm_list.add(vm_row)
@@ -279,15 +286,19 @@ def _is_app_fitting(self, appentry: BaseAppEntry):
"""
if not self.selected_vm_entry:
return False
- if appentry.app_info.vm and \
- appentry.app_info.vm.name != \
- self.selected_vm_entry.vm_entry.vm_name:
- return self.selected_vm_entry.vm_entry.parent_vm == \
- appentry.app_info.vm.name and \
- not appentry.app_info.disposable
+ if (
+ appentry.app_info.vm
+ and appentry.app_info.vm.name != self.selected_vm_entry.vm_entry.vm_name
+ ):
+ return (
+ self.selected_vm_entry.vm_entry.parent_vm == appentry.app_info.vm.name
+ and not appentry.app_info.disposable
+ )
if self.selected_vm_entry.vm_entry.is_dispvm_template:
- return appentry.app_info.disposable == \
- self.toggle_buttons.apps_toggle.get_active()
+ return (
+ appentry.app_info.disposable
+ == self.toggle_buttons.apps_toggle.get_active()
+ )
return True
def _vm_keynav_failed(self, _widget, direction: Gtk.DirectionType):
@@ -333,8 +344,7 @@ def _selection_changed(self, _widget, row: Optional[VMRow]):
self.network_indicator.set_network_state(row.vm_entry.has_network)
self.control_list.update_visibility(row.vm_entry.power_state)
self.control_list.unselect_all()
- self.app_list.ephemeral_vm = bool(
- self.selected_vm_entry.vm_entry.parent_vm)
+ self.app_list.ephemeral_vm = bool(self.selected_vm_entry.vm_entry.parent_vm)
self.app_list.invalidate_filter()
def _set_right_visibility(self, visibility: bool):
diff --git a/qubes_menu/appmenu.py b/qubes_menu/appmenu.py
index 58cbc4b..31be84d 100644
--- a/qubes_menu/appmenu.py
+++ b/qubes_menu/appmenu.py
@@ -23,35 +23,46 @@
from .custom_widgets import SelfAwareMenu
from .vm_manager import VMManager
from .page_handler import MenuPage
-from .constants import INITIAL_PAGE_FEATURE, SORT_RUNNING_FEATURE, \
- POSITION_FEATURE
+from .constants import (
+ INITIAL_PAGE_FEATURE,
+ SORT_RUNNING_FEATURE,
+ POSITION_FEATURE,
+ DISABLE_RECENT_FEATURE,
+)
import gi
-gi.require_version('Gtk', '3.0')
-gi.require_version('GtkLayerShell', '0.1')
-from gi.repository import Gtk, Gdk, GLib, Gio, GtkLayerShell
+
+gi.require_version("Gtk", "3.0")
+gi.require_version("GtkLayerShell", "0.1")
+from gi.repository import Gtk, Gdk, GLib, Gio, GtkLayerShell, GObject
try:
from gi.events import GLibEventLoopPolicy
+
asyncio.set_event_loop_policy(GLibEventLoopPolicy())
HAS_GBULB = False
except ImportError:
import gbulb
+
gbulb.install()
HAS_GBULB = True
-PAGE_LIST = [
- "search_page", "app_page", "favorites_page", "settings_page"
-]
+PAGE_LIST = ["search_page", "app_page", "favorites_page", "settings_page"]
+
+POSITION_LIST = ["mouse", "top-left", "top-right", "bottom-left", "bottom-right"]
-POSITION_LIST = [
- "mouse", "top-left", "top-right", "bottom-left", "bottom-right"
-]
+logger = logging.getLogger("qubes-appmenu")
-logger = logging.getLogger('qubes-appmenu')
+GObject.signal_new(
+ "app-started",
+ Gtk.Application,
+ GObject.SignalFlags.RUN_LAST,
+ None,
+ (str,),
+)
-def load_theme(widget: Gtk.Widget, light_theme_path: str,
- dark_theme_path: str):
+
+def load_theme(widget: Gtk.Widget, light_theme_path: str, dark_theme_path: str):
"""
Load a dark or light theme to current screen, based on widget's
current (system) defaults.
@@ -65,18 +76,20 @@ def load_theme(widget: Gtk.Widget, light_theme_path: str,
provider = Gtk.CssProvider()
provider.load_from_path(path)
Gtk.StyleContext.add_provider_for_screen(
- screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
+ screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
+ )
def is_theme_light(widget):
"""Check if current theme is light or dark"""
style_context: Gtk.StyleContext = widget.get_style_context()
background_color: Gdk.RGBA = style_context.get_background_color(
- Gtk.StateType.NORMAL)
- text_color: Gdk.RGBA = style_context.get_color(
- Gtk.StateType.NORMAL)
- background_intensity = background_color.red + \
- background_color.blue + background_color.green
+ Gtk.StateType.NORMAL
+ )
+ text_color: Gdk.RGBA = style_context.get_color(Gtk.StateType.NORMAL)
+ background_intensity = (
+ background_color.red + background_color.blue + background_color.green
+ )
text_intensity = text_color.red + text_color.blue + text_color.green
return text_intensity < background_intensity
@@ -86,19 +99,23 @@ class AppMenu(Gtk.Application):
"""
Main Gtk.Application for appmenu.
"""
+
def __init__(self, qapp, dispatcher):
"""
:param qapp: qubesadmin.Qubes object
:param dispatcher: qubesadmin.vm.EventsDispatcher
"""
- super().__init__(application_id='org.qubesos.appmenu',
- flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,)
+ super().__init__(
+ application_id="org.qubesos.appmenu",
+ flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
+ )
self.qapp = qapp
self.dispatcher = dispatcher
self.primary = False
self.keep_visible = False
self.initial_page = "app_page"
self.sort_running = False
+ self.disable_recent = False
self.start_in_background = False
self.kde = "KDE" in os.getenv("XDG_CURRENT_DESKTOP", "").split(":")
@@ -122,7 +139,7 @@ def __init__(self, qapp, dispatcher):
self.highlight_tag: Optional[str] = None
self.tasks = []
- self.appmenu_position: str = 'mouse'
+ self.appmenu_position: str = "mouse"
def _add_cli_options(self):
self.add_main_option(
@@ -135,12 +152,12 @@ def _add_cli_options(self):
)
self.add_main_option(
- 'page',
- ord('p'),
+ "page",
+ ord("p"),
GLib.OptionFlags.NONE,
GLib.OptionArg.INT,
"Open menu at selected page; 1 is the apps page 1 is the favorites "
- "page and 2 is the system tools page"
+ "page and 2 is the system tools page",
)
self.add_main_option(
@@ -187,20 +204,21 @@ def _do_power_button(self, _widget):
dbus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
proxy = Gio.DBusProxy.new_sync(
dbus, # dbus
- Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS |
- Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES, # flags
+ Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS
+ | Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES, # flags
None, # info
"org.kde.LogoutPrompt", # bus name
"/LogoutPrompt", # object_path
- "org.kde.LogoutPrompt") # interface
+ "org.kde.LogoutPrompt",
+ ) # interface
proxy.call(
- 'promptAll', # method name
+ "promptAll", # method name
None, # parameters
0, # flags
- 0 # timeout_msec
+ 0, # timeout_msec
)
else:
- subprocess.Popen('xfce4-session-logout', stdin=subprocess.DEVNULL)
+ subprocess.Popen("xfce4-session-logout", stdin=subprocess.DEVNULL)
def reposition(self):
"""
@@ -208,46 +226,59 @@ def reposition(self):
"""
assert self.main_window
match self.appmenu_position:
- case 'top-left':
+ case "top-left":
if self.layer_shell:
- GtkLayerShell.set_anchor(self.main_window,
- GtkLayerShell.Edge.LEFT, True)
- GtkLayerShell.set_anchor(self.main_window,
- GtkLayerShell.Edge.TOP, True)
+ GtkLayerShell.set_anchor(
+ self.main_window, GtkLayerShell.Edge.LEFT, True
+ )
+ GtkLayerShell.set_anchor(
+ self.main_window, GtkLayerShell.Edge.TOP, True
+ )
else:
self.main_window.move(0, 0)
- case 'top-right':
+ case "top-right":
if self.layer_shell:
- GtkLayerShell.set_anchor(self.main_window,
- GtkLayerShell.Edge.RIGHT, True)
- GtkLayerShell.set_anchor(self.main_window,
- GtkLayerShell.Edge.TOP, True)
+ GtkLayerShell.set_anchor(
+ self.main_window, GtkLayerShell.Edge.RIGHT, True
+ )
+ GtkLayerShell.set_anchor(
+ self.main_window, GtkLayerShell.Edge.TOP, True
+ )
else:
self.main_window.move(
- self.main_window.get_screen().get_width() -
- self.main_window.get_size().width, 0)
- case 'bottom-left':
+ self.main_window.get_screen().get_width()
+ - self.main_window.get_size().width,
+ 0,
+ )
+ case "bottom-left":
if self.layer_shell:
- GtkLayerShell.set_anchor(self.main_window,
- GtkLayerShell.Edge.LEFT, True)
- GtkLayerShell.set_anchor(self.main_window,
- GtkLayerShell.Edge.BOTTOM, True)
+ GtkLayerShell.set_anchor(
+ self.main_window, GtkLayerShell.Edge.LEFT, True
+ )
+ GtkLayerShell.set_anchor(
+ self.main_window, GtkLayerShell.Edge.BOTTOM, True
+ )
else:
- 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(
+ 0,
+ self.main_window.get_screen().get_height()
+ - self.main_window.get_size().height,
+ )
+ case "bottom-right":
if self.layer_shell:
- GtkLayerShell.set_anchor(self.main_window,
- GtkLayerShell.Edge.RIGHT, True)
- GtkLayerShell.set_anchor(self.main_window,
- GtkLayerShell.Edge.BOTTOM, True)
+ GtkLayerShell.set_anchor(
+ self.main_window, GtkLayerShell.Edge.RIGHT, True
+ )
+ GtkLayerShell.set_anchor(
+ self.main_window, GtkLayerShell.Edge.BOTTOM, True
+ )
else:
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)
+ 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 __present(self) -> None:
assert self.main_window is not None
@@ -264,12 +295,14 @@ def __present(self) -> None:
assert max_height > 0
# The default for layer shell is no keyboard input.
# Explicitly request exclusive access to the keyboard.
- GtkLayerShell.set_keyboard_mode(self.main_window,
- GtkLayerShell.KeyboardMode.EXCLUSIVE)
+ GtkLayerShell.set_keyboard_mode(
+ self.main_window, GtkLayerShell.KeyboardMode.EXCLUSIVE
+ )
# Work around https://github.com/wmww/gtk-layer-shell/issues/167
# by explicitly setting the window size.
- self.main_window.set_size_request(current_width,
- min(current_height, max_height))
+ self.main_window.set_size_request(
+ current_width, min(current_height, max_height)
+ )
def do_activate(self, *args, **kwargs):
"""
@@ -295,13 +328,14 @@ def do_activate(self, *args, **kwargs):
if not self.start_in_background:
# The default for layer shell is no keyboard input.
# Explicitly request exclusive access to the keyboard.
- GtkLayerShell.set_keyboard_mode(self.main_window,
- GtkLayerShell.KeyboardMode.EXCLUSIVE)
+ GtkLayerShell.set_keyboard_mode(
+ self.main_window, GtkLayerShell.KeyboardMode.EXCLUSIVE
+ )
# Work around https://github.com/wmww/gtk-layer-shell/issues/167
# by explicitly setting the window size.
self.main_window.set_size_request(
- current_width,
- min(current_height, max_height))
+ current_width, min(current_height, max_height)
+ )
elif current_height > max_height:
self.main_window.resize(current_height, max_height)
@@ -314,8 +348,7 @@ def do_activate(self, *args, **kwargs):
]
else:
if self.main_notebook:
- self.main_notebook.set_current_page(
- PAGE_LIST.index(self.initial_page))
+ self.main_notebook.set_current_page(PAGE_LIST.index(self.initial_page))
if self.main_window:
self.main_window.set_keep_above(True)
if self.main_window.is_visible() and not self.keep_visible:
@@ -330,7 +363,7 @@ def hide_menu(self):
app or clicking outside the menu.
"""
# reset search tab
- self.handlers['search_page'].initialize_page()
+ self.handlers["search_page"].initialize_page()
if not self.keep_visible and self.main_window:
self.main_window.hide()
@@ -344,7 +377,7 @@ def _key_press(self, _widget, event):
if event.keyval == Gdk.KEY_Escape:
self.hide_menu()
if event.keyval == Gdk.KEY_space:
- search_page = self.handlers.get('search_page')
+ search_page = self.handlers.get("search_page")
if isinstance(search_page, SearchPage):
p = search_page.search_entry.get_position()
search_page.search_entry.insert_text(" ", p)
@@ -369,8 +402,7 @@ def initialize_state(self):
for page in self.handlers.values():
page.initialize_page()
if self.main_notebook:
- self.main_notebook.set_current_page(
- PAGE_LIST.index(self.initial_page))
+ self.main_notebook.set_current_page(PAGE_LIST.index(self.initial_page))
def perform_setup(self):
"""
@@ -380,59 +412,68 @@ def perform_setup(self):
# build the frontend
self.builder = Gtk.Builder()
- self.fav_app_list = self.builder.get_object('fav_app_list')
- self.sys_tools_list = self.builder.get_object('sys_tools_list')
+ self.fav_app_list = self.builder.get_object("fav_app_list")
+ self.sys_tools_list = self.builder.get_object("sys_tools_list")
- glade_path = (importlib.resources.files('qubes_menu') /
- 'qubes-menu.glade')
+ glade_path = importlib.resources.files("qubes_menu") / "qubes-menu.glade"
with importlib.resources.as_file(glade_path) as path:
self.builder.add_from_file(str(path))
- self.main_window = self.builder.get_object('main_window')
+ self.main_window = self.builder.get_object("main_window")
self.layer_shell = GtkLayerShell.is_supported()
- self.main_notebook = self.builder.get_object('main_notebook')
+ self.main_notebook = self.builder.get_object("main_notebook")
self.main_window.set_events(Gdk.EventMask.FOCUS_CHANGE_MASK)
- self.main_window.connect('focus-out-event', self._focus_out)
- self.main_window.connect('key_press_event', self._key_press)
+ self.main_window.connect("focus-out-event", self._focus_out)
+ self.main_window.connect("key_press_event", self._key_press)
self.add_window(self.main_window)
self.desktop_file_manager = DesktopFileManager(self.qapp)
self.vm_manager = VMManager(self.qapp, self.dispatcher)
self.handlers = {
- 'search_page': SearchPage(self.vm_manager, self.builder,
- self.desktop_file_manager),
- 'app_page': AppPage(self.vm_manager, self.builder,
- self.desktop_file_manager),
- 'favorites_page': FavoritesPage(self.qapp, self.builder,
- self.desktop_file_manager,
- self.dispatcher, self.vm_manager),
- 'settings_page': SettingsPage(self.qapp, self.builder,
- self.desktop_file_manager,
- self.dispatcher)}
- self.power_button = self.builder.get_object('power_button')
- self.power_button.connect('clicked', self._do_power_button)
- self.main_notebook.connect('switch-page', self._handle_page_switch)
- self.connect('shutdown', self.do_shutdown)
+ "search_page": SearchPage(
+ self.vm_manager, self.builder, self.desktop_file_manager
+ ),
+ "app_page": AppPage(
+ self.vm_manager, self.builder, self.desktop_file_manager
+ ),
+ "favorites_page": FavoritesPage(
+ self.qapp,
+ self.builder,
+ self.desktop_file_manager,
+ self.dispatcher,
+ self.vm_manager,
+ ),
+ "settings_page": SettingsPage(
+ self.qapp, self.builder, self.desktop_file_manager, self.dispatcher
+ ),
+ }
+ self.power_button = self.builder.get_object("power_button")
+ self.power_button.connect("clicked", self._do_power_button)
+ self.main_notebook.connect("switch-page", self._handle_page_switch)
+ self.connect("shutdown", self.do_shutdown)
self.main_window.add_events(Gdk.EventMask.KEY_PRESS_MASK)
- self.main_window.connect('key_press_event', self._key_pressed)
+ self.main_window.connect("key_press_event", self._key_pressed)
self.load_style()
- Gtk.Settings.get_default().connect('notify::gtk-theme-name',
- self.load_style)
+ Gtk.Settings.get_default().connect("notify::gtk-theme-name", self.load_style)
self.load_settings()
# monitor for settings changes
- for feature in [INITIAL_PAGE_FEATURE, SORT_RUNNING_FEATURE, \
- POSITION_FEATURE]:
+ for feature in [
+ INITIAL_PAGE_FEATURE,
+ SORT_RUNNING_FEATURE,
+ POSITION_FEATURE,
+ DISABLE_RECENT_FEATURE,
+ ]:
self.dispatcher.add_handler(
- 'domain-feature-set:' + feature,
- self._update_settings)
+ "domain-feature-set:" + feature, self._update_settings
+ )
self.dispatcher.add_handler(
- 'domain-feature-delete:' + feature,
- self._update_settings)
+ "domain-feature-delete:" + feature, self._update_settings
+ )
if self.layer_shell:
GtkLayerShell.init_for_window(self.main_window)
@@ -440,28 +481,31 @@ def perform_setup(self):
def load_style(self, *_args):
"""Load appropriate CSS stylesheet and associated properties."""
- light_ref = (importlib.resources.files('qubes_menu') /
- 'qubes-menu-light.css')
- dark_ref = (importlib.resources.files('qubes_menu') /
- 'qubes-menu-dark.css')
-
- with importlib.resources.as_file(light_ref) as light_path, \
- importlib.resources.as_file(dark_ref) as dark_path:
- load_theme(self.main_window,
- light_theme_path=str(light_path),
- dark_theme_path=str(dark_path))
+ light_ref = importlib.resources.files("qubes_menu") / "qubes-menu-light.css"
+ dark_ref = importlib.resources.files("qubes_menu") / "qubes-menu-dark.css"
+
+ with (
+ importlib.resources.as_file(light_ref) as light_path,
+ importlib.resources.as_file(dark_ref) as dark_path,
+ ):
+ load_theme(
+ self.main_window,
+ light_theme_path=str(light_path),
+ dark_theme_path=str(dark_path),
+ )
label = Gtk.Label()
style_context: Gtk.StyleContext = label.get_style_context()
- style_context.add_class('search_highlight')
+ style_context.add_class("search_highlight")
bg_color = style_context.get_background_color(Gtk.StateFlags.NORMAL)
fg_color = style_context.get_color(Gtk.StateFlags.NORMAL)
# This converts a Gdk.RGBA color to a hex representation liked by span
# tags in Pango
- self.highlight_tag = \
- f''
+ )
def load_settings(self):
"""Load settings from dom0 features."""
@@ -472,8 +516,8 @@ def load_settings(self):
initial_page = "app_page"
self.initial_page = initial_page
- self.sort_running = \
- bool(local_vm.features.get(SORT_RUNNING_FEATURE, False))
+ self.disable_recent = bool(local_vm.features.get(DISABLE_RECENT_FEATURE, False))
+ 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:
@@ -485,6 +529,8 @@ def load_settings(self):
for handler in self.handlers.values():
handler.set_sorting_order(self.sort_running)
+ if isinstance(handler, SearchPage):
+ handler.enable_recent(not self.disable_recent)
def _update_settings(self, vm, _event, **_kwargs):
if not str(vm) == self.qapp.local_name:
@@ -494,14 +540,17 @@ def _update_settings(self, vm, _event, **_kwargs):
@staticmethod
def _rgba_color_to_hex(color: Gdk.RGBA):
- return '#' + ''.join([f'{int(c*255):0>2x}'
- for c in (color.red, color.green, color.blue)])
+ return "#" + "".join(
+ [f"{int(c*255):0>2x}" for c in (color.red, color.green, color.blue)]
+ )
def _key_pressed(self, _widget, event_key: Gdk.EventKey):
"""If user presses a non-control key, move to search."""
- if Gdk.keyval_to_unicode(event_key.keyval) > 32 or \
- event_key.keyval == Gdk.KEY_BackSpace:
- search_page = self.handlers.get('search_page')
+ if (
+ Gdk.keyval_to_unicode(event_key.keyval) > 32
+ or event_key.keyval == Gdk.KEY_BackSpace
+ ):
+ search_page = self.handlers.get("search_page")
if not isinstance(search_page, SearchPage):
return False
@@ -531,7 +580,8 @@ def get_currently_selected_vm(self):
"""
assert self.main_notebook
current_page_handler = self.handlers[
- PAGE_LIST[self.main_notebook.get_current_page()]]
+ PAGE_LIST[self.main_notebook.get_current_page()]
+ ]
if hasattr(current_page_handler, "get_selected_vm"):
return current_page_handler.get_selected_vm()
return None
@@ -556,5 +606,5 @@ def main():
app.run(sys.argv)
-if __name__ == '__main__':
+if __name__ == "__main__":
sys.exit(main())
diff --git a/qubes_menu/constants.py b/qubes_menu/constants.py
index d78c601..e95625e 100644
--- a/qubes_menu/constants.py
+++ b/qubes_menu/constants.py
@@ -23,25 +23,26 @@
"""
STATE_DICTIONARY = {
- 'domain-pre-start': 'Transient',
- 'domain-start': 'Running',
- 'domain-start-failed': 'Halted',
- 'domain-paused': 'Paused',
- 'domain-unpaused': 'Running',
- 'domain-shutdown': 'Halted',
- 'domain-pre-shutdown': 'Transient',
- 'domain-shutdown-failed': 'Running'
+ "domain-pre-start": "Transient",
+ "domain-start": "Running",
+ "domain-start-failed": "Halted",
+ "domain-paused": "Paused",
+ "domain-unpaused": "Running",
+ "domain-shutdown": "Halted",
+ "domain-pre-shutdown": "Transient",
+ "domain-shutdown-failed": "Running",
}
-INITIAL_PAGE_FEATURE = 'menu-initial-page'
-SORT_RUNNING_FEATURE = 'menu-sort-running'
-POSITION_FEATURE = 'menu-position'
+INITIAL_PAGE_FEATURE = "menu-initial-page"
+SORT_RUNNING_FEATURE = "menu-sort-running"
+POSITION_FEATURE = "menu-position"
+DISABLE_RECENT_FEATURE = "menu-disable-recent"
-FAVORITES_FEATURE = 'menu-favorites'
-DISPOSABLE_PREFIX = '@disp:'
+FAVORITES_FEATURE = "menu-favorites"
+DISPOSABLE_PREFIX = "@disp:"
-RESTART_PARAM_LONG = 'restart'
-RESTART_PARAM_SHORT = 'r'
+RESTART_PARAM_LONG = "restart"
+RESTART_PARAM_SHORT = "r"
# Timeout for activation change when hovering over a menu item, in microseconds
HOVER_TIMEOUT = 15
diff --git a/qubes_menu/custom_widgets.py b/qubes_menu/custom_widgets.py
index 802d0f3..d4bb16c 100644
--- a/qubes_menu/custom_widgets.py
+++ b/qubes_menu/custom_widgets.py
@@ -29,7 +29,8 @@
from .desktop_file_manager import ApplicationInfo
import gi
-gi.require_version('Gtk', '3.0')
+
+gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, GLib, Pango
@@ -38,6 +39,7 @@ class LimitedWidthLabel(Gtk.Label):
Gtk.Label, but with ellipsization and capped at 35 characters wide
(which is not coincidentally 4 characters more than maximum VM name length)
"""
+
def __init__(self, label_text=None):
"""
:param label_text: optional text of the newly instantiated label
@@ -52,6 +54,7 @@ def __init__(self, label_text=None):
class HoverEventBox(Gtk.EventBox):
"""An EventBox that grabs provided widget on mouse hover."""
+
def __init__(self, focus_widget: Gtk.Widget):
super().__init__()
self.mouse = False
@@ -59,8 +62,8 @@ def __init__(self, focus_widget: Gtk.Widget):
self.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK)
self.add_events(Gdk.EventMask.LEAVE_NOTIFY_MASK)
- self.connect('enter-notify-event', self._enter_event)
- self.connect('leave-notify-event', self._leave_event)
+ self.connect("enter-notify-event", self._enter_event)
+ self.connect("leave-notify-event", self._leave_event)
def _enter_event(self, *_args):
self.mouse = True
@@ -81,6 +84,7 @@ class HoverListBox(Gtk.ListBoxRow):
Gtk.ListBoxRow, but selects itself on hover (after a timeout specified in
constants.py)
"""
+
def __init__(self):
super().__init__()
self.mouse = False
@@ -90,7 +94,7 @@ def __init__(self):
self.event_box.add(self.main_box)
self.add(self.event_box)
- self.connect('focus-in-event', self._on_focus)
+ self.connect("focus-in-event", self._on_focus)
def _on_focus(self, *_args):
if self.get_mapped():
@@ -103,13 +107,14 @@ class SelfAwareMenu(Gtk.Menu):
Gtk.Menu, but the class has a counter of number of currently opened menus.
There can be only one menu open at a time.
"""
+
OPEN_MENUS = 0
def __init__(self, **kwargs):
super().__init__(**kwargs)
- self.get_style_context().add_class('right_menu')
- self.connect('realize', self._add_to_open)
- self.connect('unrealize', self._remove_from_open)
+ self.get_style_context().add_class("right_menu")
+ self.connect("realize", self._add_to_open)
+ self.connect("unrealize", self._remove_from_open)
@staticmethod
def _add_to_open(*_args):
@@ -124,12 +129,13 @@ class FavoritesMenu(SelfAwareMenu):
"""
Menu for showing add to favorites option.
"""
+
def __init__(self, app_info_getter: Callable[[], ApplicationInfo]):
super().__init__()
self.app_info_getter = app_info_getter
- self.add_menu_item = Gtk.CheckMenuItem(label='Add to favorites')
- self.add_menu_item.connect('activate', self._add_to_favorites)
+ self.add_menu_item = Gtk.CheckMenuItem(label="Add to favorites")
+ self.add_menu_item.connect("activate", self._add_to_favorites)
self.add(self.add_menu_item)
self.show_all()
@@ -140,7 +146,7 @@ def _has_favorite_sibling(self):
"""
if self.app_info_getter():
for entry in self.app_info_getter().entries:
- if type(entry).__name__ == 'FavoritesAppEntry':
+ if type(entry).__name__ == "FavoritesAppEntry":
return True
return False
@@ -149,12 +155,18 @@ def _add_to_favorites(self, *_args, **_kwargs):
"Add to favorites" action: sets appropriate VM feature
"""
target_vm = self.app_info_getter().vm
+ entry_name = self.app_info_getter().entry_name
+ if not entry_name:
+ return
+
if not target_vm:
target_vm = self.app_info_getter().qapp.domains[
- self.app_info_getter().qapp.local_name]
+ self.app_info_getter().qapp.local_name
+ ]
- add_to_feature(target_vm, constants.FAVORITES_FEATURE,
- self.app_info_getter().entry_name) # type: ignore
+ add_to_feature(
+ target_vm, constants.FAVORITES_FEATURE, entry_name
+ ) # type: ignore
def set_menu_state(self):
"""
@@ -165,8 +177,10 @@ def set_menu_state(self):
:return:
"""
- if (getattr(self.get_parent(), 'ephemeral_vm', False) or
- not self.app_info_getter()):
+ if (
+ getattr(self.get_parent(), "ephemeral_vm", False)
+ or not self.app_info_getter()
+ ):
self.add_menu_item.set_active(False)
self.add_menu_item.set_sensitive(False)
else:
@@ -180,14 +194,17 @@ class NetworkIndicator(Gtk.Box):
Network Indicator Gtk.Box - changes appearance when set_network_state is
called.
"""
+
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.icon_size = Gtk.IconSize.LARGE_TOOLBAR
self.network_on: Gtk.Image = Gtk.Image.new_from_pixbuf(
- load_icon('qappmenu-networking-yes', self.icon_size))
+ load_icon("qappmenu-networking-yes", self.icon_size)
+ )
self.network_off: Gtk.Image = Gtk.Image.new_from_pixbuf(
- load_icon('qappmenu-networking-no', self.icon_size))
+ load_icon("qappmenu-networking-no", self.icon_size)
+ )
_, height, _ = Gtk.icon_size_lookup(self.icon_size)
self.network_on.set_size_request(-1, height * 1.3)
@@ -196,13 +213,13 @@ def __init__(self, *args, **kwargs):
self.pack_end(self.network_on, False, True, 10)
self.pack_end(self.network_off, False, True, 10)
- self.network_on.set_tooltip_text('Qube is networked')
- self.network_off.set_tooltip_text('Qube is not networked')
+ self.network_on.set_tooltip_text("Qube is networked")
+ self.network_off.set_tooltip_text("Qube is not networked")
self.network_on.set_no_show_all(True)
self.network_off.set_no_show_all(True)
- self.get_style_context().add_class('network_indicator')
+ self.get_style_context().add_class("network_indicator")
def set_network_state(self, state: bool):
"""
@@ -218,20 +235,20 @@ class SettingsEntry(Gtk.ListBoxRow):
"""
Gtk.ListBoxRow especially for a (run VM) Settings entry.
"""
+
def __init__(self, desktop_file_manager):
super().__init__()
self.desktop_file_manager = desktop_file_manager
self.event_box = HoverEventBox(focus_widget=self)
self.hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
self.event_box.add(self.hbox)
- self.settings_icon = Gtk.Image.new_from_pixbuf(
- load_icon('settings-black'))
+ self.settings_icon = Gtk.Image.new_from_pixbuf(load_icon("settings-black"))
self.hbox.pack_start(self.settings_icon, False, False, 5)
self.settings_label = Gtk.Label(label="Settings", xalign=0)
self.hbox.pack_start(self.settings_label, False, False, 5)
- self.get_style_context().add_class('app_entry')
+ self.get_style_context().add_class("app_entry")
self.add(self.event_box)
- self.event_box.connect('button-press-event', self.show_menu)
+ self.event_box.connect("button-press-event", self.show_menu)
self.menu = FavoritesMenu(self.get_appinfo)
@@ -240,16 +257,17 @@ def __init__(self, desktop_file_manager):
def run_app(self, vm):
"""Run settings for specified vm."""
# pylint: disable=consider-using-with
- subprocess.Popen(
- ['qubes-vm-settings', vm.name], stdin=subprocess.DEVNULL)
+ subprocess.Popen(["qubes-vm-settings", vm.name], stdin=subprocess.DEVNULL)
self.get_toplevel().get_application().hide_menu()
def get_appinfo(self) -> ApplicationInfo:
"""Get relevant app_info for currently selected vm"""
- vm_entry: VMEntry = (self.get_toplevel().get_application().
- get_currently_selected_vm())
+ vm_entry: VMEntry = (
+ self.get_toplevel().get_application().get_currently_selected_vm()
+ )
return self.desktop_file_manager.get_app_info_by_name(
- vm_entry.settings_desktop_file_name)
+ vm_entry.settings_desktop_file_name
+ )
def update_state(self, state): # pylint: disable=unused-argument
"""Update state: should be always visible."""
@@ -270,8 +288,10 @@ class VMRow(HoverListBox):
"""
Helper widget representing a VM row.
"""
- def __init__(self, vm_entry: VMEntry,
- show_dispvm_inheritance: Optional[bool] = True):
+
+ def __init__(
+ self, vm_entry: VMEntry, show_dispvm_inheritance: Optional[bool] = True
+ ):
"""
:param vm_entry: VMEntry object, stored and managed by VMManager
:param show_dispvm_inheritance: bool, should dispvm children be shown
@@ -281,15 +301,15 @@ def __init__(self, vm_entry: VMEntry,
self.vm_entry = vm_entry
self.vm_name = vm_entry.vm_name
self.show_dispvm_inheritance = show_dispvm_inheritance
- self.get_style_context().add_class('vm_entry')
+ self.get_style_context().add_class("vm_entry")
self.icon_img = Gtk.Image()
self.dispvm_icon = Gtk.Image()
# add the icon for dispvm parent existing
if self.vm_entry.parent_vm:
- dispvm_icon_img = load_icon('qappmenu-dispvm-child', None, 15)
+ dispvm_icon_img = load_icon("qappmenu-dispvm-child", None, 15)
self.dispvm_icon.set_from_pixbuf(dispvm_icon_img)
- self.dispvm_icon.get_style_context().add_class('dispvm_icon')
+ self.dispvm_icon.get_style_context().add_class("dispvm_icon")
self.dispvm_icon.set_valign(Gtk.Align.START)
self.main_box.pack_start(self.dispvm_icon, False, False, 2)
self.dispvm_icon.set_no_show_all(True)
@@ -298,35 +318,41 @@ def __init__(self, vm_entry: VMEntry,
self.label = Gtk.Label(label=self.vm_entry.vm_name)
self.main_box.pack_start(self.label, False, False, 2)
- self.update_contents(update_power_state=True, update_label=True,
- update_has_network=True, update_type=True)
+ self.update_contents(
+ update_power_state=True,
+ update_label=True,
+ update_has_network=True,
+ update_type=True,
+ )
def update_style(self, update_power_state: bool = True):
"""Update own style, based on whether VM is running or not and
what type it has."""
style_context: Gtk.StyleContext = self.get_style_context()
if self.vm_entry.is_dispvm_template:
- style_context.add_class('dvm_template_entry')
+ style_context.add_class("dvm_template_entry")
elif self.vm_entry.parent_vm:
# has a parent VM means that it should have arrow etc.
- style_context.add_class('dispvm_entry')
+ style_context.add_class("dispvm_entry")
else:
- style_context.remove_class('dispvm_entry')
- style_context.remove_class('dvm_template_entry')
+ style_context.remove_class("dispvm_entry")
+ style_context.remove_class("dvm_template_entry")
if update_power_state:
- if self.vm_entry.power_state == 'Running':
- style_context.add_class('running_vm')
+ if self.vm_entry.power_state == "Running":
+ style_context.add_class("running_vm")
else:
- style_context.remove_class('running_vm')
+ style_context.remove_class("running_vm")
self.dispvm_icon.set_visible(self.show_dispvm_inheritance)
- def update_contents(self,
- update_power_state=False,
- update_label=False,
- update_has_network=False,
- update_type=False):
+ def update_contents(
+ self,
+ update_power_state=False,
+ update_label=False,
+ update_has_network=False,
+ update_type=False,
+ ):
"""
Update own contents (or related widgets, if applicable) based on state
change.
@@ -362,35 +388,41 @@ def sort_order(self):
class SearchVMRow(VMRow):
"""VM Row used for the Search tab."""
- def update_contents(self,
- update_power_state=False,
- update_label=False,
- update_has_network=False,
- update_type=False):
+
+ def update_contents(
+ self,
+ update_power_state=False,
+ update_label=False,
+ update_has_network=False,
+ update_type=False,
+ ):
"""
Search rows do not show power state.
"""
- super().update_contents(update_power_state=False,
- update_label=update_label,
- update_has_network=False,
- update_type=update_type)
+ super().update_contents(
+ update_power_state=False,
+ update_label=update_label,
+ update_has_network=False,
+ update_type=update_type,
+ )
class AnyVMRow(HoverListBox):
"""Generic Any VM row for search purposes."""
+
def __init__(self):
super().__init__()
self.vm_name = None
- self.sort_order = ''
- self.get_style_context().add_class('vm_entry')
+ self.sort_order = ""
+ self.get_style_context().add_class("vm_entry")
icon_img = Gtk.Image()
- icon_vm = load_icon('qubes-logo-icon')
+ icon_vm = load_icon("qubes-logo-icon")
icon_img.set_from_pixbuf(icon_vm)
self.main_box.pack_start(icon_img, False, False, 2)
self.label = Gtk.Label()
- self.label.set_markup('Any qube')
+ self.label.set_markup("Any qube")
self.main_box.pack_start(self.label, False, False, 2)
self.show_all()
@@ -400,10 +432,11 @@ class ControlRow(Gtk.ListBoxRow):
Gtk.ListBoxRow representing one of the VM control options: start/shutdown/
pause etc.
"""
+
def __init__(self):
super().__init__()
self.row_label = LimitedWidthLabel()
- self.get_style_context().add_class('app_entry')
+ self.get_style_context().add_class("app_entry")
self.event_box = HoverEventBox(focus_widget=self)
self.add(self.event_box)
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
@@ -437,20 +470,23 @@ class StartControlItem(ControlRow):
shutdown if it's running, unpause if it's paused, and kill if it's
transient.
"""
+
def __init__(self, desktop_file_manager):
super().__init__()
self.desktop_file_manager = desktop_file_manager
self.state = None
- self.event_box.connect('button-press-event', self.show_menu)
+ self.event_box.connect("button-press-event", self.show_menu)
self.menu = FavoritesMenu(self.get_appinfo)
def get_appinfo(self) -> ApplicationInfo:
"""Get relevant app_info for currently selected vm"""
- vm_entry: VMEntry = (self.get_toplevel().get_application().
- get_currently_selected_vm())
+ vm_entry: VMEntry = (
+ self.get_toplevel().get_application().get_currently_selected_vm()
+ )
return self.desktop_file_manager.get_app_info_by_name(
- vm_entry.settings_desktop_file_name)
+ vm_entry.settings_desktop_file_name
+ )
def show_menu(self, _widget, event):
"""
@@ -467,30 +503,34 @@ def update_state(self, state):
state.
"""
self.state = state
- if state == 'Running':
- self.row_label.set_label('Shutdown qube')
- self.command = 'qvm-shutdown'
- self.icon.set_from_pixbuf(load_icon("qappmenu-shutdown", size=None,
- pixel_size=15))
+ if state == "Running":
+ self.row_label.set_label("Shutdown qube")
+ self.command = "qvm-shutdown"
+ self.icon.set_from_pixbuf(
+ load_icon("qappmenu-shutdown", size=None, pixel_size=15)
+ )
return
- if 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))
+ if 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'
- self.icon.set_from_pixbuf(load_icon("qappmenu-start", size=None,
- pixel_size=15))
+ if state == "Halted":
+ self.row_label.set_label("Start qube")
+ self.command = "qvm-start"
+ self.icon.set_from_pixbuf(
+ load_icon("qappmenu-start", size=None, pixel_size=15)
+ )
return
- if state == 'Paused':
- self.row_label.set_label('Unpause qube')
- self.command = 'qvm-unpause'
- self.icon.set_from_pixbuf(load_icon("qappmenu-start", size=None,
- pixel_size=15))
+ if state == "Paused":
+ self.row_label.set_label("Unpause qube")
+ self.command = "qvm-unpause"
+ self.icon.set_from_pixbuf(
+ load_icon("qappmenu-start", size=None, pixel_size=15)
+ )
return
@@ -498,10 +538,10 @@ class PauseControlItem(ControlRow):
"""
Control Row item representing pausing VM: visible only when it's running.
"""
+
def __init__(self):
super().__init__()
- self.icon.set_from_pixbuf(load_icon("qappmenu-pause", size=None,
- pixel_size=15))
+ self.icon.set_from_pixbuf(load_icon("qappmenu-pause", size=None, pixel_size=15))
self.state = None
def update_state(self, state):
@@ -510,13 +550,13 @@ def update_state(self, state):
state.
"""
self.state = state
- if state == 'Running':
- self.row_label.set_label('Pause qube')
+ if state == "Running":
+ self.row_label.set_label("Pause qube")
self.set_sensitive(True)
- self.command = 'qvm-pause'
+ self.command = "qvm-pause"
self.icon.show()
return
- self.row_label.set_label(' ')
+ self.row_label.set_label(" ")
self.set_sensitive(False)
self.command = None
self.icon.hide()
@@ -526,11 +566,12 @@ class ControlList(Gtk.ListBox):
"""
ListBox containing VM state control items.
"""
+
def __init__(self, app_page):
super().__init__()
self.app_page = app_page
- self.get_style_context().add_class('control_panel')
+ self.get_style_context().add_class("control_panel")
self.settings_item = SettingsEntry(self.app_page.desktop_file_manager)
@@ -554,11 +595,12 @@ class KeynavController:
not enough, namely, when we have a bunch of ListBoxes stacked on top of
each other.
"""
+
def __init__(self, widgets_in_order: List[Gtk.ListBox]):
self.widgets_in_order = widgets_in_order
for widget in self.widgets_in_order:
- widget.connect('keynav-failed', self._keynav_failed)
+ widget.connect("keynav-failed", self._keynav_failed)
def get_neighbor(self, widget: Gtk.ListBox, direction: Gtk.DirectionType):
"""Get next widget in given direction"""
@@ -569,8 +611,7 @@ def get_neighbor(self, widget: Gtk.ListBox, direction: Gtk.DirectionType):
return self.widgets_in_order[(i + 1) % len(self.widgets_in_order)]
return None
- def _keynav_failed(self, widget: Gtk.ListBox,
- direction: Gtk.DirectionType):
+ def _keynav_failed(self, widget: Gtk.ListBox, direction: Gtk.DirectionType):
"""
Callback to be performed when keyboard nav fails. Attempts to
find next widget and move keyboard focus to it.
@@ -579,7 +620,8 @@ def _keynav_failed(self, widget: Gtk.ListBox,
if not next_widget:
return
next_focus_widget = get_visible_child(
- next_widget, reverse=direction == Gtk.DirectionType.UP)
+ next_widget, reverse=direction == Gtk.DirectionType.UP
+ )
if next_focus_widget:
widget.select_row(None)
next_focus_widget.grab_focus()
diff --git a/qubes_menu/desktop_file_manager.py b/qubes_menu/desktop_file_manager.py
index 6f03161..b8d1ee0 100644
--- a/qubes_menu/desktop_file_manager.py
+++ b/qubes_menu/desktop_file_manager.py
@@ -36,7 +36,7 @@
from . import constants
-logger = logging.getLogger('qubes-appmenu')
+logger = logging.getLogger("qubes-appmenu")
def exec_parse(desktop_entry: xdg.DesktopEntry.DesktopEntry):
@@ -47,13 +47,12 @@ def exec_parse(desktop_entry: xdg.DesktopEntry.DesktopEntry):
split_str = shlex.split(desktop_entry.getExec())
result = []
for s in split_str:
- if s in ['%f', '%F', '%u', '%U', '%d', '%D', '%n', '%N', '%v',
- '%m', '%k']:
+ if s in ["%f", "%F", "%u", "%U", "%d", "%D", "%n", "%N", "%v", "%m", "%k"]:
continue
- if s == '%i' and desktop_entry.getIcon():
- result.extend(['--icon', desktop_entry.getIcon()])
+ if s == "%i" and desktop_entry.getIcon():
+ result.extend(["--icon", desktop_entry.getIcon()])
continue
- if s == '%c':
+ if s == "%c":
result.append(desktop_entry.getName())
continue
result.append(s)
@@ -64,6 +63,7 @@ class ApplicationInfo:
"""
Class representing data within a single .desktop file.
"""
+
def __init__(self, qapp, file_path):
self.qapp: qubesadmin.Qubes = qapp
self.file_path: PosixPath = file_path
@@ -81,20 +81,20 @@ def __init__(self, qapp, file_path):
def load_data(self, entry):
"""Fill own data with information from xdg.DesktopEntry provided."""
- vm_name = entry.get('X-Qubes-VmName') or None
+ vm_name = entry.get("X-Qubes-VmName") or None
try:
self.vm = self.qapp.domains[vm_name]
except KeyError:
self.vm = None
- self.app_name = entry.getName() or ''
+ self.app_name = entry.getName() or ""
if self.vm:
self.app_name = self.app_name.split(": ", 1)[-1]
self.sort_name = str(self.app_name).lower()
self.vm_icon = self.vm.icon if self.vm else None
self.app_icon = entry.getIcon()
- self.disposable = bool(entry.get('X-Qubes-NonDispvmExec'))
- self.entry_name = entry.get('X-Qubes-AppName') or self.file_path.name
+ self.disposable = bool(entry.get("X-Qubes-NonDispvmExec"))
+ self.entry_name = entry.get("X-Qubes-AppName") or self.file_path.name
if self.disposable:
self.entry_name = constants.DISPOSABLE_PREFIX + self.entry_name
self.exec = exec_parse(entry)
@@ -111,35 +111,40 @@ def get_command_for_vm(self, vm=None):
their own .desktop files."""
command = self.exec
if vm and not self.vm:
- logger.warning('Unexpected command: cannot run local'
- ' application for a non-local VM: %s', vm)
+ logger.warning(
+ "Unexpected command: cannot run local"
+ " application for a non-local VM: %s",
+ vm,
+ )
return command
if vm and str(self.vm) != str(vm):
# replace name of the old VM - used for opening apps from DVM
# template in their child dispvm
if len(command) < 6 or command[5] != str(self.vm):
- logger.error(
- 'Unexpected command for a disposable VM: %s', command)
+ logger.error("Unexpected command for a disposable VM: %s", command)
return []
return command[:5] + [str(vm)] + command[6:]
return command
def is_qubes_specific(self):
"""Check if the current file represents a qubes-generated app."""
- return 'X-Qubes-VM' in self.categories
+ return "X-Qubes-VM" in self.categories
class DesktopFileManager:
"""
Class that loads, caches and observes changes in .desktop files.
"""
+
desktop_dirs = [
- Path(xdg.BaseDirectory.xdg_data_home) / 'applications',
- Path('/usr/share/applications')]
+ Path(xdg.BaseDirectory.xdg_data_home) / "applications",
+ Path("/usr/share/applications"),
+ ]
# pylint: disable=invalid-name
class EventProcessor(pyinotify.ProcessEvent):
"""pyinotify helper class"""
+
def __init__(self, parent):
self.parent = parent
super().__init__()
@@ -183,8 +188,7 @@ def __init__(self, qapp):
# directories used by Qubes menu tools, not necessarily all possible
# XDG directories
- self.current_environments = \
- os.environ.get('XDG_CURRENT_DESKTOP', '').split(':')
+ self.current_environments = os.environ.get("XDG_CURRENT_DESKTOP", "").split(":")
self.app_entries: Dict[Path, ApplicationInfo] = {}
@@ -250,14 +254,13 @@ def load_file(self, path: Union[str, Path]):
self.remove_file(path)
return
- if not path.name.endswith('.desktop'):
+ if not path.name.endswith(".desktop"):
return
try:
entry = xdg.DesktopEntry.DesktopEntry(path)
except Exception as ex: # pylint: disable=broad-except
- logger.warning(
- 'Cannot load desktop entry file %s: %s', path, str(ex))
+ logger.warning("Cannot load desktop entry file %s: %s", path, str(ex))
self.remove_file(path)
return
@@ -286,14 +289,12 @@ def _eligibility_check(self, entry: xdg.DesktopEntry.DesktopEntry):
if entry.getNoDisplay():
return False
if entry.getOnlyShowIn():
- if not set(entry.getOnlyShowIn()).intersection(
- self.current_environments):
+ if not set(entry.getOnlyShowIn()).intersection(self.current_environments):
return False
if entry.getNotShowIn():
- if set(entry.getNotShowIn()).intersection(
- self.current_environments):
+ if set(entry.getNotShowIn()).intersection(self.current_environments):
return False
- if entry.get('X-AppStream-Ignore'):
+ if entry.get("X-AppStream-Ignore"):
return False
return True
@@ -304,17 +305,23 @@ def _initialize_watchers(self):
self.watch_manager = pyinotify.WatchManager()
# pylint: disable=no-member
- mask = pyinotify.IN_CREATE | pyinotify.IN_DELETE | \
- pyinotify.IN_MODIFY | pyinotify.IN_MOVED_FROM | \
- pyinotify.IN_MOVED_TO
+ mask = (
+ pyinotify.IN_CREATE
+ | pyinotify.IN_DELETE
+ | pyinotify.IN_MODIFY
+ | pyinotify.IN_MOVED_FROM
+ | pyinotify.IN_MOVED_TO
+ )
loop = asyncio.get_event_loop()
self.notifier = pyinotify.AsyncioNotifier(
- self.watch_manager, loop,
- default_proc_fun=DesktopFileManager.EventProcessor(self))
+ self.watch_manager,
+ loop,
+ default_proc_fun=DesktopFileManager.EventProcessor(self),
+ )
for path in self.desktop_dirs:
self.watches.append(
- self.watch_manager.add_watch(
- str(path), mask, rec=True, auto_add=True))
+ self.watch_manager.add_watch(str(path), mask, rec=True, auto_add=True)
+ )
diff --git a/qubes_menu/favorites_page.py b/qubes_menu/favorites_page.py
index b31e1ab..89a294e 100644
--- a/qubes_menu/favorites_page.py
+++ b/qubes_menu/favorites_page.py
@@ -21,6 +21,7 @@
Qubes App Menu favorites page and related widgets.
"""
import logging
+from functools import partial
import qubesadmin.events
from .desktop_file_manager import DesktopFileManager
@@ -30,45 +31,89 @@
from . import constants
import gi
-gi.require_version('Gtk', '3.0')
+
+gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
-logger = logging.getLogger('qubes-appmenu')
+logger = logging.getLogger("qubes-appmenu")
class FavoritesPage(MenuPage):
"""
Helper class for managing the entirety of Favorites menu page.
"""
- def __init__(self, qapp: qubesadmin.Qubes, builder: Gtk.Builder,
- desktop_file_manager: DesktopFileManager,
- dispatcher: qubesadmin.events.EventsDispatcher,
- vm_manager: VMManager):
+
+ def __init__(
+ self,
+ qapp: qubesadmin.Qubes,
+ builder: Gtk.Builder,
+ desktop_file_manager: DesktopFileManager,
+ dispatcher: qubesadmin.events.EventsDispatcher,
+ vm_manager: VMManager,
+ ):
self.qapp = qapp
self.desktop_file_manager = desktop_file_manager
self.dispatcher = dispatcher
self.vm_manager = vm_manager
- self.page_widget: Gtk.Box = builder.get_object("favorites_page")
+ self.sort_names: bool | None = None # sort by app name, AZ (True) or ZA (False)
+ self.sort_qubes: bool | None = (
+ None # sort by qube name, AZ (True) or ZA (False)
+ )
- self.app_list: Gtk.ListBox = builder.get_object('fav_app_list')
- self.app_list.connect('row-activated', self._app_clicked)
+ self.page_widget: Gtk.Box = builder.get_object("favorites_page")
- self.app_list.set_sort_func(
- lambda x, y: x.app_info.sort_name > y.app_info.sort_name)
+ self.sort_qube_az_button: Gtk.ToggleButton = builder.get_object(
+ "favorites_qube_az_toggle"
+ )
+ self.sort_qube_za_button: Gtk.ToggleButton = builder.get_object(
+ "favorites_qube_za_toggle"
+ )
+ self.sort_appname_az_button: Gtk.ToggleButton = builder.get_object(
+ "favorites_appname_az_toggle"
+ )
+ self.sort_appname_za_button: Gtk.ToggleButton = builder.get_object(
+ "favorites_appname_za_toggle"
+ )
+ self.sort_buttons = [
+ self.sort_appname_za_button,
+ self.sort_appname_az_button,
+ self.sort_qube_az_button,
+ self.sort_qube_za_button,
+ ]
+ self.sort_qube_az_button.connect(
+ "toggled", partial(self._button_toggled, "sort_qubes", True)
+ )
+ self.sort_qube_za_button.connect(
+ "toggled", partial(self._button_toggled, "sort_qubes", False)
+ )
+ self.sort_appname_az_button.connect(
+ "toggled", partial(self._button_toggled, "sort_names", True)
+ )
+ self.sort_appname_za_button.connect(
+ "toggled", partial(self._button_toggled, "sort_names", False)
+ )
+
+ self.app_list: Gtk.ListBox = builder.get_object("fav_app_list")
+ self.app_list.connect("row-activated", self._app_clicked)
+
+ self.app_list.set_sort_func(self._favorites_sort)
self.desktop_file_manager.register_callback(self._app_info_callback)
self.app_list.show_all()
self.app_list.invalidate_sort()
self.app_list.set_selection_mode(Gtk.SelectionMode.NONE)
self.dispatcher.add_handler(
- f'domain-feature-delete:{constants.FAVORITES_FEATURE}',
- self._feature_deleted)
+ f"domain-feature-delete:{constants.FAVORITES_FEATURE}",
+ self._feature_deleted,
+ )
self.dispatcher.add_handler(
- f'domain-feature-set:{constants.FAVORITES_FEATURE}',
- self._feature_set)
- self.dispatcher.add_handler('domain-add', self._domain_added)
- self.dispatcher.add_handler('domain-delete', self._domain_deleted)
+ f"domain-feature-set:{constants.FAVORITES_FEATURE}", self._feature_set
+ )
+ self.dispatcher.add_handler("domain-add", self._domain_added)
+ self.dispatcher.add_handler("domain-delete", self._domain_deleted)
+
+ self.sort_appname_az_button.toggled()
def _load_vms_favorites(self, vm):
"""
@@ -80,14 +125,15 @@ def _load_vms_favorites(self, vm):
vm = self.qapp.domains[vm]
except KeyError:
return
- favorites = vm.features.get(constants.FAVORITES_FEATURE, '')
- favorites = favorites.split(' ')
+ favorites = vm.features.get(constants.FAVORITES_FEATURE, "")
+ favorites = favorites.split(" ")
is_local_vm = vm.name == self.qapp.local_name
for app_info in self.desktop_file_manager.get_app_infos():
- if (not is_local_vm and app_info.vm == vm)\
- or (is_local_vm and not app_info.vm):
+ if (not is_local_vm and app_info.vm == vm) or (
+ is_local_vm and not app_info.vm
+ ):
if app_info.entry_name in favorites:
self._add_from_app_info(app_info)
self.app_list.invalidate_sort()
@@ -100,7 +146,7 @@ def _app_info_callback(self, app_info):
else:
vm = app_info.qapp.domains[app_info.qapp.local_name]
- feature = vm.features.get(constants.FAVORITES_FEATURE, '').split(' ')
+ feature = vm.features.get(constants.FAVORITES_FEATURE, "").split(" ")
if app_info.entry_name in feature:
self._add_from_app_info(app_info)
@@ -124,8 +170,7 @@ def _feature_deleted(self, vm, _event, _feature, *_args, **_kwargs):
self.app_list.remove(child)
self.app_list.invalidate_sort()
except Exception as ex: # pylint: disable=broad-except
- logger.warning(
- 'Encountered problem removing favorite entry: %s', repr(ex))
+ logger.warning("Encountered problem removing favorite entry: %s", repr(ex))
def _feature_set(self, vm, event, feature, *_args, **_kwargs):
"""When VM feature specified in constants.py is changed, all existing
@@ -135,8 +180,7 @@ def _feature_set(self, vm, event, feature, *_args, **_kwargs):
self._feature_deleted(vm, event, feature)
self._load_vms_favorites(vm)
except Exception as ex: # pylint: disable=broad-except
- logger.warning(
- 'Encountered problem adding favorite entry: %s', repr(ex))
+ logger.warning("Encountered problem adding favorite entry: %s", repr(ex))
def _domain_added(self, _submitter, _event, vm, **_kwargs):
"""On a newly created domain, load all favorites from features
@@ -150,3 +194,34 @@ def _domain_deleted(self, _submitter, event, vm, **_kwargs):
def initialize_page(self):
"""Favorites page does not require additional post-init setup"""
+
+ def _button_toggled(self, var_name, state, widget, *_args):
+ if not widget.get_active():
+ return
+ self.sort_names = None
+ self.sort_qubes = None
+ setattr(self, var_name, state)
+ for button in self.sort_buttons:
+ if button == widget:
+ continue
+ button.set_active(False)
+ self.app_list.invalidate_sort()
+
+ def _favorites_sort(self, x: FavoritesAppEntry, y: FavoritesAppEntry):
+ sort_name_x = x.app_info.app_name or ""
+ sort_name_y = y.app_info.app_name or ""
+
+ if self.sort_qubes is not None:
+ sort_name_x = (
+ (x.app_info.vm.name if x.app_info.vm else "") + " | " + sort_name_x
+ )
+ sort_name_y = (
+ (y.app_info.vm.name if y.app_info.vm else "") + " | " + sort_name_y
+ )
+ if self.sort_qubes:
+ return sort_name_x > sort_name_y
+ return sort_name_x < sort_name_y
+
+ if self.sort_names:
+ return sort_name_x > sort_name_y
+ return sort_name_x < sort_name_y
diff --git a/qubes_menu/page_handler.py b/qubes_menu/page_handler.py
index eca31c2..e2d1882 100644
--- a/qubes_menu/page_handler.py
+++ b/qubes_menu/page_handler.py
@@ -21,12 +21,14 @@
import abc
import gi
-gi.require_version('Gtk', '3.0')
+
+gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
class MenuPage(abc.ABC):
"""Abstract Menu Page."""
+
page_widget: Gtk.Widget
@abc.abstractmethod
diff --git a/qubes_menu/qubes-menu-base.css b/qubes_menu/qubes-menu-base.css
index e96cb52..ff5d0df 100644
--- a/qubes_menu/qubes-menu-base.css
+++ b/qubes_menu/qubes-menu-base.css
@@ -78,6 +78,24 @@ list :not(:selected) {
font-weight: 900;
}
+.sort_buttons {
+ background: @left-background;
+ opacity: 0.9;
+ border-width: 0px 0px 0px 2px;
+ border-color: @left-background;
+ border-radius: 0px;
+ margin: 4px 10px 4px 2px;
+ padding: 5px;
+}
+
+.sort_buttons:checked {
+ border-color: @text-color;
+}
+
+.sort_buttons:hover, .sort_buttons:focus {
+ transition: none;
+}
+
.vm_entry {
padding: 10px;
}
diff --git a/qubes_menu/qubes-menu.glade b/qubes_menu/qubes-menu.glade
index 91d1001..c2acb62 100644
--- a/qubes_menu/qubes-menu.glade
+++ b/qubes_menu/qubes-menu.glade
@@ -1,7 +1,7 @@
-
+
1
- 3
+ 2
-
+
True
- True
+ False
True
- True
- never
- in
+ vertical
-
+
True
False
+ start
+ Recent applications
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ True
+ True
+ never
+ in
+ True
-
+
True
False
-
-
+ True
+
+
True
False
- start
start
- No recent searches
+ True
+
+
+ True
+ False
+ start
+ start
+ True
+ No recent applications
+
+
+
-
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ True
+ False
+ start
+ Recently searched
+
+
+ False
+ True
+ 2
+
+
+
+
+ True
+ True
+ True
+ True
+ never
+ in
+
+
+ True
+ False
+ True
+
+
+ True
+ False
+ start
+ True
+
+
+ True
+ False
+ start
+ start
+ True
+ No recent searches
+
+
+
+
+
+
+
+
+
+
+
+ False
+ True
+ 3
+
-
-
-
- 0
- 2
- 2
-
-
-
-
- True
- False
- start
- Recently searched
-
0
@@ -178,9 +259,6 @@
2
-
-
-
@@ -483,8 +561,117 @@ To add an application to favorites, right-click on it in the application list.
-
+
+ True
+ False
+ start
+ vertical
+
+
+ True
+ True
+ True
+ Sort by application name (A-Z)
+
+
+ True
+ False
+ qappmenu-az
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ True
+ True
+ Sort by application name (Z-A)
+
+
+ True
+ False
+ qappmenu-za
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ True
+ True
+ True
+ Sort by qube and application name (A-Z)
+
+
+ True
+ False
+ qappmenu-qube-az
+
+
+
+
+
+ False
+ True
+ 2
+
+
+
+
+ True
+ True
+ True
+ Sort by qube and application name (Z-A)
+
+
+ True
+ False
+ qappmenu-qube-za
+
+
+
+
+
+ False
+ True
+ 3
+
+
+
+
+ False
+ True
+ 1
+
+
2
diff --git a/qubes_menu/search_page.py b/qubes_menu/search_page.py
index 2a085a2..15ee7a2 100644
--- a/qubes_menu/search_page.py
+++ b/qubes_menu/search_page.py
@@ -28,7 +28,8 @@
from .utils import load_icon, parse_search
import gi
-gi.require_version('Gtk', '3.0')
+
+gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk
@@ -36,32 +37,71 @@ class RecentSearchRow(Gtk.ListBoxRow):
"""
Gtk.ListBoxRow with a recently searched text.
"""
+
def __init__(self, search_text: str):
super().__init__()
self.search_text = search_text
self.hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
- self.recent_icon = Gtk.Image.new_from_pixbuf(
- load_icon('qappmenu-search'))
+ self.recent_icon = Gtk.Image.new_from_pixbuf(load_icon("qappmenu-search"))
self.hbox.pack_start(self.recent_icon, False, False, 5)
self.search_label = Gtk.Label(label=search_text, xalign=0)
self.hbox.pack_start(self.search_label, False, False, 5)
- self.get_style_context().add_class('app_entry')
+ self.get_style_context().add_class("app_entry")
self.add(self.hbox)
self.show_all()
class RecentSearchManager:
"""Class for managing the list of recent searches."""
+
SEARCH_VALUES_TO_KEEP = 10
- def __init__(self, recent_list: Gtk.ListBox, search_box: Gtk.SearchEntry):
+
+ def __init__(
+ self,
+ recent_list: Gtk.ListBox,
+ search_box: Gtk.SearchEntry,
+ enabled: bool,
+ other_widgets: list[Gtk.ListBox],
+ ):
+ self.recent_enabled = enabled
self.recent_list_box = recent_list
self.search_box = search_box
self.recent_searches: Dict[str, RecentSearchRow] = {}
- self.recent_list_box.connect('row-activated', self._row_clicked)
+ self.other_widgets = other_widgets
+ self.recent_list_box.connect("row-activated", self._row_clicked)
+ self.recent_list_box.connect("row-selected", self._deselect_others)
+
+ def _deselect_others(self, *_args):
+ for widget in self.other_widgets:
+ widget.select_row(None)
+
+ def set_recent_enabled(self, state):
+ """Set whether recent searches should be stored or not."""
+ self.recent_enabled = state
+ self.recent_searches.clear()
+ for child in self.recent_list_box.get_children():
+ self.recent_list_box.remove(child)
+
+ label = Gtk.Label()
+ label.get_style_context().add_class("placeholder")
+ label.set_visible(True)
+ label.set_halign(Gtk.Align.START)
+ label.set_valign(Gtk.Align.START)
+
+ if state:
+ label.set_text("No recent searches")
+ else:
+ label.set_text(
+ "Recent searches saving disabled.\nUse Menu Settings to enable."
+ )
+
+ self.recent_list_box.set_placeholder(label)
def add_new_recent_search(self, text: str):
"""Add new recent search entry"""
+ if not self.recent_enabled:
+ return
if not text:
return
@@ -72,7 +112,7 @@ def add_new_recent_search(self, text: str):
self.recent_list_box.insert(old_row, 0)
return
- if len(self.recent_searches) == self.SEARCH_VALUES_TO_KEEP:
+ if len(self.recent_searches) == self.SEARCH_VALUES_TO_KEEP + 1:
last_row: RecentSearchRow = self.recent_list_box.get_children()[-1]
del self.recent_searches[last_row.search_text]
self.recent_list_box.remove(last_row)
@@ -82,15 +122,104 @@ def add_new_recent_search(self, text: str):
self.recent_searches[text] = row
def _row_clicked(self, _widget, row: RecentSearchRow):
+ self._deselect_others()
self.search_box.set_text(row.search_text)
+class RecentAppsManager:
+ """Class for managing recently run apps"""
+
+ APPS_TO_KEEP = 10
+
+ def __init__(
+ self,
+ recent_list: Gtk.ListBox,
+ desktop_file_manager: DesktopFileManager,
+ vm_manager: VMManager,
+ enabled: bool,
+ other_widgets: list[Gtk.ListBox],
+ ):
+ self.recent_enabled = enabled
+ self.recent_list_box = recent_list
+ self.desktop_file_manager = desktop_file_manager
+ self.vm_manager = vm_manager
+ self.recent_apps: list[SearchAppEntry] = []
+ self.recent_list_box.connect("row-activated", self._row_clicked)
+ application = self.recent_list_box.get_toplevel().get_application()
+ if application:
+ # this is a workaround for tests: without Gtk.Application.run,
+ # this object does not exist
+ application.connect("app-started", self.add_new_recent_app)
+ self.other_widgets = other_widgets
+ self.recent_list_box.connect("row-selected", self._deselect_others)
+
+ def _deselect_others(self, *_args):
+ for widget in self.other_widgets:
+ widget.select_row(None)
+
+ def set_recent_enabled(self, state):
+ """Set whether recent apps should be stored or not."""
+ self.recent_enabled = state
+ self.recent_apps.clear()
+ for child in self.recent_list_box.get_children():
+ self.recent_list_box.remove(child)
+
+ label = Gtk.Label()
+ label.get_style_context().add_class("placeholder")
+ label.set_visible(True)
+ label.set_halign(Gtk.Align.START)
+ label.set_valign(Gtk.Align.START)
+
+ if state:
+ label.set_text("No recent applications")
+ else:
+ label.set_text(
+ "Recent application saving disabled.\nUse Menu Settings to enable."
+ )
+
+ self.recent_list_box.set_placeholder(label)
+
+ def add_new_recent_app(self, _widget, app_path: str):
+ """Add new "recent" record, based on .desktop file path given as string"""
+ if not self.recent_enabled:
+ return
+
+ # only add if not exists, if exists: bump to top and return
+ for app_entry in self.recent_apps:
+ if app_entry.app_info.file_path.name == app_path:
+ self.recent_list_box.remove(app_entry)
+ self.recent_list_box.insert(app_entry, 0)
+ return
+
+ app_info = self.desktop_file_manager.get_app_info_by_name(app_path)
+ if not app_info:
+ return
+ new_entry = SearchAppEntry(app_info, self.vm_manager)
+ self.recent_apps.append(new_entry)
+ self.recent_list_box.insert(new_entry, 0)
+
+ if len(self.recent_apps) == self.APPS_TO_KEEP + 1:
+ last_row: SearchAppEntry = self.recent_list_box.get_children()[-1]
+ del self.recent_apps[last_row]
+ self.recent_list_box.remove(last_row)
+
+ def _row_clicked(self, _widget, row: SearchAppEntry):
+ self._deselect_others()
+ if hasattr(row, "app_info"):
+ row.run_app(row.app_info.vm)
+
+
class SearchPage(MenuPage):
"""
Helper class for managing the Search menu page.
"""
- def __init__(self, vm_manager: VMManager, builder: Gtk.Builder,
- desktop_file_manager: DesktopFileManager):
+
+ def __init__(
+ self,
+ vm_manager: VMManager,
+ builder: Gtk.Builder,
+ desktop_file_manager: DesktopFileManager,
+ ):
"""
:param vm_manager: VM Manager object
:param builder: Gtk.Builder with loaded glade object
@@ -102,23 +231,24 @@ def __init__(self, vm_manager: VMManager, builder: Gtk.Builder,
self.page_widget: Gtk.Grid = builder.get_object("search_page")
self.sort_running = False # sort running vms to top
+ self.recent_enabled = True
- self.vm_list: Gtk.ListBox = builder.get_object('search_vm_list')
- self.app_list: Gtk.ListBox = builder.get_object('search_app_list')
- self.search_entry: Gtk.SearchEntry = builder.get_object('search_entry')
+ self.vm_list: Gtk.ListBox = builder.get_object("search_vm_list")
+ self.app_list: Gtk.ListBox = builder.get_object("search_app_list")
+ self.search_entry: Gtk.SearchEntry = builder.get_object("search_entry")
self.selected_vm_row: Optional[SearchVMRow] = None
self.filtered_vms: Set[str] = set()
- self.main_notebook = builder.get_object('main_notebook')
+ self.main_notebook = builder.get_object("main_notebook")
- self.search_entry.connect('search-changed', self._do_search)
- self.search_entry.connect('key-press-event', self._search_key_press)
+ self.search_entry.connect("search-changed", self._do_search)
+ self.search_entry.connect("key-press-event", self._search_key_press)
desktop_file_manager.register_callback(self._app_info_callback)
self.app_list.set_filter_func(self._is_app_fitting)
- self.app_list.connect('row-activated', self._app_clicked)
+ self.app_list.connect("row-activated", self._app_clicked)
self.vm_list.add(AnyVMRow())
vm_manager.register_new_vm_callback(self._vm_callback)
@@ -129,37 +259,47 @@ def __init__(self, vm_manager: VMManager, builder: Gtk.Builder,
self.app_list.invalidate_sort()
self.vm_list.invalidate_sort()
- self.recent_list: Gtk.ListBox = builder.get_object('search_recent_list')
+ self.recent_list: Gtk.ListBox = builder.get_object("search_recent_list")
+ self.recent_app_list: Gtk.ListBox = builder.get_object(
+ "search_recent_apps_list"
+ )
- self.app_view: Gtk.ScrolledWindow = \
- builder.get_object("search_app_view")
- self.app_placeholder: Gtk.Label = \
- builder.get_object('search_app_placeholder')
+ self.app_view: Gtk.ScrolledWindow = builder.get_object("search_app_view")
+ self.app_placeholder: Gtk.Label = builder.get_object("search_app_placeholder")
self.vm_view: Gtk.ScrolledWindow = builder.get_object("search_vm_view")
- self.recent_view: Gtk.ScrolledWindow = \
- builder.get_object("search_recent_view")
- self.recent_title: Gtk.Label = builder.get_object('search_recent_title')
+ self.recent_box: Gtk.Box = builder.get_object("search_no_box")
self.recent_search_manager = RecentSearchManager(
- self.recent_list, self.search_entry)
-
- self.vm_list.connect('row-selected', self._selection_changed)
- self.search_entry.connect('activate', self._move_to_first)
+ self.recent_list,
+ self.search_entry,
+ self.recent_enabled,
+ [self.recent_app_list],
+ )
+ self.recent_apps_manager = RecentAppsManager(
+ self.recent_app_list,
+ self.desktop_file_manager,
+ self.vm_manager,
+ self.recent_enabled,
+ [self.recent_list],
+ )
+
+ self.vm_list.connect("row-selected", self._selection_changed)
+ self.search_entry.connect("activate", self._move_to_first)
self.control_list = ControlList(self)
self.page_widget.attach(self.control_list, 1, 4, 1, 1)
- self.control_list.connect('row-activated', self._app_clicked)
+ self.control_list.connect("row-activated", self._app_clicked)
self.control_list.set_selection_mode(Gtk.SelectionMode.NONE)
self.keynav_manager = KeynavController(
- widgets_in_order=[self.app_list, self.control_list])
+ widgets_in_order=[self.app_list, self.control_list]
+ )
def _app_clicked(self, _widget, row):
- self.recent_search_manager.add_new_recent_search(
- self.search_entry.get_text())
+ self.recent_search_manager.add_new_recent_search(self.search_entry.get_text())
if self.selected_vm_row:
row.run_app(self.selected_vm_row.vm_entry.vm)
- elif hasattr(row, 'app_info'):
+ elif hasattr(row, "app_info"):
row.run_app(row.app_info.vm)
def _app_info_callback(self, app_info):
@@ -184,13 +324,11 @@ def _vm_callback(self, vm_entry: VMEntry):
def _do_search(self, *_args):
has_search = bool(self.search_entry.get_text())
self.app_view.set_visible(has_search)
- self.recent_view.set_visible(not has_search)
- self.recent_title.set_visible(not has_search)
+ self.recent_box.set_visible(not has_search)
self._filter_lists()
- self.vm_view.set_visible(has_search and
- not self.app_placeholder.get_mapped())
+ self.vm_view.set_visible(has_search and not self.app_placeholder.get_mapped())
if not self.app_placeholder.get_mapped():
for row in self.app_list.get_children():
@@ -297,17 +435,49 @@ def initialize_page(self):
"""
Initialize own state.
"""
- self.search_entry.set_text('')
+ self.search_entry.set_text("")
self.app_list.select_row(None)
self.vm_list.select_row(None)
self.app_view.set_visible(False)
self.vm_view.set_visible(False)
- self.recent_view.set_visible(True)
+ self.recent_box.set_visible(True)
self._filter_lists()
self.search_entry.grab_focus_without_selecting()
+ def enable_recent(self, state: bool):
+ """Enable/disable storing recent apps/searches"""
+ self.recent_enabled = state
+ self.recent_search_manager.set_recent_enabled(state)
+ self.recent_apps_manager.set_recent_enabled(state)
+
+ app_label = Gtk.Label()
+ app_label.get_style_context().add_class("placeholder")
+ search_label = Gtk.Label()
+ search_label.get_style_context().add_class("placeholder")
+ app_label.set_visible(True)
+ search_label.set_visible(True)
+
+ if state:
+ app_label.set_text(
+ "No recent applications. \nUse Menu Settings to "
+ "disable recent applications."
+ )
+ search_label.set_text(
+ "No recent searches. \nUse Menu Settings to disable recent searches."
+ )
+ else:
+ app_label.set_text(
+ "Recent application saving disabled.\nUse Menu Settings to enable."
+ )
+ search_label.set_text(
+ "Recent searches saving disabled.\nUse Menu Settings to enable."
+ )
+
+ self.recent_app_list.set_placeholder(app_label)
+ self.recent_list.set_placeholder(search_label)
+
def reset_page(self):
"""Reset page after hiding the menu."""
self.initialize_page()
diff --git a/qubes_menu/settings_page.py b/qubes_menu/settings_page.py
index cd69428..ffc6351 100644
--- a/qubes_menu/settings_page.py
+++ b/qubes_menu/settings_page.py
@@ -28,7 +28,8 @@
from .page_handler import MenuPage
import gi
-gi.require_version('Gtk', '3.0')
+
+gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
@@ -37,13 +38,14 @@ class SettingsCategoryRow(custom_widgets.HoverListBox):
A custom widget representing a category of Settings; selects itself
on hover.
"""
+
def __init__(self, name, filter_func):
super().__init__()
self.name = name
self.label = custom_widgets.LimitedWidthLabel(self.name)
self.main_box.add(self.label)
self.filter_func = filter_func
- self.get_style_context().add_class('settings_category_row')
+ self.get_style_context().add_class("settings_category_row")
self.show_all()
@@ -51,31 +53,37 @@ class SettingsPage(MenuPage):
"""
Helper class for managing the entirety of Settings menu page.
"""
- def __init__(self, qapp, builder: Gtk.Builder,
- desktop_file_manager: DesktopFileManager,
- dispatcher: qubesadmin.events.EventsDispatcher):
+
+ def __init__(
+ self,
+ qapp,
+ builder: Gtk.Builder,
+ desktop_file_manager: DesktopFileManager,
+ dispatcher: qubesadmin.events.EventsDispatcher,
+ ):
self.qapp = qapp
self.desktop_file_manager = desktop_file_manager
self.dispatcher = dispatcher
self.page_widget: Gtk.Box = builder.get_object("settings_page")
- self.app_list: Gtk.ListBox = builder.get_object('sys_tools_list')
- self.app_list.connect('row-activated', self._app_clicked)
+ self.app_list: Gtk.ListBox = builder.get_object("sys_tools_list")
+ self.app_list.connect("row-activated", self._app_clicked)
self.app_list.set_sort_func(
- lambda x, y: x.app_info.sort_name > y.app_info.sort_name)
+ lambda x, y: x.app_info.sort_name > y.app_info.sort_name
+ )
self.app_list.set_filter_func(self._filter_apps)
- self.category_list: Gtk.ListBox = builder.get_object(
- 'settings_categories')
+ self.category_list: Gtk.ListBox = builder.get_object("settings_categories")
- self.category_list.connect('row-selected', self._category_clicked)
- self.category_list.add(SettingsCategoryRow('Qubes Tools',
- self._filter_qubes_tools))
+ self.category_list.connect("row-selected", self._category_clicked)
+ self.category_list.add(
+ SettingsCategoryRow("Qubes Tools", self._filter_qubes_tools)
+ )
self.category_list.add(
- SettingsCategoryRow('System Settings',
- self._filter_system_settings))
- self.category_list.add(SettingsCategoryRow('Other', self._filter_other))
+ SettingsCategoryRow("System Settings", self._filter_system_settings)
+ )
+ self.category_list.add(SettingsCategoryRow("Other", self._filter_other))
self.desktop_file_manager.register_callback(self._app_info_callback)
@@ -88,30 +96,32 @@ def initialize_page(self):
self.category_list.select_row(None)
def _filter_apps(self, row):
- filter_func = getattr(self.category_list.get_selected_row(),
- 'filter_func', None)
+ filter_func = getattr(
+ self.category_list.get_selected_row(), "filter_func", None
+ )
if not filter_func:
return False
return filter_func(row)
@staticmethod
def _filter_qubes_tools(row):
- if 'X-XFCE-SettingsDialog' not in row.app_info.categories:
+ if "X-XFCE-SettingsDialog" not in row.app_info.categories:
return False
- return 'qubes' in row.app_info.entry_name
+ return "qubes" in row.app_info.entry_name
@staticmethod
def _filter_system_settings(row):
- if 'X-XFCE-SettingsDialog' in row.app_info.categories:
- return 'qubes' not in row.app_info.entry_name
- if 'Settings' in row.app_info.categories:
+ if "X-XFCE-SettingsDialog" in row.app_info.categories:
+ return "qubes" not in row.app_info.entry_name
+ if "Settings" in row.app_info.categories:
return True
return False
@staticmethod
def _filter_other(row):
- return not SettingsPage._filter_qubes_tools(row) and \
- not SettingsPage._filter_system_settings(row)
+ return not SettingsPage._filter_qubes_tools(
+ row
+ ) and not SettingsPage._filter_system_settings(row)
def _category_clicked(self, *_args):
self.app_list.invalidate_filter()
diff --git a/qubes_menu/tests/test_appmenu.py b/qubes_menu/tests/test_appmenu.py
index 766582e..b51ccf7 100644
--- a/qubes_menu/tests/test_appmenu.py
+++ b/qubes_menu/tests/test_appmenu.py
@@ -29,6 +29,7 @@ def test_app_menu_conffeatures():
qapp._qubes['dom0'].features['menu-initial-page'] = 'favorites_page'
qapp._qubes['dom0'].features['menu-sort-running'] = '1'
qapp._qubes['dom0'].features['menu-position'] = ''
+ qapp._qubes['dom0'].features['menu-disable-recent'] = '1'
qapp.update_vm_calls()
dispatcher = MockDispatcher(qapp)
@@ -40,6 +41,7 @@ def test_app_menu_conffeatures():
assert app_menu.initial_page == "favorites_page"
assert app_menu.sort_running
assert app_menu.appmenu_position == "mouse"
+ assert app_menu.disable_recent == True
def test_app_menu_conffeatures_default():
@@ -51,7 +53,9 @@ def test_app_menu_conffeatures_default():
features={'menu-favorites': '',
'menu-initial-page': 'fake',
'menu-sort-running': 'fake',
- 'menu-position': 'fake'})
+ 'menu-position': 'fake',
+ 'menu-disable-recent': ''
+ })
qapp.update_vm_calls()
dispatcher = MockDispatcher(qapp)
@@ -63,6 +67,7 @@ def test_app_menu_conffeatures_default():
assert app_menu.initial_page == "app_page"
assert not app_menu.sort_running
assert app_menu.appmenu_position == "mouse"
+ assert not app_menu.disable_recent
def test_appmenu_options():
@@ -73,6 +78,7 @@ def test_appmenu_options():
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._qubes['dom0'].features['menu-disable-recent'] = ''
qapp.update_vm_calls()
dispatcher = MockDispatcher(qapp)
@@ -101,6 +107,7 @@ def test_appmenu_positioning():
qapp._qubes['dom0'].features['menu-initial-page'] = 'app_page'
qapp._qubes['dom0'].features['menu-sort-running'] = '1'
qapp._qubes['dom0'].features['menu-position'] = ''
+ qapp._qubes['dom0'].features['menu-disable-recent'] = ''
qapp.update_vm_calls()
dispatcher = MockDispatcher(qapp)
diff --git a/qubes_menu/tests/test_search.py b/qubes_menu/tests/test_search.py
index 308d61f..1dd0d42 100644
--- a/qubes_menu/tests/test_search.py
+++ b/qubes_menu/tests/test_search.py
@@ -24,7 +24,6 @@
from qubesadmin.tests.mock_app import MockDispatcher
from ..search_page import SearchPage
-
def test_search(test_desktop_file_path, test_qapp, test_builder):
dispatcher = MockDispatcher(test_qapp)
vm_manager = VMManager(test_qapp, dispatcher)
@@ -90,3 +89,71 @@ def test_search(test_desktop_file_path, test_qapp, test_builder):
if search_page._is_app_fitting(row)]
assert len(found_entries) == 1
assert found_entries[0].app_info.app_name == 'Xfce Appearance Settings'
+
+@mock.patch('gi.repository.Gtk.Application')
+def test_recent_searches(mock_application, 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]):
+ desktop_file_manager = DesktopFileManager(test_qapp)
+
+ search_page = SearchPage(vm_manager, test_builder, desktop_file_manager)
+
+ assert search_page.search_entry.get_sensitive()
+
+ search_page.search_entry.set_text('dragons')
+
+ # find a dom0 app
+ search_page.search_entry.set_text('dom0')
+
+ for row in search_page.app_list.get_children():
+ if search_page._is_app_fitting(row):
+ with mock.patch('subprocess.Popen') as mock_run, mock.patch.object(
+ row.get_toplevel(), 'get_application', side_effect=mock_application):
+ row.activate()
+ assert mock_run.call_count == 1
+ assert mock.call().emit('app-started', 'test3.desktop') in mock_application.mock_calls
+
+ # we are faking signals here
+ search_page.recent_apps_manager.add_new_recent_app(None, 'test3.desktop')
+
+ texts = [row.search_text for row in search_page.recent_list.get_children()]
+ assert texts == ['dom0']
+ apps = [row.app_info.entry_name for row in
+ search_page.recent_app_list.get_children()]
+ assert apps == ['test3.desktop']
+
+ # do two more searches, but one should be the same as an existing search
+ search_page.search_entry.set_text('')
+ search_page.search_entry.set_text('xTeRm')
+
+ for row in search_page.app_list.get_children():
+ if search_page._is_app_fitting(row):
+ with mock.patch('subprocess.Popen') as mock_run, mock.patch.object(
+ row.get_toplevel(), 'get_application', side_effect=mock_application):
+ row.activate()
+ assert mock_run.call_count == 1
+ assert mock.call().emit('app-started', 'test1.desktop') in mock_application.mock_calls
+
+ search_page.recent_apps_manager.add_new_recent_app(None, 'test1.desktop')
+ search_page.search_entry.set_text('')
+ search_page.search_entry.set_text('dom0')
+
+ for row in search_page.app_list.get_children():
+ if search_page._is_app_fitting(row):
+ with mock.patch('subprocess.Popen') as mock_run, mock.patch.object(
+ row.get_toplevel(), 'get_application', side_effect=mock_application):
+ row.activate()
+ assert mock_run.call_count == 1
+ assert mock.call().emit('app-started', 'test3.desktop') in mock_application.mock_calls
+
+ search_page.recent_apps_manager.add_new_recent_app(None, 'test3.desktop')
+
+ texts = [row.search_text for row in search_page.recent_list.get_children()]
+ assert texts == ['dom0', 'xTeRm']
+ apps = [row.app_info.entry_name for row in
+ search_page.recent_app_list.get_children()]
+ assert apps == ['test3.desktop', 'test1.desktop']
diff --git a/qubes_menu/utils.py b/qubes_menu/utils.py
index 885e0c0..5fed8a4 100644
--- a/qubes_menu/utils.py
+++ b/qubes_menu/utils.py
@@ -26,13 +26,15 @@
import qubesadmin.vm
-gi.require_version('Gtk', '3.0')
+gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GdkPixbuf, GLib
-def load_icon(icon_name,
- size: Optional[Gtk.IconSize] = Gtk.IconSize.LARGE_TOOLBAR,
- pixel_size: Optional[int] = None):
+def load_icon(
+ icon_name,
+ size: Optional[Gtk.IconSize] = Gtk.IconSize.LARGE_TOOLBAR,
+ pixel_size: Optional[int] = None,
+):
"""Load icon from provided name, if available. If not, attempt to treat
provided name as a path. If icon not found in any of the above ways,
load a blank icon of specified size.
@@ -49,30 +51,32 @@ def load_icon(icon_name,
try:
# icon name is a path
image: GdkPixbuf.Pixbuf = Gtk.IconTheme.get_default().load_icon(
- icon_name, width, Gtk.IconLookupFlags.FORCE_SIZE)
+ icon_name, width, Gtk.IconLookupFlags.FORCE_SIZE
+ )
return image
except (TypeError, GLib.Error):
# icon not found in any way
pixbuf: GdkPixbuf.Pixbuf = GdkPixbuf.Pixbuf.new(
- GdkPixbuf.Colorspace.RGB, True, 8, width, height)
+ GdkPixbuf.Colorspace.RGB, True, 8, width, height
+ )
pixbuf.fill(0x000)
return pixbuf
+
def show_error(title, text):
"""
Helper function to display error messages.
"""
- dialog = Gtk.MessageDialog(
- None, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK)
+ dialog = Gtk.MessageDialog(None, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK)
dialog.set_title(title)
dialog.set_markup(GLib.markup_escape_text(text))
dialog.connect("response", lambda *x: dialog.destroy())
dialog.show()
+
def parse_search(search_text: str) -> List[str]:
"""Parse search text into separate words"""
- search_words = search_text.lower().replace(
- '-', ' ').replace('_', ' ').split(' ')
+ search_words = search_text.lower().replace("-", " ").replace("_", " ").split(" ")
return [w for w in search_words if w]
@@ -92,8 +96,9 @@ def text_search(search_word: str, text_words: List[str]):
return 0
-def highlight_words(labels: List[Gtk.Label], search_words: List[str],
- hl_tag: Optional[str] = None) -> None:
+def highlight_words(
+ labels: List[Gtk.Label], search_words: List[str], hl_tag: Optional[str] = None
+) -> None:
"""Highlight provided search_words in the provided labels."""
if not labels:
return
@@ -123,13 +128,14 @@ def highlight_words(labels: List[Gtk.Label], search_words: List[str],
if not found_intervals:
continue
- found_intervals.sort(key= lambda x: x[0])
+ found_intervals.sort(key=lambda x: x[0])
result_intervals = [found_intervals[0]]
for interval in found_intervals[1:]:
if interval[0] <= result_intervals[-1][1]:
- result_intervals[-1] = \
- (result_intervals[-1][0],
- max(result_intervals[-1][1], interval[1]))
+ result_intervals[-1] = (
+ result_intervals[-1][0],
+ max(result_intervals[-1][1], interval[1]),
+ )
else:
result_intervals.append(interval)
@@ -139,7 +145,7 @@ def highlight_words(labels: List[Gtk.Label], search_words: List[str],
markup_list.append(GLib.markup_escape_text(text[last_start:start]))
markup_list.append(hl_tag)
markup_list.append(GLib.markup_escape_text(text[start:end]))
- markup_list.append('')
+ markup_list.append("")
last_start = end
markup_list.append(GLib.markup_escape_text(text[last_start:]))
@@ -166,24 +172,23 @@ def add_to_feature(vm: qubesadmin.vm.QubesVM, feature_name: str, text: str):
"""
current_feature = vm.features.get(feature_name)
if current_feature:
- feature_list = current_feature.split(' ')
+ feature_list = current_feature.split(" ")
else:
feature_list = []
if text in feature_list:
return
feature_list.append(text)
- vm.features[feature_name] = ' '.join(feature_list)
+ vm.features[feature_name] = " ".join(feature_list)
-def remove_from_feature(vm: qubesadmin.vm.QubesVM,
- feature_name: str, text: str):
+def remove_from_feature(vm: qubesadmin.vm.QubesVM, feature_name: str, text: str):
"""
Remove a given string to a feature containing a list of space-separated
strings.Can raise ValueError if ext was not found in the feature.
"""
- current_feature = vm.features.get(feature_name, '').split(' ')
+ current_feature = vm.features.get(feature_name, "").split(" ")
current_feature.remove(text)
- vm.features[feature_name] = ' '.join(current_feature)
+ vm.features[feature_name] = " ".join(current_feature)
diff --git a/qubes_menu/vm_manager.py b/qubes_menu/vm_manager.py
index 98953bd..7cb25d2 100644
--- a/qubes_menu/vm_manager.py
+++ b/qubes_menu/vm_manager.py
@@ -57,12 +57,8 @@ def __init__(self, vm: QubesVM):
except qubesadmin.exc.QubesDaemonAccessError:
self._internal = False
self._servicevm = bool(self.vm.features.get("servicevm", False))
- self._is_dispvm_template = getattr(
- self.vm, "template_for_dispvms", False
- )
- self._has_network = (
- self.vm.is_networked() if vm.klass != "AdminVM" else False
- )
+ self._is_dispvm_template = getattr(self.vm, "template_for_dispvms", False)
+ self._has_network = self.vm.is_networked() if vm.klass != "AdminVM" else False
self._vm_icon_name = getattr(
self.vm, "icon", getattr(self.vm.label, "icon", None)
)
@@ -179,20 +175,14 @@ def show_in_apps(self):
def _escaped_name(self) -> str:
"""Name escaped according to rules from desktop-linux-common
package"""
- return (
- self.vm_name.replace("_", "_u")
- .replace("-", "_d")
- .replace(".", "_p")
- )
+ return self.vm_name.replace("_", "_u").replace("-", "_d").replace(".", "_p")
@property
def settings_desktop_file_name(self) -> str:
"""
Name of relevant .desktop vm settings file.
"""
- return (
- "org.qubes-os.qubes-vm-settings._" + self._escaped_name + ".desktop"
- )
+ return "org.qubes-os.qubes-vm-settings._" + self._escaped_name + ".desktop"
@property
def start_vm_desktop_file_name(self) -> str:
@@ -275,9 +265,7 @@ def _update_domain_state(self, vm_name, event, **_kwargs):
state = constants.STATE_DICTIONARY[event]
vm_entry.power_state = state
- def _update_domain_property(
- self, vm_name, event, newvalue, *_args, **_kwargs
- ):
+ def _update_domain_property(self, vm_name, event, newvalue, *_args, **_kwargs):
vm_entry = self.load_vm_from_name(vm_name)
if not vm_entry:
@@ -298,9 +286,7 @@ def _update_domain_property(
# it will disable any future event handling
pass
- def _update_domain_feature(
- self, vm, _event, feature=None, value=None, **_kwargs
- ):
+ def _update_domain_feature(self, vm, _event, feature=None, value=None, **_kwargs):
vm_entry = self.load_vm_from_name(vm)
if not vm_entry:
@@ -339,36 +325,20 @@ def _update_domain_feature(
def register_events(self):
"""Register handlers for all relevant VM events."""
- self.dispatcher.add_handler(
- "domain-pre-start", self._update_domain_state
- )
+ self.dispatcher.add_handler("domain-pre-start", self._update_domain_state)
self.dispatcher.add_handler("domain-start", self._update_domain_state)
- self.dispatcher.add_handler(
- "domain-start-failed", self._update_domain_state
- )
+ self.dispatcher.add_handler("domain-start-failed", self._update_domain_state)
self.dispatcher.add_handler("domain-paused", self._update_domain_state)
- self.dispatcher.add_handler(
- "domain-unpaused", self._update_domain_state
- )
- self.dispatcher.add_handler(
- "domain-shutdown", self._update_domain_state
- )
- self.dispatcher.add_handler(
- "domain-pre-shutdown", self._update_domain_state
- )
- self.dispatcher.add_handler(
- "domain-shutdown-failed", self._update_domain_state
- )
+ self.dispatcher.add_handler("domain-unpaused", self._update_domain_state)
+ self.dispatcher.add_handler("domain-shutdown", self._update_domain_state)
+ self.dispatcher.add_handler("domain-pre-shutdown", self._update_domain_state)
+ self.dispatcher.add_handler("domain-shutdown-failed", self._update_domain_state)
self.dispatcher.add_handler("domain-add", self._add_domain)
self.dispatcher.add_handler("domain-delete", self._remove_domain)
- self.dispatcher.add_handler(
- "property-set:netvm", self._update_domain_property
- )
- self.dispatcher.add_handler(
- "property-set:label", self._update_domain_property
- )
+ self.dispatcher.add_handler("property-set:netvm", self._update_domain_property)
+ self.dispatcher.add_handler("property-set:label", self._update_domain_property)
self.dispatcher.add_handler(
"property-set:template_for_dispvms", self._update_domain_property
)
diff --git a/qubes_menu_settings/menu_settings.glade b/qubes_menu_settings/menu_settings.glade
index 80268ae..3bd0fb8 100644
--- a/qubes_menu_settings/menu_settings.glade
+++ b/qubes_menu_settings/menu_settings.glade
@@ -9,7 +9,7 @@
appmenu-settings-program-icon
center
-
+
True
False
10
@@ -32,7 +32,7 @@
-
+
-
+
-
+
True
False
@@ -187,7 +187,21 @@
-
+
+ Show recent applications/search queries
+ True
+ True
+ False
+ True
+
+
+ False
+ True
+ 4
+
+
+
+
True
False
end
@@ -250,7 +264,7 @@
False
True
- 4
+ 5
diff --git a/qubes_menu_settings/menu_settings.py b/qubes_menu_settings/menu_settings.py
index cfd07bf..2094da3 100644
--- a/qubes_menu_settings/menu_settings.py
+++ b/qubes_menu_settings/menu_settings.py
@@ -29,30 +29,35 @@
from qubes_config.widgets.gtk_widgets import ImageListModeler
-gi.require_version('Gtk', '3.0')
+gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk
-from qubes_menu.constants import INITIAL_PAGE_FEATURE, SORT_RUNNING_FEATURE, \
- POSITION_FEATURE
+from qubes_menu.constants import (
+ INITIAL_PAGE_FEATURE,
+ SORT_RUNNING_FEATURE,
+ POSITION_FEATURE,
+ DISABLE_RECENT_FEATURE,
+)
MENU_PAGES = {
"search_page": "Search",
"app_page": "Applications",
- "favorites_page": "Favorites"
+ "favorites_page": "Favorites",
}
MENU_PAGES_DICT = {
"Search": {"icon": "qappmenu-search", "object": "search_page"},
"Applications": {"icon": "qappmenu-qube", "object": "app_page"},
- "Favorites": {"icon": "qappmenu-favorites", "object": "favorites_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"
+ "mouse": "Mouse",
}
MENU_POSITIONS_DICT = {
@@ -60,17 +65,20 @@
"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"}}
+ "Mouse": {"icon": "input-mouse-symbolic", "object": "mouse"},
+}
+
class AppMenuSettings(Gtk.Application):
"""
Qubes Menu Settings app.
"""
+
def __init__(self, qapp: qubesadmin.Qubes):
"""
:param qapp: qubesadmin.Qubes object
"""
- super().__init__(application_id='org.qubesos.appmenusettings')
+ super().__init__(application_id="org.qubesos.appmenusettings")
self.qapp = qapp
self.vm = self.qapp.domains[self.qapp.local_name]
@@ -92,49 +100,57 @@ def perform_setup(self):
"""
self.builder = Gtk.Builder()
- glade_path = (importlib.resources.files('qubes_menu_settings') /
- 'menu_settings.glade')
+ glade_path = (
+ importlib.resources.files("qubes_menu_settings") / "menu_settings.glade"
+ )
with importlib.resources.as_file(glade_path) as path:
self.builder.add_from_file(str(path))
- self.main_window : Gtk.ApplicationWindow = \
- self.builder.get_object('main_window')
+ self.main_window: Gtk.ApplicationWindow = self.builder.get_object("main_window")
- self.confirm_button: Gtk.Button = \
- self.builder.get_object('button_confirm')
- self.apply_button: Gtk.Button = self.builder.get_object('button_apply')
- self.cancel_button: Gtk.Button = \
- self.builder.get_object('button_cancel')
+ self.confirm_button: Gtk.Button = self.builder.get_object("button_confirm")
+ self.apply_button: Gtk.Button = self.builder.get_object("button_apply")
+ self.cancel_button: Gtk.Button = self.builder.get_object("button_cancel")
- self.starting_page_combo: Gtk.ComboBox = \
- self.builder.get_object("starting_page_combo")
+ 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.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")
+ self.sort_running_check: Gtk.CheckButton = self.builder.get_object(
+ "sort_running_to_top_check"
+ )
+ self.show_recent_check: Gtk.CheckButton = self.builder.get_object(
+ "show_recent_apps_check"
+ )
screen = Gdk.Screen.get_default()
provider = Gtk.CssProvider()
- css_path = (importlib.resources.files('qubes_menu_settings') /
- 'menu_settings.css')
+ css_path = (
+ importlib.resources.files("qubes_menu_settings") / "menu_settings.css"
+ )
with importlib.resources.as_file(css_path) as path:
provider.load_from_path(str(path))
Gtk.StyleContext.add_provider_for_screen(
- screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
+ screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
+ )
self.confirm_button.connect("clicked", self._save_exit)
self.apply_button.connect("clicked", self._save)
self.cancel_button.connect("clicked", self._quit)
self.initial_page_model = ImageListModeler(
- self.starting_page_combo, MENU_PAGES_DICT)
+ self.starting_page_combo, MENU_PAGES_DICT
+ )
self.menu_position_model = ImageListModeler(
- self.menu_position_combo, MENU_POSITIONS_DICT)
+ self.menu_position_combo, MENU_POSITIONS_DICT
+ )
self.load_state()
@@ -156,11 +172,12 @@ def load_state(self):
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))
+ sort_running = bool(self.vm.features.get(SORT_RUNNING_FEATURE, False))
self.sort_running_check.set_active(sort_running)
+ disable_recent = bool(self.vm.features.get(DISABLE_RECENT_FEATURE, False))
+ self.show_recent_check.set_active(not disable_recent)
+
def _quit(self, *_args):
self.quit()
@@ -175,19 +192,26 @@ def _save(self, *_args):
if old_sort_running:
del self.vm.features[SORT_RUNNING_FEATURE]
- old_initial_page = self.vm.features.get(INITIAL_PAGE_FEATURE,
- "app_page")
+ old_disable_recent = self.vm.features.get(DISABLE_RECENT_FEATURE, None)
+
+ if self.show_recent_check.get_active():
+ if old_disable_recent:
+ del self.vm.features[DISABLE_RECENT_FEATURE]
+ else:
+ if not old_sort_running:
+ self.vm.features[DISABLE_RECENT_FEATURE] = "1"
+
+ old_initial_page = self.vm.features.get(INITIAL_PAGE_FEATURE, "app_page")
if self.initial_page_model.get_selected() != old_initial_page:
- self.vm.features[INITIAL_PAGE_FEATURE] = \
+ self.vm.features[INITIAL_PAGE_FEATURE] = (
self.initial_page_model.get_selected()
+ )
- old_menu_position = self.vm.features.get(POSITION_FEATURE,
- "mouse")
+ 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()
+ self.vm.features[POSITION_FEATURE] = self.menu_position_model.get_selected()
def _save_exit(self, *_args):
self._save()
@@ -203,5 +227,5 @@ def main():
app.run()
-if __name__ == '__main__':
+if __name__ == "__main__":
sys.exit(main())
diff --git a/qubes_menu_settings/test_menu_settings.py b/qubes_menu_settings/test_menu_settings.py
index daf5bc0..1d381b3 100644
--- a/qubes_menu_settings/test_menu_settings.py
+++ b/qubes_menu_settings/test_menu_settings.py
@@ -24,10 +24,11 @@
def test_menu_settings_load():
qapp = MockQubesComplete()
- 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._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._qubes["dom0"].features["menu-disable-recent"] = "1"
qapp.update_vm_calls()
@@ -38,14 +39,16 @@ def test_menu_settings_load():
assert app.initial_page_model.get_selected() == "favorites_page"
assert app.menu_position_model.get_selected() == "mouse"
assert app.sort_running_check.get_active()
+ assert not app.show_recent_check.get_active()
def test_menu_settings_change():
qapp = MockQubesComplete()
- 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._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._qubes["dom0"].features["menu-disable-recent"] = "1"
qapp.update_vm_calls()
@@ -56,24 +59,36 @@ def test_menu_settings_change():
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()
+ assert not app.show_recent_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.show_recent_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"
+ qapp.expected_calls[
+ ("dom0", "admin.vm.feature.Remove", "menu-disable-recent", None)
+ ] = b"0\0"
app._save()
def test_menu_settings_change2():
qapp = MockQubesComplete()
- 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._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._qubes["dom0"].features["menu-disable-recent"] = ""
qapp.update_vm_calls()
@@ -83,9 +98,16 @@ def test_menu_settings_change2():
assert app.initial_page_model.get_selected() == "app_page"
assert not app.sort_running_check.get_active()
+ assert app.show_recent_check.get_active()
app.starting_page_combo.set_active_id("Favorites")
-
- qapp.expected_calls[('dom0', 'admin.vm.feature.Set', 'menu-initial-page', b'favorites_page')] = b'0\0'
+ app.show_recent_check.set_active(False)
+
+ qapp.expected_calls[
+ ("dom0", "admin.vm.feature.Set", "menu-initial-page", b"favorites_page")
+ ] = b"0\0"
+ qapp.expected_calls[
+ ("dom0", "admin.vm.feature.Set", "menu-disable-recent", b"1")
+ ] = b"0\0"
app._save()
diff --git a/rpm_spec/qubes-desktop-linux-menu.spec.in b/rpm_spec/qubes-desktop-linux-menu.spec.in
index 8e7d896..66e878a 100644
--- a/rpm_spec/qubes-desktop-linux-menu.spec.in
+++ b/rpm_spec/qubes-desktop-linux-menu.spec.in
@@ -143,6 +143,10 @@ gtk-update-icon-cache %{_datadir}/icons/hicolor &>/dev/null || :
/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/qappmenu-az.svg
+/usr/share/icons/hicolor/scalable/apps/qappmenu-za.svg
+/usr/share/icons/hicolor/scalable/apps/qappmenu-qube-az.svg
+/usr/share/icons/hicolor/scalable/apps/qappmenu-qube-za.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