Source code for lantz.ui.widgets

# -*- coding: utf-8 -*-
"""
    lantz.ui.widgets
    ~~~~~~~~~~~~~~~~

    Implements UI widgets based on Qt widgets. To achieve functionality,
    instances of QtWidgets are patched.

    :copyright: 2015 by Lantz Authors, see AUTHORS for more details.
    :license: BSD, see LICENSE for more details.
"""

import time
import json
import inspect

from lantz.utils import is_building_docs
from lantz.utils.qt import QtCore, QtGui

__PRINT_TRACEBACK__ = True

try:
    from docutils import core as doc_core
except ImportError:
    class doc_core(object):

        @staticmethod
        def publish_parts(rst, *args, **kwargs):
            return rst


from .. import Q_, Driver, initialize_many
from ..feat import MISSING, DictFeat
from ..log import get_logger

QtGui.QToolTip.setFont(QtGui.QFont('SansSerif', 10))

logger = get_logger('lantz.ui', False)


def _rst_to_html(rst):
    """Convert rst docstring to HTML.
    """
    parts = doc_core.publish_parts(rst, writer_name="html")
    return parts['body']


def _params_doc(rst):
    """Extract
    """
    if not rst:
        return ''
    docs = {}
    rst = ' '.join(rst.splitlines())
    key = None
    for line in rst.split(':'):
        line = line.strip()
        if key:
            docs[key] = line.strip()
            key = None
        else:
            for prefix in ('param', 'parameter', 'arg', 'argument', 'key', 'keyword'):
                if line.startswith(prefix):
                    key = line[len(prefix):].strip()
                    break

    return docs


