Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion qubes_config/widgets/gtk_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def load_icon(icon_name: str, width: int = 24, height: int = 24):
try:
# icon_name is a name
image: GdkPixbuf.Pixbuf = Gtk.IconTheme.get_default().load_icon(
icon_name, width, 0
icon_name, width, Gtk.IconLookupFlags.FORCE_SIZE
)
return image
except (TypeError, GLib.Error):
Expand Down
8 changes: 6 additions & 2 deletions qui/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,9 @@ def icon(self) -> Gtk.Image:
except exc.QubesDaemonCommunicationError:
# no permission to access icon
icon = "appvm-black"
icon_vm = Gtk.IconTheme.get_default().load_icon(icon, 16, 0)
icon_vm = Gtk.IconTheme.get_default().load_icon(
icon, 16, Gtk.IconLookupFlags.FORCE_SIZE
)
icon_img = Gtk.Image.new_from_pixbuf(icon_vm)
return icon_img

Expand Down Expand Up @@ -301,7 +303,9 @@ def create_icon(name) -> Gtk.Image:
pixbuf = None
for icon_name in names:
try:
pixbuf = Gtk.IconTheme.get_default().load_icon(icon_name, 16, 0)
pixbuf = Gtk.IconTheme.get_default().load_icon(
icon_name, 16, Gtk.IconLookupFlags.FORCE_SIZE
)
break
except (TypeError, GLib.Error):
continue
Expand Down
20 changes: 14 additions & 6 deletions qui/devices/actionable_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ def load_icon(icon_name: str, backup_name: str, size: int = 24):
"""
try:
image: GdkPixbuf.Pixbuf = Gtk.IconTheme.get_default().load_icon(
icon_name, size, 0
icon_name, size, Gtk.IconLookupFlags.FORCE_SIZE
)
return image
except (TypeError, GLib.Error):
try:
image: GdkPixbuf.Pixbuf = Gtk.IconTheme.get_default().load_icon(
backup_name, size, 0
backup_name, size, Gtk.IconLookupFlags.FORCE_SIZE
)
return image
except (TypeError, GLib.Error):
Expand Down Expand Up @@ -500,9 +500,15 @@ def __init__(self, device: backend.Device, variant: str = "dark"):
if device.devices_to_attach_with_me:
mic_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
mic_label = Gtk.Label()
mic_label.set_markup("This device will attach with microphone")
mic_box.add(mic_label)
mic_img = VariantIcon("mic", variant, 18)
if device.device_class == "mic":
mic_label.set_markup("This device will attach with camera ")
mic_box.add(mic_label)
else:
mic_label.set_markup("This device will attach with microphone ")
mic_box.add(mic_label)
mic_img = VariantIcon(
"camera" if device.device_class == "mic" else "mic", variant, 18
)
mic_box.add(mic_img)
mic_box.set_halign(Gtk.Align.CENTER)
self.add(mic_box)
Expand Down Expand Up @@ -572,7 +578,9 @@ def get_child_widgets(self, vms, disp_vm_templates) -> Iterable[ActionableWidget
other_vms = [
vm
for vm in vms
if vm not in self.device.attachments and vm not in self.device.assignments
if vm not in self.device.attachments
and vm not in self.device.assignments
and vm.name != self.device.backend_domain.name
]

# all devices have a header
Expand Down
20 changes: 18 additions & 2 deletions qui/devices/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@

FEATURE_HIDE_CHILDREN = "device-hide-children"
FEATURE_ATTACH_WITH_MIC = "device-attach-with-mic"
FEATURE_RESOLUTION = "device-qvc-resolution" # dev_id=resolution, space delimited


class VM:
Expand Down Expand Up @@ -134,7 +135,7 @@ def toggle_feature_value(self, feature_name, value):
class Device:
@classmethod
def id_from_device(cls, dev: qubesadmin.devices.DeviceInfo) -> str:
return str(dev.port) + ":" + str(dev.device_id)
return str(dev.devclass) + ":" + str(dev.port) + ":" + str(dev.device_id)

def __init__(self, dev: qubesadmin.devices.DeviceInfo, gtk_app: Gtk.Application):
self.gtk_app: Gtk.Application = gtk_app
Expand All @@ -152,7 +153,6 @@ def __init__(self, dev: qubesadmin.devices.DeviceInfo, gtk_app: Gtk.Application)
self._description: str = getattr(dev, "description", "unknown")
self._devclass: str = getattr(dev, "devclass", "unknown")

main_category = None
for interface in dev.interfaces:
if interface.category.name != "Other":
main_category = interface.category
Expand All @@ -164,6 +164,7 @@ def __init__(self, dev: qubesadmin.devices.DeviceInfo, gtk_app: Gtk.Application)

self._data: Dict = getattr(dev, "data", {})
self._device_id = getattr(dev, "device_id", "*")
self.options = {}
self.parent = str(getattr(dev, "parent_device", None) or "")
self.attachments: Set[VM] = set()
self.assignments: Set[VM] = set()
Expand All @@ -186,6 +187,20 @@ def __init__(self, dev: qubesadmin.devices.DeviceInfo, gtk_app: Gtk.Application)
self.show_children: bool = True
self.hide_this_device: bool = False

if self.device_class == "webcam" and self._backend_domain:
resolution_feature = self._backend_domain.vm_object.features.get(
FEATURE_RESOLUTION, ""
)
if resolution_feature:
for w in resolution_feature.split(" "):
try:
k, v = w.split("=")
if k == self.id_string:
self.options["format"] = v
except ValueError:
# malformed options, ignore them
pass

def __str__(self):
return self._dev_name

Expand Down Expand Up @@ -286,6 +301,7 @@ def attach_to_vm(self, vm: VM, with_aux_devices: bool = True):
port_id=self._ident,
devclass=self.device_class,
device_id=self._device_id,
options=self.options,
)
vm.vm_object.devices[self.device_class].attach(assignment)
self.gtk_app.emit_notification(
Expand Down
78 changes: 68 additions & 10 deletions qui/devices/device_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@


# FUTURE: this should be moved to backend with new API changes
DEV_TYPES = ["block", "usb", "mic"]
DEV_TYPES = ["block", "usb", "mic", "webcam"]


class DeviceMenu(Gtk.Menu):
Expand Down Expand Up @@ -112,6 +112,7 @@ def __init__(self, app_name, qapp, dispatcher):
self.vms: Set[backend.VM] = set()
self.dispvm_templates: Set[backend.VM] = set()
self.parent_ports_to_hide = []
self.cameras_to_hide = []
self.active_usbvms: Set[backend.VM] = set()
self.dormant_usbvms: Set[backend.VM] = set()
self.dev_update_queue: Set = set()
Expand Down Expand Up @@ -172,6 +173,13 @@ def __init__(self, app_name, qapp, dispatcher):
self.dispatcher.add_handler(
"domain-feature-delete:internal", self.update_internal_feature
)
self.dispatcher.add_handler(
"domain-feature-set:" + backend.FEATURE_RESOLUTION, self.update_resolution
)
self.dispatcher.add_handler(
"domain-feature-delete:" + backend.FEATURE_RESOLUTION,
self.update_resolution,
)

for feature in [backend.FEATURE_HIDE_CHILDREN, backend.FEATURE_ATTACH_WITH_MIC]:

Expand Down Expand Up @@ -266,13 +274,17 @@ def device_added(self, vm, _event, device):
if dev.parent:
for potential_parent in self.devices.values():
if potential_parent.port == dev.parent:
# hide parents of webcams
if dev.device_class == "webcam":
self.cameras_to_hide.append(dev.parent)
potential_parent.hide_this_device = True
potential_parent.has_children = True
break

# connect with mic
mic_feature = vm.features.get(backend.FEATURE_ATTACH_WITH_MIC, "").split(" ")
if dev_id in mic_feature:
microphone = self.devices.get("dom0:mic:dom0:mic::m000000", None)
microphone = self.devices.get("mic:dom0:mic:dom0:mic::m000000", None)
microphone.devices_to_attach_with_me.append(dev)
dev.devices_to_attach_with_me = [microphone]

Expand All @@ -298,7 +310,7 @@ def device_removed(self, vm, _event, port):
# we never knew the device anyway
return

microphone = self.devices.get("dom0:mic:dom0:mic::m000000", None)
microphone = self.devices.get("mic:dom0:mic:dom0:mic::m000000", None)

self.emit_notification(
_("Device removed"),
Expand All @@ -310,6 +322,12 @@ def device_removed(self, vm, _event, port):
microphone.devices_to_attach_with_me.remove(dev)
if dev.port in self.parent_ports_to_hide:
self.parent_ports_to_hide.remove(dev.port)
if dev.parent in self.cameras_to_hide:
for potential_parent in self.devices.values():
if potential_parent.port == dev.parent:
potential_parent.hide_this_device = False
break
self.cameras_to_hide.remove(dev.parent)
del self.devices[dev_id]

def initialize_dev_data(self):
Expand Down Expand Up @@ -351,6 +369,12 @@ def initialize_dev_data(self):
# we have no permission to access VM's devices
continue

# hide parents of webcams
for device in self.devices.values():
if device.device_class == "webcam":
if device.parent:
self.cameras_to_hide.append(device.parent)

def device_assigned(self, vm, _event, device, **_kwargs):
dev_id = backend.Device.id_from_device(device)
if dev_id not in self.devices:
Expand Down Expand Up @@ -399,7 +423,7 @@ def update_single_feature(self, _vm, _event, feature, value=None, oldvalue=None)
add = new - old
remove = old - new

microphone = self.devices.get("dom0:mic:dom0:mic::m000000", None)
microphone = self.devices.get("mic:dom0:mic:dom0:mic::m000000", None)

for dev_name in remove:
if feature == backend.FEATURE_ATTACH_WITH_MIC:
Expand Down Expand Up @@ -428,6 +452,29 @@ def update_single_feature(self, _vm, _event, feature, value=None, oldvalue=None)
self.parent_ports_to_hide.append(dev.port)
self.hide_child_devices(dev.port, False)

def update_resolution(self, vm, _event, feature, value=None, oldvalue=None):
# pylint: disable=unused-argument
res_dict = {}
if value:
for word in value.split(" "):
try:
k, v = word.split("=")
res_dict[k] = v
except ValueError:
# the feature is malformed, ignore it
res_dict = {}

for device in self.devices.values():
if (
device.device_class == "webcam"
and device.backend_domain.name == vm.name
):
resolution = res_dict.get(device.id_string, None)
if resolution:
device.options["format"] = resolution
elif "format" in device.options:
del device.options["format"]

def vm_unpaused(self, vm, _event, **_kwargs):
wrapped_vm = backend.VM(vm)
try:
Expand Down Expand Up @@ -465,7 +512,7 @@ def initialize_features(self, *_args, **_kwargs):
"""
domains = self.qapp.domains

