Skip to content

Commit d6d6325

Browse files
committed
Merge remote-tracking branch 'origin/pr/288'
* origin/pr/288: Don't propose attaching device to its backend Add resolution settings to QCV attachment Hide parent device of QVC webcam Add Qubes Video Companion devices to widget Add device class to device_id and validate features Fix microphone saying it will attach with microphone Enforce icon size Pull request description: - add device_class to device id string - add handling for Qubes Video Companion: hide parent USB camera, add feature to handle resolution setting - fix microphone saying it will attach with microphone - enforce icon size in all widgets - remove footgun in the shape of "let's attach a device to its backend"
2 parents 729ddd1 + 2c8f095 commit d6d6325

7 files changed

Lines changed: 111 additions & 23 deletions

File tree

qubes_config/widgets/gtk_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def load_icon(icon_name: str, width: int = 24, height: int = 24):
7676
try:
7777
# icon_name is a name
7878
image: GdkPixbuf.Pixbuf = Gtk.IconTheme.get_default().load_icon(
79-
icon_name, width, 0
79+
icon_name, width, Gtk.IconLookupFlags.FORCE_SIZE
8080
)
8181
return image
8282
except (TypeError, GLib.Error):

qui/decorators.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,9 @@ def icon(self) -> Gtk.Image:
222222
except exc.QubesDaemonCommunicationError:
223223
# no permission to access icon
224224
icon = "appvm-black"
225-
icon_vm = Gtk.IconTheme.get_default().load_icon(icon, 16, 0)
225+
icon_vm = Gtk.IconTheme.get_default().load_icon(
226+
icon, 16, Gtk.IconLookupFlags.FORCE_SIZE
227+
)
226228
icon_img = Gtk.Image.new_from_pixbuf(icon_vm)
227229
return icon_img
228230

@@ -301,7 +303,9 @@ def create_icon(name) -> Gtk.Image:
301303
pixbuf = None
302304
for icon_name in names:
303305
try:
304-
pixbuf = Gtk.IconTheme.get_default().load_icon(icon_name, 16, 0)
306+
pixbuf = Gtk.IconTheme.get_default().load_icon(
307+
icon_name, 16, Gtk.IconLookupFlags.FORCE_SIZE
308+
)
305309
break
306310
except (TypeError, GLib.Error):
307311
continue