[docs]def register_wrapper(cls): """Register a class as lantz wrapper for QWidget subclasses. The class must contain a field (_WRAPPERS) with a tuple of the QWidget subclasses that it wraps. """ for wrapped in cls._WRAPPED: if wrapped in cls._WRAPPERS: logger.warn('{} is already registered to {}.'.format(wrapped, cls._WRAPPERS[wrapped])) if is_building_docs: cls._WRAPPERS[wrapped] = type(wrapped.__name__ + 'Wrapped', (cls, ), {'_IS_LANTZ_WRAPPER': True}) else: cls._WRAPPERS[wrapped] = type(wrapped.__name__ + 'Wrapped', (cls, wrapped), {'_IS_LANTZ_WRAPPER': True}) return cls
@register_wrapper
[docs]class WidgetMixin(object): """Mixin class to provide extra functionality to QWidget derived controls. Derived class must override _WRAPPED to indicate with which classes it can be mixed. To wrap an existing widget object use:: >>> widget = QComboBox() >>> WidgetMixin.wrap(widget) If you want lantz to provide an appropriate wrapped widget for a given feat:: >>> widget = WidgetMixin.from_feat(feat) In any case, after wrapping a widget you need to bind it to a feat:: >>> feat = driver.feats[feat_name] >>> widget.bind_feat(feat) Finally, you need to >>> widget.lantz_target = driver """ _WRAPPED = (QtGui.QWidget, ) #: Dictionary linking Widget types with the function to patch them _WRAPPERS = {}
[docs] def keyPressEvent(self, event): """When 'u' is pressed, request new units. When 'r' is pressed, get new value from the driver. """ super().keyPressEvent(event) if event.text() == 'r': # This should also trigger a widget update if necessary. self.value_from_feat()
[docs] def value(self): """Get widget value. """ return super().value()
[docs] def setValue(self, value): """Set widget value. """ if value is MISSING: return super().setValue(value)
[docs] def setReadOnly(self, value): """Set read only s """ super().setReadOnly(value)
[docs] def value_from_feat(self): """Update the widget value with the current Feat value of the driver. """ if self._feat is None or self._lantz_target is None: return self._feat.get(self._lantz_target, key=self._feat_key)
[docs] def value_to_feat(self): """Update the Feat value of the driver with the widget value. """ if self._feat is None or self._lantz_target is None: return self._feat.set(self._lantz_target, value=self.value(), key=self._feat_key)
@property def readable(self): """If the Feat associated with the widget can be read (get). """ if self._feat is None: return False return self._feat.fget not in (None, MISSING) @property def writable(self): """If the Feat associated with the widget can be written (set). """ if self._feat is None: return False return self._feat.fset is not None #@Slot()
[docs] def on_widget_value_changed(self, value, old_value=MISSING, other=MISSING): """When the widget is changed by the user, update the driver with the new value. """ if self._update_on_change: self.value_to_feat()
[docs] def on_feat_value_changed(self, value, old_value=MISSING, other=MISSING): """When the driver value is changed, update the widget if necessary. """ if self.value() != value: self.setValue(value)
@property def feat_key(self): """Key associated with the DictFeat. """ return self._feat_key @feat_key.setter def feat_key(self, value): if self._lantz_target: getattr(self._lantz_target, self._feat.name + '_changed').disconnect(self.on_feat_value_changed) self._feat_key = value if self._lantz_target: getattr(self._lantz_target, self._feat.name + '_changed').connect(self.on_feat_value_changed) self.value_from_feat() @property def lantz_target(self): """Driver connected to the widget. """ return self._lantz_target @lantz_target.setter def lantz_target(self, target): if self._lantz_target: getattr(self._lantz_target, self._feat.name + '_changed').disconnect(self.on_feat_value_changed) self.valueChanged.disconnect() if target: self._lantz_target = target getattr(self._lantz_target, self._feat.name + '_changed').connect(self.on_feat_value_changed) self.value_from_feat() self.valueChanged.connect(self.on_widget_value_changed) def bind_feat(self, feat): self._feat = feat try: keys = feat.keys except: keys = None if keys: self._feat_key = keys[0] else: self._feat_key = MISSING self.setReadOnly(not self.writable) @classmethod def _wrap(cls, widget): ChildrenWidgets.patch(widget) widget._lantz_target = None widget._feat = None widget._update_on_change = True @classmethod def wrap(cls, widget): if hasattr(widget, '_lantz_wrapped'): return if getattr(widget, '_IS_LANTZ_WRAPPER', False): widget._wrap(widget) else: wrapper_class = cls._WRAPPERS.get(type(widget), cls) wrapper_class._wrap(widget) widget.__class__ = wrapper_class widget._lantz_wrapped = True @classmethod
[docs] def from_feat(cls, feat, parent=None): """Return a widget appropriate to represent a lantz feature. :param feat: a lantz feature proxy, the result of inst.feats[feat_name]. :param parent: parent widget. """ _get = cls._WRAPPERS.get #_get = lambda x: x if feat.values: if isinstance(feat.values, dict): tmp = set(feat.values.keys()) else: tmp = set(feat.values) if tmp == {True, False}: widget = _get(QtGui.QCheckBox) else: widget = _get(QtGui.QComboBox) elif not feat.units is None or feat.limits: widget = _get(QtGui.QDoubleSpinBox) else: widget= _get(QtGui.QLineEdit) widget = widget(parent) cls.wrap(widget) return widget
[docs]class FeatWidget(object): """Widget to show a Feat. """ def __new__(cls, parent, target, feat): """ :param parent: parent widget. :param target: driver object to connect. :param feat: Feat to connect. """ widget = WidgetMixin.from_feat(feat, parent) widget.bind_feat(feat) widget.lanz_target = target return widget
class DictFeatWidget(QtGui.QWidget): """Widget to show a DictFeat. :param parent: parent widget. :param target: driver object to connect. :param feat: DictFeat to connect. """ def __init__(self, parent, target, feat): super().__init__(parent) self._feat = feat layout = QtGui.QHBoxLayout(self) if feat.keys: wid = QtGui.QComboBox() if isinstance(feat.keys, dict): self._keys = list(feat.keys.keys()) else: self._keys = list(feat.keys) wid.addItems([str(key) for key in self._keys]) wid.currentIndexChanged.connect(self._combobox_changed) else: wid = QtGui.QLineEdit() wid.textChanged.connect(self._lineedit_changed) layout.addWidget(wid) self._key_widget = wid wid = WidgetMixin.from_feat(feat) wid.bind_feat(feat) wid.feat_key = self._keys[0] wid.lantz_target = target layout.addWidget(wid) self._value_widget = wid @QtCore.Slot(int, object, object) def _combobox_changed(self, value, old_value=MISSING, other=MISSING): self._value_widget.feat_key = self._keys[self._key_widget.currentIndex()] @QtCore.Slot(str, object, object) def _lineedit_changed(self, value, old_value=MISSING, other=MISSING): self._value_widget.feat_key = self._key_widget.text() def value(self): """Get widget value. """ return self._value_widget.value() def setValue(self, value): """Set widget value. """ if value is MISSING: return self._value_widget.setValue(value) def setReadOnly(self, value): """Set read only s """ self._value_widget.setReadOnly(value) @property def lantz_target(self): """Driver connected to this widget. """ return self._value_widget._lantz_target @lantz_target.setter def lantz_target(self, driver): self._value_widget._lantz_target = driver @property def readable(self): """If the Feat associated with the widget can be read (get). """ return self._value_widget.readable @property def writable(self): """If the Feat associated with the widget can be written (set). """ return self._value_widget.writable def value_from_feat(self): return self._value_widget.value_from_feat() class LabeledFeatWidget(QtGui.QWidget): """Widget containing a label, a control, and a get a set button. :param parent: parent widget. :param target: driver object to connect. :param feat: Feat to connect. """ def __init__(self, parent, target, feat): super().__init__(parent) layout = QtGui.QHBoxLayout(self) self._label = QtGui.QLabel() self._label.setText(feat.name) self._label.setFixedWidth(120) self._label.setToolTip(_rst_to_html(feat.__doc__)) layout.addWidget(self._label) if isinstance(feat.feat, DictFeat): self._widget = DictFeatWidget(parent, target, feat) else: self._widget = WidgetMixin.from_feat(feat) self._widget.bind_feat(feat) self._widget.lantz_target = target layout.addWidget(self._widget) self._get = QtGui.QPushButton() self._get.setText('get') self._get.setEnabled(self._widget.readable) self._get.setFixedWidth(60) layout.addWidget(self._get) self._set = QtGui.QPushButton() self._set.setText('set') self._set.setEnabled(self._widget.writable) self._set.setFixedWidth(60) layout.addWidget(self._set) self._get.clicked.connect(self.on_get_clicked) self._set.clicked.connect(self.on_set_clicked) self._widget._update_on_change = self._widget.writable self.widgets = (self._label, self._widget, self._get, self._set) @property def label_width(self): """Width of the label """ return self._label.width @label_width.setter def label_width(self, value): self._label.setFixedWidth(value) @property def lantz_target(self): """Driver connected to this widget. """ return self._widget._lantz_target @lantz_target.setter def lantz_target(self, driver): self._widget._lantz_target = driver @QtCore.Slot() def on_get_clicked(self): self._widget.value_from_feat() @QtCore.Slot() def on_set_clicked(self): font = QtGui.QFont() font.setItalic(False) self._widget.setFont(font) self._widget.value_to_feat() @property def readable(self): """If the Feat associated with the widget can be read (get). """ return self._widget.readable @property def writable(self): """If the Feat associated with the widget can be written (set). """ return self._widget.writable class DriverTestWidget(QtGui.QWidget): """Widget that is automatically filled to control all Feats of a given driver. :param parent: parent widget. :param target: driver object to map. """ def __init__(self, parent, target): super().__init__(parent) self._lantz_target = target layout = QtGui.QVBoxLayout(self) label = QtGui.QLabel() label.setText(str(target)) layout.addWidget(label) recall = QtGui.QPushButton() recall.setText('Refresh') recall.clicked.connect(lambda x: target.refresh()) update = QtGui.QPushButton() update.setText('Update') update.clicked.connect(lambda x: target.update(self.widgets_values_as_dict())) auto = QtGui.QCheckBox() auto.setText('Update on change') auto.setChecked(True) auto.stateChanged.connect(self.update_on_change) hlayout = QtGui.QHBoxLayout() hlayout.addWidget(recall) hlayout.addWidget(update) hlayout.addWidget(auto) layout.addLayout(hlayout) self.writable_widgets = [] self.widgets = [] # Feat for feat_name, feat in sorted(target.feats.items()): try: feat_widget = LabeledFeatWidget(self, target, feat) self.widgets.append(feat_widget) if feat_widget.writable: self.writable_widgets.append(feat_widget) layout.addWidget(feat_widget) except Exception as ex: logger.debug('Could not create control for {}: {}'.format(feat_name, ex)) if __PRINT_TRACEBACK__: import traceback traceback.print_exc() # Actions line = QtGui.QFrame(self) #self.line.setGeometry(QtCore.QRect(110, 80, 351, 31)) line.setFrameShape(QtGui.QFrame.HLine) line.setFrameShadow(QtGui.QFrame.Sunken) layout.addWidget(line) actions_label = QtGui.QLabel(self) actions_label.setText('Actions:') actions_label.setFixedWidth(120) self.actions_combo = QtGui.QComboBox(self) self.actions_combo.addItems(list(target.actions.keys())) actions_button = QtGui.QPushButton(self) actions_button.setFixedWidth(60) actions_button.setText('Run') actions_button.clicked.connect(self.on_run_clicked) alayout = QtGui.QHBoxLayout() alayout.addWidget(actions_label) alayout.addWidget(self.actions_combo) alayout.addWidget(actions_button) layout.addLayout(alayout) @QtCore.Slot() def on_run_clicked(self): ArgumentsInputDialog.run(getattr(self._lantz_target, self.actions_combo.currentText()), self) def update_on_change(self, new_state): """Set the 'update_on_change' flag to new_state in each writable widget within this widget. If True, the driver will be updated after each change. """ for widget in self.writable_widgets: widget._widget._update_on_change = new_state def widgets_values_as_dict(self): """Return a dictionary mapping each writable feat name to the current value of the widget. """ return {widget._feat.name: widget._widget.value() for widget in self.writable_widgets} @property def lantz_target(self): """Driver connected to this widget. """ return self._lantz_target @lantz_target.setter def lantz_target(self, driver): self._lantz_target = driver for widget in self.widgets: widget.lantz_target = driver class SetupTestWidget(QtGui.QWidget): """Widget to control multiple drivers. :param parent: parent widget. :param targets: iterable of driver object to map. """ def __init__(self, parent, targets): super().__init__(parent) layout = QtGui.QHBoxLayout(self) tab_widget = QtGui.QTabWidget(self) tab_widget.setTabsClosable(False) for target in targets: widget = DriverTestWidget(parent, target) tab_widget.addTab(widget, target.name) layout.addWidget(tab_widget)
[docs]def connect_feat(widget, target, feat_name=None, feat_key=MISSING): """Connect a feature from a given driver to a widget. Calling this function also patches the widget is necessary. If applied two times with the same widget, it will connect to the target provided in the second call. This behaviour can be useful to change the connection target without rebuilding the whole UI. Alternative, after connect has been called the first time, widget will have a property `lantz_target` that can be used to achieve the same thing. :param widget: widget instance. :param target: driver instance. :param feat_name: feature name. If None, connect using widget name. :param feat_key: For a DictFeat, this defines which key to show. """ logger.debug('Connecting {} to {}, {}, {}'.format(widget, target, feat_name, feat_key)) if not isinstance(target, Driver): raise TypeError('Connect target must be an instance of lantz.Driver, not {}'.format(target)) if not feat_name: feat_name = widget.objectName() #: Reconnect if hasattr(widget, '_feat.name') and widget._feat.name == feat_name: widget.lantz_target = target return feat = target.feats[feat_name] WidgetMixin.wrap(widget) widget.bind_feat(feat) widget.feat_key = feat_key widget.lantz_target = target
[docs]def connect_driver(parent, target, *, prefix='', sep='__'): """Connect all children widgets to their corresponding lantz feature matching by name. Non-matching names are ignored. :param parent: parent widget. :param target: the driver. :param prefix: prefix to be prepended to the lantz feature (default = '') :param sep: separator between prefix, name and suffix """ logger.debug('Connecting {} to {}, {}, {}'.format(parent, target, prefix, sep)) ChildrenWidgets.patch(parent) if prefix: prefix += sep for name, _, wid in parent.widgets: if prefix and name.startswith(prefix): name = name[len(prefix):] if sep in name: name, _ = name.split(sep, 1) if name in target.feats: connect_feat(wid, target, name)
[docs]def connect_setup(parent, targets, *, prefix=None, sep='__'): """Connect all children widget to their corresponding :param parent: parent widget. :param targets: iterable of drivers. :param prefix: prefix to be prepended to the lantz feature name if None, the driver name will be used (default) if it is a dict, the driver name will be used to obtain he prefix. """ logger.debug('Connecting {} to {}, {}, {}'.format(parent, targets, prefix, sep)) ChildrenWidgets.patch(parent) for target in targets: name = target.name if isinstance(prefix, dict): name = prefix[name] connect_driver(parent, target, prefix=name, sep=sep)
[docs]class ChildrenWidgets(object): """Convenience class to iterate children. :param parent: parent widget. """ def __init__(self, parent): self.parent = parent def __getattr__(self, item): return self.parent.findChild((QtGui.QWidget, ), item) def __iter__(self): pending = [self.parent, ] qualname = {self.parent: self.parent.objectName()} while pending: obj = pending.pop() for child in obj.children(): if not isinstance(child, QtGui.QWidget): continue qualname[child] = qualname[obj] + '.' + child.objectName() pending.append(child) yield child.objectName(), qualname[child], child @classmethod def patch(cls, parent): if not hasattr(parent, 'widgets'): parent.widgets = cls(parent)
[docs]def request_new_units(current_units): """Ask for new units using a dialog box and return them. :param current_units: current units or magnitude. :type current_units: Quantity """ new_units = UnitInputDialog.get_units(current_units) if new_units is None: return None try: return Q_(1, new_units) except LookupError: # cannot parse units return None
@register_wrapper class MagnitudeMixin(WidgetMixin): _WRAPPED = (QtGui.QDoubleSpinBox, ) def keyPressEvent(self, event): super().keyPressEvent(event) if self._units and event.text() == 'u': self.change_units(request_new_units(self.value())) def bind_feat(self, feat): super().bind_feat(feat) #: self._units are the current units displayed by the widget. #: Respects units declared in the suffix if feat.units: suf = (self.suffix() if hasattr(self, 'suffix') else feat.units) or feat.units self._units = Q_(1, suf) self.change_units(self._units) else: self._units = None if feat.limits: self.change_limits(None) def change_units(self, new_units): """Update displayed suffix and stored units. """ if new_units is None: return try: rescaled = self.value().to(new_units) except ValueError: # incompatible units return None else: if hasattr(self, 'setSuffix'): self.setSuffix(' ' + str(new_units.units)) self.change_limits(new_units) self._units = new_units self.setValue(rescaled) def change_limits(self, new_units): """Change the limits (range) of the control taking the original values from the feat and scaling them to the new_units. """ if not hasattr(self, 'setRange'): return rng = self._feat.limits or (float('-inf'), float('+inf')) if new_units: conv = lambda ndx: Q_(rng[ndx], self._feat.units).to(new_units).magnitude else: conv = lambda ndx: rng[ndx] if len(rng) == 1: self.setRange(0, conv(0)) else: self.setRange(conv(0), conv(1)) if len(rng) == 3: self.setSingleStep(conv(2)) def value(self): """Get widget value and scale by units. """ if self._units: return super().value() * self._units return super().value() def setValue(self, value): """Set widget value scaled by units. """ if value is MISSING: font = QtGui.QFont() font.setItalic(True) self.setFont(font) elif isinstance(value, Q_): super().setValue(value.to(self._units).magnitude) else: super().setValue(value) @register_wrapper class SliderMixin(MagnitudeMixin): _WRAPPED = (QtGui.QSlider, QtGui.QDial, QtGui.QProgressBar, QtGui.QScrollBar) def setReadOnly(self, value): super().setEnabled(not value) @register_wrapper class LCDNumberMixin(MagnitudeMixin): _WRAPPED = (QtGui.QLCDNumber, ) @classmethod def _wrap(cls, widget): super()._wrap(widget) #TODO: Create a real valueChanged Signal. widget.valueChanged = widget.overflow def setReadOnly(self, value): super().setEnabled(not value) def setValue(self, value): if value is MISSING: font = QtGui.QFont() font.setItalic(True) self.setFont(font) return elif isinstance(value, Q_): super().display(value.to(self._units).magnitude) else: super().display(value) def value(self): return super().value() @register_wrapper class QComboBoxMixin(WidgetMixin): _WRAPPED = (QtGui.QComboBox, ) @classmethod def _wrap(cls, widget): super()._wrap(widget) widget.valueChanged = widget.currentIndexChanged def value(self): return self.currentText() def setValue(self, value): if value is MISSING: font = QtGui.QFont() font.setItalic(True) self.setFont(font) return self.setCurrentIndex(self.__values.index(value)) def setReadOnly(self, value): self.setEnabled(not value) def bind_feat(self, feat): super().bind_feat(feat) if isinstance(self._feat.values, dict): self.__values = list(self._feat.values.keys()) else: self.__values = list(self.__values) self.clear() self.addItems([str(value) for value in self.__values]) @register_wrapper class QCheckBoxMixin(WidgetMixin): _WRAPPED = (QtGui.QCheckBox, ) @classmethod def _wrap(cls, widget): super()._wrap(widget) widget.valueChanged = widget.stateChanged def setReadOnly(self, value): self.setCheckable(not value) def value(self): return self.isChecked() def setValue(self, value): if value is MISSING: return self.setChecked(value) @register_wrapper class QLineEditMixin(WidgetMixin): _WRAPPED = (QtGui.QLineEdit, ) @classmethod def _wrap(cls, widget): super()._wrap(widget) widget.valueChanged = widget.textChanged def value(self): return self.text() def setValue(self, value): if value is MISSING: return return self.setText(value) class ArgumentsInputDialog(QtGui.QDialog): def __init__(self, argspec, parent=None, window_title='Function arguments', doc=None): super().__init__(parent) vlayout = QtGui.QVBoxLayout(self) layout = QtGui.QFormLayout() widgets = [] defaults = argspec.defaults if argspec.defaults else () defaults = ('', ) * (len(argspec.args[1:]) - len(defaults)) + defaults self.arguments = {} for arg, default in zip(argspec.args[1:], defaults): wid = QtGui.QLineEdit(self) wid.setObjectName(arg) wid.setText(json.dumps(default)) self.arguments[arg] = default layout.addRow(arg, wid) widgets.append(wid) wid.textChanged.connect(self.on_widget_change(wid)) if doc and arg in doc: wid.setToolTip(doc[arg]) self.widgets = widgets buttonBox = QtGui.QDialogButtonBox() buttonBox.setOrientation(QtCore.Qt.Horizontal) buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Ok) buttonBox.setEnabled(True) buttonBox.accepted.connect(self.accept) vlayout.addLayout(layout) label = QtGui.QLabel() label.setText('Values are decoded from text using as JSON.') vlayout.addWidget(label) vlayout.addWidget(buttonBox) self.buttonBox = buttonBox self.valid = {wid.objectName(): True for wid in self.widgets} self.setWindowTitle(window_title) def on_widget_change(self, widget): name = widget.objectName() def validate(value): try: if value: value = json.loads(value) else: value = None palette = QtGui.QPalette() palette.setColor(widget.backgroundRole(), QtGui.QColor('white')) widget.setPalette(palette) self.arguments[name] = value self.valid[name] = True except: palette = QtGui.QPalette() palette.setColor(widget.backgroundRole(), QtGui.QColor(255, 102, 102)) widget.setPalette(palette) self.valid[name] = False self.buttonBox.setEnabled(all(self.valid.values())) return validate def accept(self): super().accept() @staticmethod def run(func, parent=None): """Creates and display a UnitInputDialog and return new units. Return None if the user cancelled. """ wrapped = getattr(func, '__wrapped__', func) name = wrapped.__name__ doc = wrapped.__doc__ argspec = inspect.getargspec(wrapped) arguments = {} if len(argspec.args) > 1: dialog = ArgumentsInputDialog(argspec, parent, window_title=name+ ' arguments', doc=_params_doc(doc)) if not dialog.exec_(): return None arguments = dialog.arguments try: func(**arguments) except Exception as e: logger.exception(e) QtGui.QMessageBox.critical(parent, 'Lantz', 'Instrument error while calling {}'.format(name), QtGui.QMessageBox.Ok, QtGui.QMessageBox.NoButton) class UnitInputDialog(QtGui.QDialog): """Dialog to select new units. Checks compatibility while typing and does not allow to continue if incompatible. Returns None if cancelled. :param units: current units. :param parent: parent widget. >>> new_units = UnitInputDialog.get_units('ms') """ def __init__(self, units, parent=None): super().__init__(parent) self.setupUi(parent) self.units = units self.source_units.setText(str(units)) def setupUi(self, parent): self.resize(275, 172) self.setWindowTitle('Convert units') self.layout = QtGui.QVBoxLayout(parent) self.layout.setSizeConstraint(QtGui.QLayout.SetFixedSize) align = (QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter) self.layout1 = QtGui.QHBoxLayout() self.label1 = QtGui.QLabel() self.label1.setMinimumSize(QtCore.QSize(100, 0)) self.label1.setText('Convert from:') self.label1.setAlignment(align) self.layout1.addWidget(self.label1) self.source_units = QtGui.QLineEdit() self.source_units.setReadOnly(True) self.layout1.addWidget(self.source_units) self.layout.addLayout(self.layout1) self.layout2 = QtGui.QHBoxLayout() self.label2 = QtGui.QLabel() self.label2.setMinimumSize(QtCore.QSize(100, 0)) self.label2.setText('to:') self.label2.setAlignment(align) self.layout2.addWidget(self.label2) self.destination_units = QtGui.QLineEdit() self.layout2.addWidget(self.destination_units) self.layout.addLayout(self.layout2) self.message = QtGui.QLabel() self.message.setText('') self.message.setAlignment(QtCore.Qt.AlignCenter) self.layout.addWidget(self.message) self.buttonBox = QtGui.QDialogButtonBox() self.buttonBox.setOrientation(QtCore.Qt.Horizontal) self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Ok) self.layout.addWidget(self.buttonBox) self.buttonBox.setEnabled(False) self.buttonBox.accepted.connect(self.accept) self.destination_units.textChanged.connect(self.check) self.setLayout(self.layout) self.destination_units.setFocus() def check(self): units = self.destination_units.text().strip() if not units: return try: new_units = Q_(1, units) factor = self.units.to(new_units).magnitude except LookupError or SyntaxError: self.message.setText('Cannot parse units') self.buttonBox.setEnabled(False) except ValueError: self.message.setText('Incompatible units') self.buttonBox.setEnabled(False) else: self.message.setText('factor {:f}'.format(factor)) self.buttonBox.setEnabled(True) @staticmethod def get_units(units): """Creates and display a UnitInputDialog and return new units. Return None if the user cancelled. """ dialog = UnitInputDialog(Q_(1, units.units)) if dialog.exec_(): return dialog.destination_units.text() return None class InitializerHelper(QtCore.QObject): initializing = QtCore.Signal(object) initialized = QtCore.Signal(object) exception = QtCore.Signal(object, object) finished = QtCore.Signal(float) def __init__(self, drivers, register_finalizer, parallel, dependencies): super().__init__() self.drivers = drivers self.register_finalizer = register_finalizer self.parallel = parallel self.dependencies = dependencies def process(self): start = time.time() initialize_many(drivers=self.drivers, register_finalizer=self.register_finalizer, on_initializing=self.on_initializing, on_initialized=self.on_initialized, on_exception=self.on_exception, concurrent=self.parallel, dependencies=self.dependencies) self.finished.emit(time.time() - start) def on_initializing(self, driver): self.initializing.emit(driver) def on_initialized(self, driver): self.initialized.emit(driver) def on_exception(self, driver, ex): self.exception.emit(driver, ex)
[docs]def initialize_and_report(widget, drivers, register_finalizer=True, initializing_msg='Initializing ...', initialized_msg='Initialized', concurrent=True, dependencies=None): """Initialize drivers while reporting the status in a QtWidget. :param widget: Qt Widget where the status information is going to be shown. :param drivers: iterable of drivers to initialize. :param register_finalizer: register driver.finalize method to be called at python exit. :param initializing_msg: message to be displayed while initializing. :param initialized_msg: message to be displayed after successful initialization. :param concurrent: indicates that drivers with satisfied dependencies should be initialized concurrently. :param dependencies: indicates which drivers depend on others to be initialized. each key is a driver name, and the corresponding value is an iterable with its dependencies. :return: the QThread doing the initialization. """ timing = {} thread = QtCore.QThread() helper = InitializerHelper(drivers, register_finalizer, concurrent, dependencies) helper.moveToThread(thread) thread.helper = helper if isinstance(widget, QtGui.QTableWidget): def _initializing(driver): timing[driver] = time.time() row = drivers.index(driver) widget.setItem(row, 2, QtGui.QTableWidgetItem(initializing_msg)) def _initialized(driver): delta = time.time() - timing[driver] row = drivers.index(driver) widget.setItem(row, 2, QtGui.QTableWidgetItem(initialized_msg + ' ({:.1f} sec)'.format(delta))) def _exception(driver, e): delta = time.time() - timing[driver] row = drivers.index(driver) widget.setItem(row, 2, QtGui.QTableWidgetItem('{} ({:.1f} sec)'.format(e, delta))) def _done(duration): widget.setItem(len(drivers), 2, QtGui.QTableWidgetItem('{:.1f} sec'.format(duration))) thread.quit() widget.clearContents() widget.setRowCount(len(drivers) + 1) for row, driver in enumerate(drivers): widget.setItem(row, 0, QtGui.QTableWidgetItem(driver.name)) widget.setItem(row, 1, QtGui.QTableWidgetItem(driver.__class__.__name__)) widget.setItem(row, 2, QtGui.QTableWidgetItem('')) widget.resizeColumnToContents(0) widget.horizontalHeader().setStretchLastSection(True) elif isinstance(widget, QtGui.QLineEdit): def _initializing(driver): timing[driver] = time.time() widget.setText('{} ({}) > {}'.format(driver.name, driver.__class__.__name__, initializing_msg)) def _initialized(driver): delta = time.time() - timing[driver] widget.setText('{} ({}) > {} ({:.1f} sec)'.format(driver.name, driver.__class__.__name__, initialized_msg, delta)) def _exception(driver, e): delta = time.time() - timing[driver] widget.setText('{} ({}) > {} ({:.1f} sec)'.format(driver.name, driver.__class__.__name__, e, delta)) def _done(duration): widget.setText('Initialized in {:.1f} sec'.format(duration)) thread.quit() widget.setReadOnly(True) elif isinstance(widget, QtGui.QTextEdit): def _initializing(driver): timing[driver] = time.time() widget.append('{} ({}) > {}'.format(driver.name, driver.__class__.__name__, initializing_msg)) def _initialized(driver): delta = time.time() - timing[driver] widget.append('{} ({}) > {} ({:.1f} sec)'.format(driver.name, driver.__class__.__name__, initialized_msg, delta)) def _exception(driver, e): delta = time.time() - timing[driver] widget.append('{} ({}) > {} ({:.1f} sec)'.format(driver.name, driver.__class__.__name__, e, delta)) def _done(duration): widget.append('Initialized in {:.1f} sec'.format(duration)) thread.quit() widget.setReadOnly(True) else: raise TypeError('Unknown widget type {}.'.format(type(widget))) thread.started.connect(helper.process) helper.initializing.connect(_initializing) helper.initialized.connect(_initialized) helper.exception.connect(_exception) helper.finished.connect(_done) thread.start() return thread