Source code for devctrl.gui.devices.device_manager.device_manager

import logging

log = logging.getLogger(__name__)

from PyQt6.QtCore import Qt
from PyQt6.QtGui import QAction
from PyQt6.QtWidgets import QComboBox, QHBoxLayout, QMessageBox, QPushButton, QWidget, QVBoxLayout, QLabel
from PyQt6.QtWidgets import QFormLayout, QDialog, QDialogButtonBox, QLineEdit, QListWidget, QListWidgetItem
from PyQt6.QtWidgets import QScrollArea, QFrame

from ....devices.device_manager import DeviceManager, Device, DeviceRole
from ...widgets import ToolBar
from ..device import DeviceWidget
from ...widgets.settings_form import SettingsForm
from .device_dialogs import DeviceInfoWidget
from ..device_selection import select_device_dialog, SelectDeviceWidget
from devctrl.resources import load_icon


class DeviceSettingsWidget(QWidget):
    """
    Widget that shows the device info and allows for selecting a device to connect to.
    """
    def __init__(self, device_manager: DeviceManager, device_key: str, parent=None):
        super().__init__(parent)
        self.w_info = DeviceInfoWidget()             
        self._devmg = device_manager
        self._key = device_key
        if device_key in device_manager:
            dev_info = device_manager.get_info(device_key)
            self.w_info.set_device_info(dev_info)
        self.l_main = QVBoxLayout()
        self.setLayout(self.l_main)
        self.layout().addWidget(self.w_info)

        self.b_update_list = QPushButton()
        self.b_update_list.setText(self.tr("Update list of available devices"))
        self.b_update_list.clicked.connect(self.update_device_list)
        self.l_main.addWidget(self.b_update_list)

        self.w_device_list = QListWidget()
        self.l_main.addWidget(self.w_device_list)
        # initially, only show the entry of the currently connected device
        # since any new communication with devices should only be initiated by user interaction
        self._connection_info: tuple[str,str]|None = None
        if self._devmg.is_connected(self._key):
            try:
                dev_typename, dev_name = self._devmg.get_connection_info(self._key)
                w = QListWidgetItem(f"{dev_typename}: {dev_name}", parent=self.w_device_list)
                w.dev_type = dev_typename
                w.dev_name = dev_name
                self._connection_info = (dev_typename, dev_name)
                self.w_device_list.setCurrentItem(w)
            except KeyError:
                pass


    def update_device_list(self):
        """Update the list of available devices."""
        dev_info = self.w_info.get_device_info()
        type_devices = dev_info.device_type.list_devices()

        self.w_device_list.clear()
        for dev_type, dev_name in type_devices:
            w = QListWidgetItem(f"{dev_type}: {dev_name}", parent=self.w_device_list)
            w.dev_type = dev_type
            w.dev_name = dev_name
            # select the item if it is currently connected
            if self._connection_info and self._connection_info[0] == dev_type and self._connection_info[1] == dev_name:
                self.w_device_list.setCurrentItem(w)


    def get_selected_device(self) -> tuple[str,str]|None:
        """
        :return: device typename, device name of the currently selected item, or None if no item is selected
        """
        selected = self.w_device_list.currentItem()
        if selected is None:
            return None
        else:
            return selected.dev_type, selected.dev_name


    def get_device_info(self) -> DeviceRole:
        """
        :return: the current `DeviceRole` object
        """
        return self.w_info.get_device_info()

    def apply(self):
        info = self.w_info.get_device_info()
        # if key was changed, remove old reference
        if self._key != info.key:
            self._devmg.remove_device(self._key)
            self._key = info.key
        self._devmg.add_device_info(info)

    def connect(self):
        """
        Prompt the user to connect a device. Call `apply` first.
        """
        info = self.get_selected_device()
        if info is None:
            raise ValueError(self.tr("No device selected"))
        dev_typename, dev_name = info
        conn_dev_typename, conn_dev_name = None, None
        try:
            conn_dev_typename, conn_dev_name = self._devmg.get_connection_info(self._key)
        except KeyError:
            # not connected
            pass
        if dev_typename == conn_dev_typename and dev_name == conn_dev_name:
            pass
        else:
            self._devmg.connect_manually(self._key, dev_typename, dev_name)
            self._connection_info = info

    def apply_and_connect(self):
        self.apply()
        self.connect()


