diff --git a/icons/bluetooth.svg b/icons/bluetooth.svg new file mode 100644 index 00000000..2ac08b6d --- /dev/null +++ b/icons/bluetooth.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/camera.svg b/icons/camera.svg new file mode 100644 index 00000000..45a9f78d --- /dev/null +++ b/icons/camera.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/card-sim.svg b/icons/card-sim.svg new file mode 100644 index 00000000..4067fae6 --- /dev/null +++ b/icons/card-sim.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/circuit-board.svg b/icons/circuit-board.svg new file mode 100644 index 00000000..8bf3cf83 --- /dev/null +++ b/icons/circuit-board.svg @@ -0,0 +1 @@ + diff --git a/icons/clapperboard.svg b/icons/clapperboard.svg new file mode 100644 index 00000000..1bc2e6cb --- /dev/null +++ b/icons/clapperboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/cpu.svg b/icons/cpu.svg new file mode 100644 index 00000000..f4b75b2f --- /dev/null +++ b/icons/cpu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/dock.svg b/icons/dock.svg new file mode 100644 index 00000000..0a321512 --- /dev/null +++ b/icons/dock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/gpu.svg b/icons/gpu.svg new file mode 100644 index 00000000..e04f99ba --- /dev/null +++ b/icons/gpu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/keyboard.svg b/icons/keyboard.svg new file mode 100644 index 00000000..5cbeef43 --- /dev/null +++ b/icons/keyboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/memory-stick.svg b/icons/memory-stick.svg new file mode 100644 index 00000000..c85b07bb --- /dev/null +++ b/icons/memory-stick.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/monitor.svg b/icons/monitor.svg new file mode 100644 index 00000000..c333b796 --- /dev/null +++ b/icons/monitor.svg @@ -0,0 +1 @@ + diff --git a/icons/mouse.svg b/icons/mouse.svg new file mode 100644 index 00000000..2f92332b --- /dev/null +++ b/icons/mouse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/speaker.svg b/icons/speaker.svg new file mode 100644 index 00000000..e4cd50ab --- /dev/null +++ b/icons/speaker.svg @@ -0,0 +1 @@ + diff --git a/icons/usb.svg b/icons/usb.svg new file mode 100644 index 00000000..122642f0 --- /dev/null +++ b/icons/usb.svg @@ -0,0 +1 @@ + diff --git a/qubesmanager/settings.py b/qubesmanager/settings.py index 9028ddef..daa3c2b1 100644 --- a/qubesmanager/settings.py +++ b/qubesmanager/settings.py @@ -32,6 +32,7 @@ import datetime from qubesadmin.tools import QubesArgumentParser from qubesadmin import device_protocol +from qubesadmin.device_protocol import DeviceCategory from qubesadmin import utils as admin_utils from qubesadmin.tools import qvm_start import qubesadmin.exc @@ -159,6 +160,23 @@ class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtWidgets.QDialog): ) ) + device_icons = { + DeviceCategory.Network: "networking", + DeviceCategory.Keyboard: "keyboard", + DeviceCategory.Mouse: "mouse", + DeviceCategory.Camera: "camera", + DeviceCategory.Audio: "speaker", + DeviceCategory.Multimedia: "clapperboard", + DeviceCategory.Storage: "storage", + DeviceCategory.Bluetooth: "Bluetooth", + DeviceCategory.Smart_Card_Readers: "card-sim", + DeviceCategory.Display: "monitor", + DeviceCategory.Memory: "memory-stick", + DeviceCategory.Docking_Station: "dock", + DeviceCategory.Processor: "cpu", + DeviceCategory.PCI_USB: "usb", + } + # pylint: disable=too-many-positional-arguments def __init__( self, vm_name, init_page="basic", qapp=None, qubesapp=None, parent=None @@ -244,6 +262,17 @@ def __init__( self.current_strict_reset_list = [] self.new_strict_reset_list = [] self.define_strict_reset_devices() + self.device_radio_buttons = { + self.dev_network: DeviceCategory.Network, + self.dev_display: DeviceCategory.Display, + self.dev_audio: DeviceCategory.Audio, + self.dev_storage: DeviceCategory.Storage, + self.dev_usb_controller: DeviceCategory.PCI_USB, + } + self.dev_search.textChanged.connect(self.filter_devices) + for button in self.device_radio_buttons: + button.toggled.connect(self.filter_devices) + self.dev_other.toggled.connect(self.filter_devices) ####### services tab self.__init_services_tab__() @@ -1493,6 +1522,8 @@ def bootmode_changed(self): ######## devices tab def __init_devices_tab__(self): self.dev_list = multiselectwidget.MultiSelectWidget(self) + self.dev_list.selected_list.setSpacing(5) + self.dev_list.available_list.setSpacing(5) self.dev_list.change_labels( available="Available devices", selected="Devices always connected to this qube", @@ -1507,7 +1538,7 @@ def __init_devices_tab__(self): .devices["pci"] .get_exposed_devices() if dev.interfaces[0].category - != device_protocol.DeviceCategory.PCI_Bridge + != DeviceCategory.PCI_Bridge ) attached = list( self.vm.devices["pci"].get_assigned_devices(required_only=True) @@ -1530,6 +1561,15 @@ def __init__(self, dev, unknown=False, parent=None): name += " (unknown)" self.setText(name) self.dev = dev + intfs = list({i.category for i in dev.interfaces}) + if len(intfs) != 1 or intfs[0] not in VMSettingsWindow.device_icons: + self.setIcon(QtGui.QIcon(":/circuit-board")) + else: + self.setIcon( + QtGui.QIcon( + ":/{}".format(VMSettingsWindow.device_icons[intfs[0]]) + ) + ) for dev in dom0_devs: if any(attached_dev.matches(dev) for attached_dev in attached): @@ -1658,6 +1698,59 @@ def strict_reset_button_pressed(self): ) device_list_window.exec() + def filter_devices(self): + keywords = [word for word in self.dev_search.text().split(" ") if word] + for i in range(self.dev_list.available_list.count()): + device = self.dev_list.available_list.item(i) + hide = False + for button, category in self.device_radio_buttons.items(): + if button.isChecked(): + hide = True + for i in device.dev.interfaces: + if i.category == category: + hide = False + break + if self.dev_other.isChecked(): + hide = True + for i in device.dev.interfaces: + if i.category not in self.device_radio_buttons.values(): + hide = False + break + for word in keywords: + if not re.search( + word, + device.text(), + re.IGNORECASE + ): + hide = True + break + device.setHidden(hide) + for i in range(self.dev_list.selected_list.count()): + device = self.dev_list.selected_list.item(i) + hide = False + for button, category in self.device_radio_buttons.items(): + if button.isChecked(): + hide = True + for i in device.dev.interfaces: + if i.category == category: + hide = False + break + if self.dev_other.isChecked(): + hide = True + for i in device.dev.interfaces: + if i.category not in self.device_radio_buttons.values(): + hide = False + break + for word in keywords: + if not re.search( + word, + device.text(), + re.IGNORECASE + ): + hide = True + break + device.setHidden(hide) + ######## applications tab def refresh_apps_button_pressed(self): diff --git a/qubesmanager/tests/test_vm_settings.py b/qubesmanager/tests/test_vm_settings.py index 6a44610e..2b685d04 100644 --- a/qubesmanager/tests/test_vm_settings.py +++ b/qubesmanager/tests/test_vm_settings.py @@ -2044,3 +2044,35 @@ def test_603_virtmode_limitation(settings_fixture): assert "PVH" not in available_virtmodes assert "HVM (current)" in available_virtmodes assert "PV" in available_virtmodes + + +@check_errors +@pytest.mark.parametrize("settings_fixture", ["test-pci-dev"], indirect=True) +def test_604_device_filter(settings_fixture): + settings_window, page, vm_name = settings_fixture + vm = settings_window.qubesapp.domains[vm_name] + settings_window.dev_search.setText("usb") + settings_window.dev_usb_controller.setChecked(True) + + for i in range(settings_window.dev_list.available_list.count()): + item = settings_window.dev_list.available_list.item(i) + assert item.isHidden() != ("USB" in item.text()) + + for i in range(settings_window.dev_list.selected_list.count()): + item = settings_window.dev_list.selected_list.item(i) + assert item.isHidden() != ("USB" in item.text()) + + settings_window.dev_search.setText("") + settings_window.dev_other.setChecked(True) + + for i in range(settings_window.dev_list.available_list.count()): + item = settings_window.dev_list.available_list.item(i) + if not item.isHidden(): + for i in item.dev.interfaces: + assert i.category not in settings_window.device_radio_buttons.values() + + for i in range(settings_window.dev_list.selected_list.count()): + item = settings_window.dev_list.selected_list.item(i) + if not item.isHidden(): + for i in item.dev.interfaces: + assert i.category not in settings_window.device_radio_buttons.values() diff --git a/resources.qrc b/resources.qrc index 16f40cd9..5f3180fb 100644 --- a/resources.qrc +++ b/resources.qrc @@ -47,5 +47,19 @@ icons/fast-rewind.svg icons/step-back.svg icons/step-forward.svg + icons/bluetooth.svg + icons/camera.svg + icons/card-sim.svg + icons/circuit-board.svg + icons/clapperboard.svg + icons/cpu.svg + icons/dock.svg + icons/gpu.svg + icons/keyboard.svg + icons/memory-stick.svg + icons/monitor.svg + icons/mouse.svg + icons/speaker.svg + icons/usb.svg diff --git a/ui/settingsdlg.ui b/ui/settingsdlg.ui index 905a1c67..608f52bd 100644 --- a/ui/settingsdlg.ui +++ b/ui/settingsdlg.ui @@ -1849,6 +1849,103 @@ The qube must be running to disable seamless mode. This setting is not persisten + + + + + + Filter: + + + + + + + + + + + + + + Class: + + + + + + + All + + + true + + + + + + + Network + + + + :/networking:/networking + + + + + + + Display + + + + :/monitor:/monitor + + + + + + + Audio + + + + :/speaker:/speaker + + + + + + + Storage + + + + :/storage:/storage + + + + + + + USB Controller + + + + :/usb:/usb + + + + + + + Other + + + + +