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)