class DeviceSettingsDialog(QDialog):
    """Dialog showing a :class:`.DeviceSettingsWidget` for changing device settings and connecting a device."""
    def __init__(self, device_manager: DeviceManager, device_key: str, parent=None):
        super().__init__(parent=parent)
        self.l_main = QVBoxLayout()
        self.setLayout(self.l_main)
        self.setWindowTitle(self.tr("Settings for '{device_key}'").format(device_key=device_key))
        self._w_settings = DeviceSettingsWidget(device_manager, device_key)
        self.l_main.addWidget(self._w_settings)

        b_apply = QPushButton(self.tr("Apply"))
        b_apply.pressed.connect(self.apply)
        b_ok = QPushButton(self.tr("Ok"))
        b_ok.pressed.connect(self.apply_exit)
        b_cancel = QPushButton(self.tr("Cancel"))
        b_cancel.pressed.connect(self.reject)

        # ok and cancel
        buttons = QDialogButtonBox()
        # manual button creation since buttons must be changed to "Ok and connect" and "Apply and connect" when the user selects a different device
        # TODO: implement change button string
        buttons.addButton(b_ok,     QDialogButtonBox.ButtonRole.AcceptRole)
        buttons.addButton(b_apply,  QDialogButtonBox.ButtonRole.ApplyRole)
        buttons.addButton(b_cancel, QDialogButtonBox.ButtonRole.RejectRole)
        self.l_main.addWidget(buttons)

    def apply(self):
        log.debug(f"Applying settings")
        self._w_settings.apply_and_connect()

    def apply_exit(self):
        self.apply()
        self.accept()

    def connect(self):
        self._w_settings.apply_and_connect()



