diff --git a/qubes_config/widgets/gtk_utils.py b/qubes_config/widgets/gtk_utils.py index 9dad5d07..7406fb0b 100644 --- a/qubes_config/widgets/gtk_utils.py +++ b/qubes_config/widgets/gtk_utils.py @@ -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): diff --git a/qui/decorators.py b/qui/decorators.py index 27222ed9..69ec5463 100644 --- a/qui/decorators.py +++ b/qui/decorators.py @@ -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 @@ -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 diff --git a/qui/devices/actionable_widgets.py b/qui/devices/actionable_widgets.py index 0bcc1ac1..0d457a71 100644 --- a/qui/devices/actionable_widgets.py +++ b/qui/devices/actionable_widgets.py @@ -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): @@ -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) @@ -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 diff --git a/qui/devices/backend.py b/qui/devices/backend.py index 9c78c784..3f006733 100644 --- a/qui/devices/backend.py +++ b/qui/devices/backend.py @@ -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: @@ -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 @@ -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 @@ -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() @@ -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 @@ -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( diff --git a/qui/devices/device_widget.py b/qui/devices/device_widget.py index e5634296..ac704040 100644 --- a/qui/devices/device_widget.py +++ b/qui/devices/device_widget.py @@ -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): @@ -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() @@ -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]: @@ -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] @@ -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"), @@ -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): @@ -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: @@ -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: @@ -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: @@ -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 = [] @@ -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: @@ -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 diff --git a/qui/tray/disk_space.py b/qui/tray/disk_space.py index 4e8464aa..c5e73fcc 100644 --- a/qui/tray/disk_space.py +++ b/qui/tray/disk_space.py @@ -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 diff --git a/qui/tray/domains.py b/qui/tray/domains.py index fd5c6817..61dc0a68 100644 --- a/qui/tray/domains.py +++ b/qui/tray/domains.py @@ -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):