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

import logging

from devctrl.devices import device, device_info
log = logging.getLogger(__name__)

from PyQt6.QtWidgets import QComboBox, QHBoxLayout, QMessageBox, QPushButton, QWidget, QVBoxLayout, QLabel
from PyQt6.QtWidgets import QFormLayout, QDialog, QDialogButtonBox, QLineEdit, QListWidget, QListWidgetItem

from ....devices.device_manager import DeviceManager, Device, DeviceInfo
from ..device import DeviceWidget
from ...widgets.settings_form import SettingsForm
from .device_dialogs import DeviceInfoWidget
from ..device_selection import select_device_dialog, SelectDeviceWidget


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.setLayout(QVBoxLayout())
        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.layout().addWidget(self.b_update_list)

        self.w_device_list = QListWidget()
        self.layout().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):
        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) -> DeviceInfo:
        """
        :return: the current `DeviceInfo` 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(f"No device selected")
        dev_typename, dev_name = info
        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.setLayout(QVBoxLayout())
        self.setWindowTitle(self.tr("Settings for '{device_key}'").format(device_key=device_key))
        self._w_settings = DeviceSettingsWidget(device_manager, device_key)
        self.layout().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.layout().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 DeviceManagerSettingsWidget(QWidget): """Widget that lets the user add/remove devices from a device manager The widget shows an 'add' and 'remove' button that open a dialog when clicked. """
[docs] def __init__(self, device_manager: DeviceManager, parent=None): super().__init__(parent=parent) self.l = QHBoxLayout() self.setLayout(self.l) self.device_manager = device_manager self.b_add = QPushButton() self.b_add.setText(self.tr(f"Add device")) self.b_add.pressed.connect(self.add_device) self.l.addWidget(self.b_add) self.b_remove = QPushButton() self.b_remove.setText(self.tr(f"Remove device")) self.b_remove.pressed.connect(self.remove_device) self.l.addWidget(self.b_remove)
[docs] def add_device(self): """ Prompt for adding a new device """ 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) 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, "Error", f"Invalid input:\n{e}") continue self.device_manager.add_device_info(dev_info) break else: break
[docs] def remove_device(self): """ Prompt for removing a new device """ 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 i, key in enumerate(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] 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 = QVBoxLayout() self.setLayout(self.l) self.l.addStretch() self.device_manager = device_manager self._device_widgets = {} # not required, the signals will handle this even for existing devices # for dev_key in self.device_manager: # self._add_device(dev_key) self.device_manager.e_device_updated.connect(lambda key: self._update_device(key)) self.device_manager.e_device_added.connect(lambda key: self._update_device(key)) # update may also add one if it does not exist yet self.device_manager.e_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}'") w = self._device_widgets.pop(dev_key) self.l.removeWidget(w) w.deleteLater()
[docs] def _add_device_widget(self, dev: DeviceWidget|QLabel): """ Append a widget into the list """ self.l.insertWidget(self.l.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.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: raise KeyError(f"Invalid 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 __len__(self): return len(self._device_widgets)
[docs] def __getitem__(self, idx): return self._device_widgets[idx]