microphone = self.devices.get("dom0:mic:dom0:mic::m000000", None)
microphone = self.devices.get("mic:dom0:mic:dom0:mic::m000000", None)
# clear existing feature mappings
for dev in self.devices.values():
dev.devices_to_attach_with_me = []
Expand All @@ -479,13 +526,21 @@ def initialize_features(self, *_args, **_kwargs):
mic_feature = domain.features.get(
backend.FEATURE_ATTACH_WITH_MIC, False
)
if not mic_feature:
continue
except qubesadmin.exc.QubesDaemonAccessError:
continue
if isinstance(mic_feature, str):
mic_dev_strings.extend(
[dev for dev in mic_feature.split(" ") if dev]
)

for dev in mic_feature.split(" "):
if not dev:
continue
try:
_class, vm_name, _rest = dev.split(":", 2)
if vm_name != domain.name:
continue
mic_dev_strings.append(dev)
except ValueError:
# malformed name
pass
microphone.devices_to_attach_with_me = []

for dev in mic_dev_strings:
Expand All @@ -511,6 +566,9 @@ def initialize_features(self, *_args, **_kwargs):
dev.show_children = False

self.hide_child_devices()
for dev in self.devices.values():
if dev.port in self.cameras_to_hide:
dev.hide_this_device = True

def hide_child_devices(
self, parent_port: Optional[str] = None, state: bool = False
Expand Down
4 changes: 3 additions & 1 deletion qui/tray/disk_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ def __create_widgets(vm_usage):
icon = getattr(vm, "icon", vm.label.icon)
except exc.QubesPropertyAccessError:
icon = "appvm-black"
icon_vm = Gtk.IconTheme.get_default().load_icon(icon, 16, 0)
icon_vm = Gtk.IconTheme.get_default().load_icon(
icon, 16, Gtk.IconLookupFlags.FORCE_SIZE
)
icon_img = Gtk.Image.new_from_pixbuf(icon_vm)

# description widget
Expand Down
2 changes: 1 addition & 1 deletion qui/tray/domains.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def get_icon(self, icon_name):
else:
try:
icon = Gtk.IconTheme.get_default().load_icon(
self.icon_files[icon_name], 16, 0
self.icon_files[icon_name], 16, Gtk.IconLookupFlags.FORCE_SIZE
)
self.icons[icon_name] = icon
except (TypeError, GLib.Error):
Expand Down