qui/devices/actionable_widgets.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,13 @@ def load_icon(icon_name: str, backup_name: str, size: int = 24):
5353
"""
5454
try:
5555
image: GdkPixbuf.Pixbuf = Gtk.IconTheme.get_default().load_icon(
56-
icon_name, size, 0
56+
icon_name, size, Gtk.IconLookupFlags.FORCE_SIZE
5757
)
5858
return image
5959
except (TypeError, GLib.Error):
6060
try:
6161
image: GdkPixbuf.Pixbuf = Gtk.IconTheme.get_default().load_icon(
62-
backup_name, size, 0
62+
backup_name, size, Gtk.IconLookupFlags.FORCE_SIZE
6363
)
6464
return image
6565
except (TypeError, GLib.Error):
@@ -505,9 +505,15 @@ def __init__(self, device: backend.Device, variant: str = "dark"):
505505
if device.devices_to_attach_with_me:
506506
mic_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
507507
mic_label = Gtk.Label()
508-
mic_label.set_markup("This device will attach with microphone")
509-
mic_box.add(mic_label)
510-
mic_img = VariantIcon("mic", variant, 18)
508+
if device.device_class == "mic":
509+
mic_label.set_markup("This device will attach with camera ")
510+
mic_box.add(mic_label)
511+
else:
512+
mic_label.set_markup("This device will attach with microphone ")
513+
mic_box.add(mic_label)
514+
mic_img = VariantIcon(
515+
"camera" if device.device_class == "mic" else "mic", variant, 18
516+
)
511517
mic_box.add(mic_img)
512518
mic_box.set_halign(Gtk.Align.CENTER)
513519
self.add(mic_box)
@@ -577,7 +583,9 @@ def get_child_widgets(self, vms, disp_vm_templates) -> Iterable[ActionableWidget
577583
other_vms = [
578584
vm
579585
for vm in vms
580-
if vm not in self.device.attachments and vm not in self.device.assignments
586+
if vm not in self.device.attachments
587+
and vm not in self.device.assignments
588+
and vm.name != self.device.backend_domain.name
581589
]
582590

583591
# all devices have a header

qui/devices/backend.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838

3939
FEATURE_HIDE_CHILDREN = "device-hide-children"
4040
FEATURE_ATTACH_WITH_MIC = "device-attach-with-mic"
41+
FEATURE_RESOLUTION = "device-qvc-resolution" # dev_id=resolution, space delimited
4142

4243

4344
class VM:
@@ -149,7 +150,7 @@ def update_denied_devices(self):
149150
class Device:
150151
@classmethod
151152
def id_from_device(cls, dev: qubesadmin.devices.DeviceInfo) -> str:
152-
return str(dev.port) + ":" + str(dev.device_id)
153+
return str(dev.devclass) + ":" + str(dev.port) + ":" + str(dev.device_id)
153154

154155
def __init__(self, dev: qubesadmin.devices.DeviceInfo, gtk_app: Gtk.Application):
155156
self.gtk_app: Gtk.Application = gtk_app
@@ -168,7 +169,6 @@ def __init__(self, dev: qubesadmin.devices.DeviceInfo, gtk_app: Gtk.Application)
168169
self._description: str = getattr(dev, "description", "unknown")
169170
self._devclass: str = getattr(dev, "devclass", "unknown")
170171

171-
main_category = None
172172
for interface in dev.interfaces:
173173
if interface.category.name != "Other":
174174
main_category = interface.category
@@ -180,6 +180,7 @@ def __init__(self, dev: qubesadmin.devices.DeviceInfo, gtk_app: Gtk.Application)
180180

181181
self._data: Dict = getattr(dev, "data", {})
182182
self._device_id = getattr(dev, "device_id", "*")
183+
self.options = {}
183184
self.parent = str(getattr(dev, "parent_device", None) or "")
184185
self.attachments: Set[VM] = set()
185186
self.assignments: Set[VM] = set()
@@ -202,6 +203,20 @@ def __init__(self, dev: qubesadmin.devices.DeviceInfo, gtk_app: Gtk.Application)
202203
self.show_children: bool = True
203204
self.hide_this_device: bool = False
204205

206+
if self.device_class == "webcam" and self._backend_domain:
207+
resolution_feature = self._backend_domain.vm_object.features.get(
208+
FEATURE_RESOLUTION, ""
209+
)
210+
if resolution_feature:
211+
for w in resolution_feature.split(" "):
212+
try:
213+
k, v = w.split("=")
214+
if k == self.id_string:
215+
self.options["format"] = v
216+
except ValueError:
217+
# malformed options, ignore them
218+
pass
219+
205220
def __str__(self):
206221
return self._dev_name
207222

@@ -302,6 +317,7 @@ def attach_to_vm(self, vm: VM, with_aux_devices: bool = True):
302317
port_id=self._ident,
303318
devclass=self.device_class,
304319
device_id=self._device_id,
320+
options=self.options,
305321
)
306322
vm.vm_object.devices[self.device_class].attach(assignment)
307323
self.gtk_app.emit_notification(

qui/devices/device_widget.py

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565

6666

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

7070

7171
class DeviceMenu(Gtk.Menu):
@@ -112,6 +112,7 @@ def __init__(self, app_name, qapp, dispatcher):
112112
self.vms: Set[backend.VM] = set()
113113
self.dispvm_templates: Set[backend.VM] = set()
114114
self.parent_ports_to_hide = []
115+
self.cameras_to_hide = []
115116
self.active_usbvms: Set[backend.VM] = set()
116117
self.dormant_usbvms: Set[backend.VM] = set()
117118
self.dev_update_queue: Set = set()
@@ -180,6 +181,13 @@ def __init__(self, app_name, qapp, dispatcher):
180181
self.dispatcher.add_handler(
181182
"domain-feature-delete:internal", self.update_internal_feature
182183
)
184+
self.dispatcher.add_handler(
185+
"domain-feature-set:" + backend.FEATURE_RESOLUTION, self.update_resolution
186+
)
187+
self.dispatcher.add_handler(
188+
"domain-feature-delete:" + backend.FEATURE_RESOLUTION,
189+
self.update_resolution,
190+
)
183191

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

@@ -274,13 +282,17 @@ def device_added(self, vm, _event, device):
274282
if dev.parent:
275283
for potential_parent in self.devices.values():
276284
if potential_parent.port == dev.parent:
285+
# hide parents of webcams
286+
if dev.device_class == "webcam":
287+
self.cameras_to_hide.append(dev.parent)
288+
potential_parent.hide_this_device = True
277289
potential_parent.has_children = True
278290
break
279291

280292
# connect with mic
281293
mic_feature = vm.features.get(backend.FEATURE_ATTACH_WITH_MIC, "").split(" ")
282294
if dev_id in mic_feature:
283-
microphone = self.devices.get("dom0:mic:dom0:mic::m000000", None)
295+
microphone = self.devices.get("mic:dom0:mic:dom0:mic::m000000", None)
284296
microphone.devices_to_attach_with_me.append(dev)
285297
dev.devices_to_attach_with_me = [microphone]
286298

@@ -306,7 +318,7 @@ def device_removed(self, vm, _event, port):
306318
# we never knew the device anyway
307319
return
308320

309-
microphone = self.devices.get("dom0:mic:dom0:mic::m000000", None)
321+
microphone = self.devices.get("mic:dom0:mic:dom0:mic::m000000", None)
310322

311323
self.emit_notification(
312324
_("Device removed"),
@@ -318,6 +330,12 @@ def device_removed(self, vm, _event, port):
318330
microphone.devices_to_attach_with_me.remove(dev)
319331
if dev.port in self.parent_ports_to_hide:
320332
self.parent_ports_to_hide.remove(dev.port)
333+
if dev.parent in self.cameras_to_hide:
334+
for potential_parent in self.devices.values():
335+
if potential_parent.port == dev.parent:
336+
potential_parent.hide_this_device = False
337+
break
338+
self.cameras_to_hide.remove(dev.parent)
321339
del self.devices[dev_id]
322340

323341
def initialize_dev_data(self):
@@ -359,6 +377,12 @@ def initialize_dev_data(self):
359377
# we have no permission to access VM's devices
360378
continue
361379

380+
# hide parents of webcams
381+
for device in self.devices.values():
382+
if device.device_class == "webcam":
383+
if device.parent:
384+
self.cameras_to_hide.append(device.parent)
385+
362386
def device_assigned(self, vm, _event, device, **_kwargs):
363387
dev_id = backend.Device.id_from_device(device)
364388
if dev_id not in self.devices:
@@ -407,7 +431,7 @@ def update_single_feature(self, _vm, _event, feature, value=None, oldvalue=None)
407431
add = new - old
408432
remove = old - new
409433

410-
microphone = self.devices.get("dom0:mic:dom0:mic::m000000", None)
434+
microphone = self.devices.get("mic:dom0:mic:dom0:mic::m000000", None)
411435

412436
for dev_name in remove:
413437
if feature == backend.FEATURE_ATTACH_WITH_MIC:
@@ -436,6 +460,29 @@ def update_single_feature(self, _vm, _event, feature, value=None, oldvalue=None)
436460
self.parent_ports_to_hide.append(dev.port)
437461
self.hide_child_devices(dev.port, False)
438462

463+
def update_resolution(self, vm, _event, feature, value=None, oldvalue=None):
464+
# pylint: disable=unused-argument
465+
res_dict = {}
466+
if value:
467+
for word in value.split(" "):
468+
try:
469+
k, v = word.split("=")
470+
res_dict[k] = v
471+
except ValueError:
472+
# the feature is malformed, ignore it
473+
res_dict = {}
474+
475+
for device in self.devices.values():
476+
if (
477+
device.device_class == "webcam"
478+
and device.backend_domain.name == vm.name
479+
):
480+
resolution = res_dict.get(device.id_string, None)
481+
if resolution:
482+
device.options["format"] = resolution
483+
elif "format" in device.options:
484+
del device.options["format"]
485+
439486
def vm_unpaused(self, vm, _event, **_kwargs):
440487
wrapped_vm = backend.VM(vm)
441488
try:
@@ -473,7 +520,7 @@ def initialize_features(self, *_args, **_kwargs):
473520
"""
474521
domains = self.qapp.domains
475522