[docs] class DeviceManagerWidget(QWidget): """Widget that contains device widgets for all devices that are handled by a :class:`device manager <.DeviceManager>` instance. """
[docs] def __init__(self, device_manager: DeviceManager, parent=None): super().__init__(parent=parent) self.l_main = QVBoxLayout() self.setLayout(self.l_main) # Toolbar actions for adding/removing device roles; the logic lives in # the actions so they can be reused elsewhere (e.g. module toolbars). self.a_add_device = QAction(load_icon("list-add"), self.tr("Add device"), self) self.a_add_device.triggered.connect(self.add_device_dialog) self.a_remove_device = QAction(load_icon("delete-five"), self.tr("Remove device"), self) self.a_remove_device.triggered.connect(self.remove_device_dialog) self.toolbar = ToolBar(self, orientation=Qt.Orientation.Horizontal, style=Qt.ToolButtonStyle.ToolButtonTextBesideIcon, icon_size=(24, 24)) self.toolbar.addAction(self.a_add_device) self.toolbar.addAction(self.a_remove_device) self.l_main.addWidget(self.toolbar) self.l_devices = QVBoxLayout() self.l_devices.addStretch() # The device list lives in a scroll area, so device widgets always keep # their ideal size and a scrollbar appears when the list exceeds the # visible height. # The device list layout must be attached to a widget's layout (here: # the scroll area's content widget), otherwise the device widgets are # added to an orphaned layout and the Devices tab renders empty even # though the widgets exist internally. self.w_device_list_widget = QWidget() self.w_device_list_widget.setLayout(self.l_devices) self.w_device_scroll = QScrollArea() self.w_device_scroll.setWidgetResizable(True) self.w_device_scroll.setFrameShape(QFrame.Shape.NoFrame) self.w_device_scroll.setWidget(self.w_device_list_widget) self.l_main.addWidget(self.w_device_scroll) self.device_manager = device_manager self._device_widgets = {} # Populate with any device roles already registered on the manager # (roles are usually added before this widget is constructed, so the # device_added signals were emitted before we subscribed to them). for dev_key in self.device_manager: self._add_device(dev_key) self.device_manager.device_updated.connect(lambda key: self._update_device(key)) self.device_manager.device_added.connect(lambda key: self._update_device(key)) # update may also add one if it does not exist yet self.device_manager.device_removed.connect(lambda key: self._remove_device(key))
# self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
[docs] def _get_new_device_widget(self, dev_key): """ Create a widget for the device with `dev_key` """ kw = dict(key=dev_key, icon_button_f=lambda: self.edit_device_dialog(dev_key)) if self.device_manager.is_connected(dev_key): w = DeviceWidget.get_widget(self.device_manager[dev_key], name=self.device_manager.get_info(dev_key).name, **kw) else: w = DeviceWidget(device=None, **kw) return w
[docs] def _add_device(self, dev_key): """ Create and add a widget for device with `dev_key` to the list. """ log.debug(f"Adding widget for device '{dev_key}'") w = self._get_new_device_widget(dev_key) self._device_widgets[dev_key] = w self._add_device_widget(w)
[docs] def _remove_device(self, dev_key): """ Remove the widget of device with `dev_key` from the list """ log.debug(f"Removed widget for device '{dev_key}'") # signals may be delivered late/out of order (the pure-python signal # fallback queues emissions), so the widget may already be gone. w = self._device_widgets.pop(dev_key, None) if w is None: return self.l_devices.removeWidget(w) w.deleteLater()
[docs] def _add_device_widget(self, dev: DeviceWidget|QLabel): """ Append a widget into the list """ self.l_devices.insertWidget(self.l_devices.count() - 1, dev, stretch=0)
[docs] def _update_device_widget(self, dev_key: str): """ Update the widget for device `dev_key`, by deleting the old widget and creating a new one. """ # w = self._device_widgets[key].update() w_old = self._device_widgets[dev_key] w_new = self._get_new_device_widget(dev_key) layout_item = self.l_devices.replaceWidget(w_old, w_new) self._device_widgets[dev_key] = w_new # layout_item.deleteLater() # layout_item.widget().deleteLater() w_old.deleteLater() log.debug(f"Updated widget for device '{dev_key}'")
[docs] def _update_device(self, dev_key): """ Update or add a widget for the device with `dev_key`. """ log.debug(f"Updating or adding device '{dev_key}'") # either updated or added if dev_key in self.device_manager: # added if dev_key not in self._device_widgets: self._add_device(dev_key) # updated else: self._update_device_widget(dev_key) # removed elif dev_key in self._device_widgets: self._remove_device(dev_key) else: # signals may be delivered late (the pure-python signal fallback # queues emissions), so the device may already be gone. log.warning(f"Ignoring update for unknown device key: '{dev_key}'")
[docs] def _update_devices(self): log.info(f"Updating devices list widget") raise NotImplementedError()
[docs] def edit_device_dialog(self, dev_key: str): dialog = DeviceSettingsDialog(self.device_manager, dev_key) dialog.exec()
[docs] def add_device_dialog(self): """Prompt for adding a new device to the manager.""" dialog = QDialog(self) dialog.setWindowTitle(self.tr("Add new device")) dialog.setLayout(QVBoxLayout()) w_add = DeviceInfoWidget() dialog.layout().addWidget(w_add) # ok and cancel buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) buttons.accepted.connect(dialog.accept) buttons.rejected.connect(dialog.reject) dialog.layout().addWidget(buttons) # Re-open the dialog on invalid input (e.g. missing device type), close on cancel. while True: ret = dialog.exec() if ret == QDialog.DialogCode.Accepted: try: dev_info = w_add.get_device_info() except ValueError as e: QMessageBox.critical(self, self.tr("Error"), self.tr("Invalid input:\n{error}").format(error=e)) continue self.device_manager.add_device_info(dev_info) break else: break
[docs] def remove_device_dialog(self): """Prompt for removing a device from the manager.""" dialog = QDialog(self) dialog.setWindowTitle(self.tr("Remove device")) l_dialog = QVBoxLayout() dialog.setLayout(l_dialog) w_devs = QListWidget() l_dialog.addWidget(w_devs) for key in self.device_manager: w_devs.addItem(key) # ok and cancel buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) buttons.accepted.connect(dialog.accept) buttons.rejected.connect(dialog.reject) l_dialog.addWidget(buttons) ret = dialog.exec() if ret == QDialog.DialogCode.Accepted: if w_devs.currentItem() is not None: dev_key = w_devs.currentItem().text() self.device_manager.remove_device(dev_key)
[docs] def __len__(self): return len(self._device_widgets)
[docs] def __getitem__(self, idx): return self._device_widgets[idx]