Skip to content

Commit

Permalink
Rework domains widget
Browse files Browse the repository at this point in the history
  • Loading branch information
fepitre committed Nov 30, 2024
1 parent 3a74244 commit 15c3e12
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 113 deletions.
4 changes: 1 addition & 3 deletions qubes_config/tests/test_policy_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,7 @@ def return_files(service_name):


@patch("qubes_config.global_config.policy_manager.PolicyClient.policy_get")
@patch(
"qubes_config.global_config.policy_manager.PolicyClient.policy_replace"
)
@patch("qubes_config.global_config.policy_manager.PolicyClient.policy_replace")
def test_get_policy_from_file_new_no_default(mock_replace, mock_get):
manager = PolicyManager()

Expand Down
2 changes: 1 addition & 1 deletion qui/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ def device_domain_hbox(vm, attached: bool) -> Gtk.Box:


def create_icon(name) -> Gtk.Image:
""" " Create an icon from string; tries for both the normal and -symbolic
"""Create an icon from string; tries for both the normal and -symbolic
variants, because some themes only have the symbolic variant. If not
found, outputs a blank icon."""

Expand Down
227 changes: 118 additions & 109 deletions qui/tray/domains.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,22 @@
# -*- coding: utf-8 -*-
# pylint: disable=wrong-import-position,import-error,superfluous-parens
""" A menu listing domains """
import abc
import asyncio
import os
import subprocess
import sys
import os
import threading
import traceback

import gi # isort:skip
import qubesadmin
import qubesadmin.events
import qui.utils
import qui.decorators

from qubesadmin import exc

import gi # isort:skip
import qui.decorators
import qui.utils

gi.require_version("Gtk", "3.0") # isort:skip
from gi.repository import Gio, Gtk, GObject, GLib, GdkPixbuf # isort:skip
from gi.repository import Gio, Gtk, GLib, GdkPixbuf # isort:skip

import gbulb

Expand Down Expand Up @@ -85,38 +82,61 @@ def show_error(title, text):
GLib.idle_add(dialog.show)


class VMActionMenuItem(Gtk.ImageMenuItem):
def __init__(self, vm, icon_cache, icon_name, label):
class ActionMenuItem(Gtk.MenuItem):
def __init__(self, label, img=None, icon_cache=None, icon_name=None):
super().__init__()
self.vm = vm

img = Gtk.Image.new_from_pixbuf(icon_cache.get_icon(icon_name))
# Create a container for the custom layout
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)

# Add an icon to the menu item
if icon_cache and icon_name:
img = Gtk.Image.new_from_pixbuf(icon_cache.get_icon(icon_name))
if img:
img.show() # Ensure the image is visible
box.pack_start(img, False, False, 0)

# Add a label to the menu item
label_widget = Gtk.Label(label=label, xalign=0)
box.pack_start(label_widget, True, True, 0)

self.set_image(img)
self.set_label(label)
# Add the box to the menu item
self.add(box)

self.connect("activate", self.instantiate_thread_with_function)
# Connect the "activate" signal to the async function
self.connect("activate", self.on_activate)

@abc.abstractmethod
def perform_action(self):
async def perform_action(self):
"""
Action this item should perform.
Action this item should perform (to be implemented by subclasses).
"""
raise NotImplementedError("Subclasses must implement this method.")

def instantiate_thread_with_function(self, *_args, **_kwargs):
"""Make a thread to run potentially slow processes like vm.kill in the
background"""
thread = threading.Thread(target=self.perform_action)
thread.start()
def on_activate(self, *_args, **_kwargs):
asyncio.create_task(self.perform_action())


class VMActionMenuItem(ActionMenuItem):
# pylint: disable=abstract-method
def __init__(self, vm, label, img=None, icon_cache=None, icon_name=None):
super().__init__(
label=label, img=img, icon_cache=icon_cache, icon_name=icon_name
)
self.vm = vm


class PauseItem(VMActionMenuItem):
"""Shutdown menu Item. When activated pauses the domain."""

def __init__(self, vm, icon_cache):
super().__init__(vm, icon_cache, "pause", _("Emergency pause"))
super().__init__(
vm,
label=_("Emergency pause"),
icon_cache=icon_cache,
icon_name="pause",
)

def perform_action(self):
async def perform_action(self):
try:
self.vm.pause()
except exc.QubesException as ex:
Expand All @@ -133,9 +153,11 @@ class UnpauseItem(VMActionMenuItem):
"""Unpause menu Item. When activated unpauses the domain."""

def __init__(self, vm, icon_cache):
super().__init__(vm, icon_cache, "unpause", _("Unpause"))
super().__init__(
vm, label=_("Unpause"), icon_cache=icon_cache, icon_name="unpause"
)

def perform_action(self):
async def perform_action(self):
try:
self.vm.unpause()
except exc.QubesException as ex:
Expand All @@ -152,9 +174,11 @@ class ShutdownItem(VMActionMenuItem):
"""Shutdown menu Item. When activated shutdowns the domain."""

def __init__(self, vm, icon_cache):
super().__init__(vm, icon_cache, "shutdown", _("Shutdown"))
super().__init__(
vm, label=_("Shutdown"), icon_cache=icon_cache, icon_name="shutdown"
)

def perform_action(self):
async def perform_action(self):
try:
self.vm.shutdown()
except exc.QubesException as ex:
Expand All @@ -167,26 +191,17 @@ def perform_action(self):
)


class RestartItem(Gtk.ImageMenuItem):
class RestartItem(VMActionMenuItem):
"""Restart menu Item. When activated shutdowns the domain and
then starts it again."""

def __init__(self, vm, icon_cache):
super().__init__()
self.vm = vm

img = Gtk.Image.new_from_pixbuf(icon_cache.get_icon("restart"))

self.set_image(img)
self.set_label(_("Restart"))
super().__init__(
vm, label=_("Restart"), icon_cache=icon_cache, icon_name="restart"
)
self.restart_thread = None

self.connect("activate", self.restart)

def restart(self, *_args, **_kwargs):
asyncio.ensure_future(self.perform_restart())

async def perform_restart(self):
async def perform_action(self, *_args, **_kwargs):
try:
self.vm.shutdown()
while self.vm.is_running():
Expand All @@ -211,9 +226,11 @@ class KillItem(VMActionMenuItem):
"""Kill domain menu Item. When activated kills the domain."""

def __init__(self, vm, icon_cache):
super().__init__(vm, icon_cache, "kill", _("Kill"))
super().__init__(
vm, label=_("Kill"), icon_cache=icon_cache, icon_name="kill"
)

def perform_action(self, *_args, **_kwargs):
async def perform_action(self, *_args, **_kwargs):
try:
self.vm.kill()
except exc.QubesException as ex:
Expand All @@ -230,47 +247,46 @@ class PreferencesItem(VMActionMenuItem):
"""Preferences menu Item. When activated shows preferences dialog"""

def __init__(self, vm, icon_cache):
super().__init__(vm, icon_cache, "preferences", _("Settings"))
super().__init__(
vm,
label=_("Settings"),
icon_cache=icon_cache,
icon_name="preferences",
)

def perform_action(self):
async def perform_action(self):
# pylint: disable=consider-using-with
subprocess.Popen(["qubes-vm-settings", self.vm.name])
await asyncio.create_subprocess_exec(
"qubes-vm-settings", self.vm.name, stderr=subprocess.PIPE
)


class LogItem(Gtk.ImageMenuItem):
class LogItem(ActionMenuItem):
def __init__(self, name, path):
super().__init__()
self.path = path

img = Gtk.Image.new_from_file(
"/usr/share/icons/HighContrast/16x16/apps/logviewer.png"
)
super().__init__(label=name, img=img)
self.path = path

self.set_image(img)
self.set_label(name)

self.connect("activate", self.launch_log_viewer)

def launch_log_viewer(self, *_args, **_kwargs):
# pylint: disable=consider-using-with
subprocess.Popen(["qubes-log-viewer", self.path])
async def perform_action(self):
await asyncio.create_subprocess_exec(
"qubes-log-viewer", self.path, stderr=subprocess.PIPE
)


class RunTerminalItem(Gtk.ImageMenuItem):
class RunTerminalItem(VMActionMenuItem):
"""Run Terminal menu Item. When activated runs a terminal emulator."""

def __init__(self, vm, icon_cache):
super().__init__()
self.vm = vm

img = Gtk.Image.new_from_pixbuf(icon_cache.get_icon("terminal"))

self.set_image(img)
self.set_label(_("Run Terminal"))

self.connect("activate", self.run_terminal)
super().__init__(
vm,
label=_("Run Terminal"),
icon_cache=icon_cache,
icon_name="terminal",
)

def run_terminal(self, _item):
async def perform_action(self):
try:
self.vm.run_service("qubes.StartApp+qubes-run-terminal")
except exc.QubesException as ex:
Expand All @@ -283,22 +299,19 @@ def run_terminal(self, _item):
)


class OpenFileManagerItem(Gtk.ImageMenuItem):
class OpenFileManagerItem(VMActionMenuItem):
"""Attempts to open a file manager in the VM. If fails, displays an
error message."""

def __init__(self, vm, icon_cache):
super().__init__()
self.vm = vm

img = Gtk.Image.new_from_pixbuf(icon_cache.get_icon("files"))

self.set_image(img)
self.set_label(_("Open File Manager"))

self.connect("activate", self.open_file_manager)
super().__init__(
vm,
label=_("Open File Manager"),
icon_cache=icon_cache,
icon_name="files",
)

def open_file_manager(self, _item):
async def perform_action(self):
try:
self.vm.run_service("qubes.StartApp+qubes-open-file-manager")
except exc.QubesException as ex:
Expand Down Expand Up @@ -428,27 +441,20 @@ def __init__(self, vm, icon_cache, working_correctly=True):
self.show_all()


def run_manager(_item):
# pylint: disable=consider-using-with
subprocess.Popen(["qubes-qube-manager"])


class QubesManagerItem(Gtk.ImageMenuItem):
class QubesManagerItem(ActionMenuItem):
def __init__(self):
super().__init__()
img = Gtk.Image.new_from_icon_name("qubes-logo-icon", Gtk.IconSize.MENU)
super().__init__(label=_("Open Qube Manager"), img=img)
self.show_all()

self.set_image(
Gtk.Image.new_from_icon_name("qubes-logo-icon", Gtk.IconSize.MENU)
async def perform_action(self):
# pylint: disable=consider-using-with
await asyncio.create_subprocess_exec(
"qubes-qube-manager", stderr=subprocess.PIPE
)

self.set_label(_("Open Qube Manager"))

self.connect("activate", run_manager)

self.show_all()


class DomainMenuItem(Gtk.ImageMenuItem):
class DomainMenuItem(Gtk.MenuItem):
def __init__(self, vm, app, icon_cache, state=None):
super().__init__()
self.vm = vm
Expand All @@ -463,9 +469,15 @@ def __init__(self, vm, app, icon_cache, state=None):
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
# hbox.set_homogeneous(True)

iconbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
if self.decorator.icon():
iconbox.pack_start(self.decorator.icon(), False, True, 0)

hbox.pack_start(iconbox, False, True, 10)

namebox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
self.name = self.decorator.name()
namebox.pack_start(self.name, True, True, 0)
namebox.pack_start(self.name, False, True, 0)
self.spinner = Gtk.Spinner()
namebox.pack_start(self.spinner, False, True, 0)

Expand All @@ -487,17 +499,14 @@ def __init__(self, vm, app, icon_cache, state=None):
self.cpu.update_state(header=True)
self.memory.update_state(header=True)
self.show_all() # header should always be visible
elif self.vm.klass == "AdminVM": # no submenu for AdminVM
self.set_reserve_indicator(True) # align with submenu triangles
else:
if not state:
self.update_state(self.vm.get_power_state())
if self.vm.klass == "AdminVM": # no submenu for AdminVM
self.set_reserve_indicator(True) # align with submenu triangles
else:
self.update_state(state)
self.set_label_icon()

def set_label_icon(self):
self.set_image(self.decorator.icon())
if not state:
self.update_state(self.vm.get_power_state())
else:
self.update_state(state)

def _set_submenu(self, state):
if self.vm.features.get("internal", False):
Expand Down Expand Up @@ -595,7 +604,7 @@ def __init__(self, app_name, qapp, dispatcher, stats_dispatcher):
self.pause_notification_out = False

# add refreshing tooltips with storage info
GObject.timeout_add_seconds(120, self.refresh_tooltips)
GLib.timeout_add_seconds(120, self.refresh_tooltips)

self.register_events()
self.set_application_id(app_name)
Expand Down

0 comments on commit 15c3e12

Please sign in to comment.