476-
microphone = self.devices.get("dom0:mic:dom0:mic::m000000", None)
523+
microphone = self.devices.get("mic:dom0:mic:dom0:mic::m000000", None)
477524
# clear existing feature mappings
478525
for dev in self.devices.values():
479526
dev.devices_to_attach_with_me = []
@@ -487,13 +534,21 @@ def initialize_features(self, *_args, **_kwargs):
487534
mic_feature = domain.features.get(
488535
backend.FEATURE_ATTACH_WITH_MIC, False
489536
)
537+
if not mic_feature:
538+
continue
490539
except qubesadmin.exc.QubesDaemonAccessError:
491540
continue
492-
if isinstance(mic_feature, str):
493-
mic_dev_strings.extend(
494-
[dev for dev in mic_feature.split(" ") if dev]
495-
)
496-
541+
for dev in mic_feature.split(" "):
542+
if not dev:
543+
continue
544+
try:
545+
_class, vm_name, _rest = dev.split(":", 2)
546+
if vm_name != domain.name:
547+
continue
548+
mic_dev_strings.append(dev)
549+
except ValueError:
550+
# malformed name
551+
pass
497552
microphone.devices_to_attach_with_me = []
498553

499554
for dev in mic_dev_strings:
@@ -519,6 +574,9 @@ def initialize_features(self, *_args, **_kwargs):
519574
dev.show_children = False
520575

521576
self.hide_child_devices()
577+
for dev in self.devices.values():
578+
if dev.port in self.cameras_to_hide:
579+
dev.hide_this_device = True
522580

523581
def hide_child_devices(
524582
self, parent_port: Optional[str] = None, state: bool = False

qui/tray/disk_space.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ def __create_widgets(vm_usage):
8181
icon = getattr(vm, "icon", vm.label.icon)
8282
except exc.QubesPropertyAccessError:
8383
icon = "appvm-black"
84-
icon_vm = Gtk.IconTheme.get_default().load_icon(icon, 16, 0)
84+
icon_vm = Gtk.IconTheme.get_default().load_icon(
85+
icon, 16, Gtk.IconLookupFlags.FORCE_SIZE
86+
)
8587
icon_img = Gtk.Image.new_from_pixbuf(icon_vm)
8688

8789
# description widget

qui/tray/domains.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def get_icon(self, icon_name):
7373
else:
7474
try:
7575
icon = Gtk.IconTheme.get_default().load_icon(
76-
self.icon_files[icon_name], 16, 0
76+
self.icon_files[icon_name], 16, Gtk.IconLookupFlags.FORCE_SIZE
7777
)
7878
self.icons[icon_name] = icon
7979
except (TypeError, GLib.Error):

0 commit comments

Comments
 (0)