Source code for QVideo.lib.QCameraTree

'''Auto-building pyqtgraph parameter tree for QCamera property inspection.'''
from qtpy import QtCore, QtGui
from pyqtgraph.parametertree import Parameter, ParameterTree
from QVideo.lib import QCamera, QVideoSource
import logging


__all__ = ['QCameraTree']


logger = logging.getLogger(__name__)
logger.setLevel(logging.WARNING)


Source = QCamera | QVideoSource
Description = list[dict[str, object]]
Change = tuple[Parameter, str, QCamera.PropertyValue]
Changes = list[Change]


[docs] class QCameraTree(ParameterTree): '''A parameter tree widget for controlling :class:`~QVideo.lib.QCamera.QCamera` properties. Wraps a :class:`~QVideo.lib.QVideoSource.QVideoSource` (or a bare :class:`~QVideo.lib.QCamera.QCamera`) and presents its settings as an editable :class:`~pyqtgraph.parametertree.ParameterTree`. Changes made in the tree are pushed to the camera; camera-side changes are reflected back into the tree. Parameters ---------- source : QCamera or QVideoSource The video source to control. Must already be open. description : list[dict] or None Parameter-tree description of the camera properties to expose. If ``None`` a default description is generated from :attr:`~QVideo.lib.QCamera.QCamera.settings`. *args : Additional positional arguments forwarded to :class:`~pyqtgraph.parametertree.ParameterTree`. **kwargs : Additional keyword arguments forwarded to :class:`~pyqtgraph.parametertree.ParameterTree`. Raises ------ RuntimeError If *source* is not open when the tree is created. ''' @classmethod def _getParameters(cls, parameter: Parameter) -> dict[str, Parameter]: '''Recursively collect leaf :class:`~pyqtgraph.parametertree.Parameter` nodes.''' parameters = dict() for child in parameter.children(): if child.hasChildren(): parameters.update(cls._getParameters(child)) else: parameters.update({child.name(): child}) return parameters @staticmethod def _defaultDescription(camera: QCamera) -> Description: entries = [] for name, spec in camera._properties.items(): value = spec['getter']() if value is None: continue entry = {'name': name, 'type': type(value).__name__, 'value': value, 'default': value} if spec['setter'] is None: entry['enabled'] = False entries.append(entry) return entries def __init__(self, source: Source, description: Description | None = None, *args, **kwargs) -> None: super().__init__(*args, **kwargs) if not source.isOpen(): raise RuntimeError('Video source is not open') if isinstance(source, QCamera): self._source = QVideoSource(source) else: self._source = source self._ignoreSync = False self._createTree(description) self._connectSignals() self._setupUi()
[docs] def closeEvent(self, event: QtGui.QCloseEvent) -> None: '''Stop the video source when the widget is closed.''' self.stop() super().closeEvent(event)
def _createTree(self, description: Description | None) -> None: if description is None: description = self._defaultDescription(self.camera) logger.debug(description) self._tree = Parameter.create(name=self.camera.name, type='group', children=description) self.setParameters(self._tree) self._parameters = self._getParameters(self._tree) def _connectSignals(self) -> None: self._tree.sigTreeStateChanged.connect(self._sync) QtCore.QCoreApplication.instance().aboutToQuit.connect(self.stop) def _setupUi(self) -> None: header = self.header() self.setTextElideMode(QtCore.Qt.TextElideMode.ElideRight) self.setIndentation(10) count = self.columnCount() for n in range(count - 1): header.setSectionResizeMode(n, header.ResizeMode.Interactive) self.resizeColumnToContents(n) header.setSectionResizeMode(count - 1, header.ResizeMode.Stretch) self.adjustSize() self.setMinimumWidth(self.width()) @QtCore.Slot(object, object) def _sync(self, root: Parameter, changes: Changes) -> None: if self._ignoreSync: return for param, change, value in changes: if (change == 'value'): key = param.name() logger.debug(f'Syncing {key}: {change}: {value}') self.camera.set(key, value) self._ignoreSync = True for key, value in self.camera.settings.items(): self.set(key, value) self._ignoreSync = False
[docs] @QtCore.Slot(str, object) def set(self, key: str, value: QCamera.PropertyValue) -> None: '''Set a camera property and update the tree. Parameters ---------- key : str Property name. value : QCamera.PropertyValue New value. ''' if key in self._parameters: logger.debug(f'set {key}: {value}') self._parameters[key].setValue(value) else: logger.warning(f'Unsupported property: {key}')
[docs] def get(self, key: str) -> QCamera.PropertyValue | None: '''Get a camera property value from the tree. Parameters ---------- key : str Property name. Returns ------- QCamera.PropertyValue or None Current value, or ``None`` if *key* is not in the tree. ''' if key in self._parameters: return self._parameters[key].value() logger.warning(f'Unsupported property: {key}') return None
@property def source(self) -> QVideoSource: '''The underlying :class:`~QVideo.lib.QVideoSource.QVideoSource`.''' return self._source @property def camera(self) -> QCamera: '''The :class:`~QVideo.lib.QCamera.QCamera` driven by this tree.''' return self.source.source
[docs] @QtCore.Slot() def start(self) -> 'QCameraTree': '''Start the video source. Returns ------- QCameraTree ``self``, to allow chaining (e.g. ``tree = QCameraTree(...).start()``). ''' self.source.start() return self
[docs] @QtCore.Slot() def stop(self) -> None: '''Stop and join the video source thread.''' if self.source.isRunning(): self.source.stop() self.source.quit() self.source.wait()
[docs] @classmethod def example(cls: type['QCameraTree']) -> None: # pragma: no cover '''Demonstrate the widget with a default camera source.''' import pyqtgraph as pg app = pg.mkQApp(f'{cls.__name__} Example') tree = cls().start() tree.show() app.exec()