Ejemplo n.º 1
0
class PyDMEnumButton(QWidget, PyDMWritableWidget, WidgetType):
    """
    A QWidget that renders buttons for every option of Enum Items.
    For now three types of buttons can be rendered:
    - Push Button
    - Radio Button

    Parameters
    ----------
    parent : QWidget
        The parent widget for the Label
    init_channel : str, optional
        The channel to be used by the widget.

    Signals
    -------
    send_value_signal : int, float, str, bool or np.ndarray
        Emitted when the user changes the value.
    """
    Q_ENUMS(WidgetType)
    WidgetType = WidgetType

    def __init__(self, parent=None, init_channel=None):
        QWidget.__init__(self, parent)
        PyDMWritableWidget.__init__(self, init_channel=init_channel)
        self._has_enums = False
        self._checkable = True
        self.setLayout(QGridLayout(self))
        self._btn_group = QButtonGroup()
        self._btn_group.setExclusive(True)
        self._btn_group.buttonClicked[int].connect(self.handle_button_clicked)
        self._widget_type = WidgetType.PushButton
        self._orientation = Qt.Vertical
        self._widgets = []
        self.rebuild_widgets()

    def minimumSizeHint(self):
        """
        This property holds the recommended minimum size for the widget.

        Returns
        -------
        QSize
        """
        # This is totally arbitrary, I just want *some* visible nonzero size
        return QSize(50, 100)

    @Property("QStringList")
    def items(self):
        """
        Items to be displayed in the button group.

        This property can be overridden by the items coming from the control system.
        Because C++ QStringList expects a list type, we need to make sure that None is never returned.

        Returns
        -------
        List[str]
        """
        return self.enum_strings or []

    @items.setter
    def items(self, value):
        self.enum_strings_changed(value)

    @Property(WidgetType)
    def widgetType(self):
        """
        The widget type to be used when composing the group.

        Returns
        -------
        WidgetType
        """
        return self._widget_type

    @widgetType.setter
    def widgetType(self, new_type):
        """
        The widget type to be used when composing the group.

        Parameters
        ----------
        new_type : WidgetType
        """
        if new_type != self._widget_type:
            self._widget_type = new_type
            self.rebuild_widgets()

    @Property(Qt.Orientation)
    def orientation(self):
        """
        Whether to lay out the bit indicators vertically or horizontally.

        Returns
        -------
        int
        """
        return self._orientation

    @orientation.setter
    def orientation(self, new_orientation):
        """
        Whether to lay out the bit indicators vertically or horizontally.

        Parameters
        -------
        new_orientation : Qt.Orientation, int
        """
        if new_orientation != self._orientation:
            self._orientation = new_orientation
            self.rebuild_layout()

    @Property(bool)
    def checkable(self):
        """
        Whether or not the button should be checkable.

        Returns
        -------
        bool
        """
        return self._checkable

    @checkable.setter
    def checkable(self, value):
        if value != self._checkable:
            self._checkable = value
            for widget in self._widgets:
                widget.setCheckable(value)

    @Slot(int)
    def handle_button_clicked(self, id):
        """
        Handles the event of a button being clicked.

        Parameters
        ----------
        id : int
            The clicked button id.
        """
        self.send_value_signal.emit(id)

    def clear(self):
        """
        Remove all inner widgets from the layout
        """
        for col in range(0, self.layout().columnCount()):
            for row in range(0, self.layout().rowCount()):
                item = self.layout().itemAtPosition(row, col)
                if item is not None:
                    w = item.widget()
                    if w is not None:
                        self.layout().removeWidget(w)

    def rebuild_widgets(self):
        """
        Rebuild the list of widgets based on a new enum or generates a default
        list of fake strings so we can see something at Designer.
        """

        def generate_widgets(items):
            while len(self._widgets) != 0:
                w = self._widgets.pop(0)
                self._btn_group.removeButton(w)
                w.deleteLater()

            for idx, entry in enumerate(items):
                w = class_for_type[self._widget_type](parent=self)
                w.setCheckable(self.checkable)
                w.setText(entry)
                self._widgets.append(w)
                self._btn_group.addButton(w, idx)

        self.clear()
        if self._has_enums:
            generate_widgets(self.enum_strings)
        else:
            generate_widgets(["Item 1", "Item 2", "Item ..."])

        self.rebuild_layout()

    def rebuild_layout(self):
        """
        Method to reorganize the top-level widget and its contents
        according to the layout property values.
        """
        self.clear()
        if self.orientation == Qt.Vertical:
            for i, widget in enumerate(self._widgets):
                self.layout().addWidget(widget, i, 0)
        elif self.orientation == Qt.Horizontal:
            for i, widget in enumerate(self._widgets):
                self.layout().addWidget(widget, 0, i)

    def check_enable_state(self):
        """
        Checks whether or not the widget should be disable.
        This method also disables the widget and add a Tool Tip
        with the reason why it is disabled.

        """
        status = self._write_access and self._connected and self._has_enums
        tooltip = ""
        if not self._connected:
            tooltip += "Channel is disconnected."
        elif not self._write_access:
            if data_plugins.is_read_only():
                tooltip += "Running PyDM on Read-Only mode."
            else:
                tooltip += "Access denied by Channel Access Security."
        elif not self._has_enums:
            tooltip += "Enums not available."

        self.setToolTip(tooltip)
        self.setEnabled(status)

    def value_changed(self, new_val):
        """
        Callback invoked when the Channel value is changed.

        Parameters
        ----------
        new_val : int
            The new value from the channel.
        """
        if new_val is not None and new_val != self.value:
            super(PyDMEnumButton, self).value_changed(new_val)
            btn = self._btn_group.button(new_val)
            if btn:
                btn.setChecked(True)

    def enum_strings_changed(self, new_enum_strings):
        """
        Callback invoked when the Channel has new enum values.
        This callback also triggers a value_changed call so the
        new enum values to be broadcasted.

        Parameters
        ----------
        new_enum_strings : tuple
            The new list of values
        """
        if new_enum_strings is not None \
                and new_enum_strings != self.enum_strings:
            super(PyDMEnumButton, self).enum_strings_changed(new_enum_strings)
            self._has_enums = True
            self.check_enable_state()
            self.rebuild_widgets()

    def paintEvent(self, _):
        """
        Paint events are sent to widgets that need to update themselves,
        for instance when part of a widget is exposed because a covering
        widget was moved.

        At PyDMDrawing this method handles the alarm painting with parameters
        from the stylesheet, configures the brush, pen and calls ```draw_item```
        so the specifics can be performed for each of the drawing classes.

        Parameters
        ----------
        event : QPaintEvent
        """
        painter = QPainter(self)
        opt = QStyleOption()
        opt.initFrom(self)
        self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self)
        painter.setRenderHint(QPainter.Antialiasing)
Ejemplo n.º 2
0
class HalLabel(QLabel, HALWidget, HalType):
    """HAL Label"""

    Q_ENUMS(HalType)

    def __init__(self, parent=None):
        super(HalLabel, self).__init__(parent)

        self._in_pin = None
        self._enable_pin = None

        self._typ = "float"
        self._fmt = "%s"

        self.setValue(0)

    def setValue(self, value):
        self.setText(self._fmt % value)

    @Property(str)
    def textFormat(self):
        """Text Format Property

        Args:
            fmt (str) : A valid python style format string. Defaults to ``%s``.
        """
        return self._fmt

    @textFormat.setter
    def textFormat(self, fmt):
        self._fmt = fmt
        try:
            val = {'bit': False, 'u32': 0, 's32': 0, 'float': 0.0}[self._typ]
            self.setValue(val)
        except:
            pass

    @Property(HalType)
    def pinType(self):
        return getattr(HalType, self._typ)

    @pinType.setter
    def pinType(self, typ_enum):
        self._typ = HalType.toString(typ_enum)
        try:
            val = {'bit': False, 'u32': 0, 's32': 0, 'float': 0.0}[self._typ]
            self.setValue(val)
        except:
            pass

    def initialize(self):
        comp = hal.COMPONENTS['qtpyvcp']
        obj_name = str(self.objectName()).replace('_', '-')

        # add label.enable HAL pin
        self._enable_pin = comp.addPin(obj_name + ".enable", "bit", "in")
        self._enable_pin.value = self.isEnabled()
        self._enable_pin.valueChanged.connect(self.setEnabled)

        # add label.in HAL pin
        self._in_pin = comp.addPin(obj_name + ".in", self._typ, "in")
        self.setValue(self._in_pin.value)
        self._in_pin.valueChanged.connect(self.setValue)
Ejemplo n.º 3
0
class TyphosDeviceDisplay(utils.TyphosBase, widgets.TyphosDesignerMixin,
                          _DisplayTypes):
    """
    Main display for a single ophyd Device.

    This contains the widgets for all of the root devices signals, and any
    methods you would like to display. By typhos convention, the base
    initialization sets up the widgets and the :meth:`.from_device` class
    method will automatically populate the resulting display.

    Parameters
    ----------
    parent : QWidget, optional
        The parent widget.

    scrollable : bool, optional
        If ``True``, put the loaded template into a :class:`QScrollArea`.
        Otherwise, the display widget will go directly in this widget's layout.

    composite_heuristics : bool, optional
        Enable composite heuristics, which may change the suggested detailed
        screen based on the contents of the added device.  See also
        :meth:`.suggest_composite_screen`.

    embedded_templates : list, optional
        List of embedded templates to use in addition to those found on disk.

    detailed_templates : list, optional
        List of detailed templates to use in addition to those found on disk.

    engineering_templates : list, optional
        List of engineering templates to use in addition to those found on
        disk.

    display_type : DisplayTypes, optional
        The default display type.

    nested : bool, optional
        An optional annotation for a display that may be nested inside another.
    """

    # Template types and defaults
    Q_ENUMS(_DisplayTypes)
    TemplateEnum = DisplayTypes  # For convenience

    device_count_threshold = 0
    signal_count_threshold = 30

    def __init__(self,
                 parent=None,
                 *,
                 scrollable=True,
                 composite_heuristics=True,
                 embedded_templates=None,
                 detailed_templates=None,
                 engineering_templates=None,
                 display_type='detailed_screen',
                 nested=False):

        self._composite_heuristics = composite_heuristics
        self._current_template = None
        self._forced_template = ''
        self._macros = {}
        self._display_widget = None
        self._scrollable = False
        self._searched = False
        self._hide_empty = False
        self._nested = nested

        self.templates = {name: [] for name in DisplayTypes.names}
        self._display_type = normalize_display_type(display_type)

        instance_templates = {
            'embedded_screen': embedded_templates or [],
            'detailed_screen': detailed_templates or [],
            'engineering_screen': engineering_templates or [],
        }
        for view, path_list in instance_templates.items():
            paths = [pathlib.Path(p).expanduser().resolve() for p in path_list]
            self.templates[view].extend(paths)

        self._scroll_area = QtWidgets.QScrollArea()
        self._scroll_area.setAlignment(Qt.AlignTop)
        self._scroll_area.setObjectName('scroll_area')
        self._scroll_area.setFrameShape(QtWidgets.QFrame.StyledPanel)
        self._scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self._scroll_area.setWidgetResizable(True)
        self._scroll_area.setFrameStyle(QtWidgets.QFrame.NoFrame)

        super().__init__(parent=parent)

        layout = QtWidgets.QHBoxLayout()
        self.setLayout(layout)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self._scroll_area)

        self.scrollable = scrollable

    @Property(bool)
    def composite_heuristics(self):
        """Allow composite screen to be suggested first by heuristics."""
        return self._composite_heuristics

    @composite_heuristics.setter
    def composite_heuristics(self, composite_heuristics):
        self._composite_heuristics = bool(composite_heuristics)

    @Property(bool)
    def scrollable(self):
        """Place the display in a scrollable area."""
        return self._scrollable

    @scrollable.setter
    def scrollable(self, scrollable):
        # Switch between using the scroll area layout or
        if scrollable == self._scrollable:
            return

        self._scrollable = bool(scrollable)
        self._move_display_to_layout(self._display_widget)

    @Property(bool)
    def hideEmpty(self):
        """Toggle hiding or showing empty panels."""
        return self._hide_empty

    @hideEmpty.setter
    def hideEmpty(self, checked):
        if checked != self._hide_empty:
            self._hide_empty = checked

    def _move_display_to_layout(self, widget):
        if not widget:
            return

        widget.setParent(None)
        if self._scrollable:
            self._scroll_area.setWidget(widget)
        else:
            self.layout().addWidget(widget)

        self._scroll_area.setVisible(self._scrollable)

    def _generate_template_menu(self, base_menu):
        """Generate the template switcher menu, adding it to ``base_menu``."""
        for view, filenames in self.templates.items():
            if view.endswith('_screen'):
                view = view.split('_screen')[0]
            menu = base_menu.addMenu(view.capitalize())

            for filename in filenames:

                def switch_template(*, filename=filename):
                    self.force_template = filename

                action = menu.addAction(os.path.split(filename)[-1])
                action.triggered.connect(switch_template)

        refresh_action = base_menu.addAction("Refresh Templates")
        refresh_action.triggered.connect(self._refresh_templates)

    def _refresh_templates(self):
        """Context menu 'Refresh Templates' clicked."""
        # Force an update of the display cache.
        cache.get_global_display_path_cache().update()
        self.search_for_templates()
        self.load_best_template()

    @property
    def current_template(self):
        """Get the current template being displayed."""
        return self._current_template

    @Property(_DisplayTypes)
    def display_type(self):
        """Get or set the current display type."""
        return self._display_type

    @display_type.setter
    def display_type(self, value):
        value = normalize_display_type(value)
        if self._display_type != value:
            self._display_type = value
            self.load_best_template()

    @property
    def macros(self):
        """Get or set the macros for the display."""
        return dict(self._macros)

    @macros.setter
    def macros(self, macros):
        self._macros.clear()
        self._macros.update(**(macros or {}))

        # If any display macros are specified, re-search for templates:
        if any(view in self._macros for view in DisplayTypes.names):
            self.search_for_templates()

    @Property(str, designable=False)
    def device_class(self):
        """Get the full class with module name of loaded device."""
        device = self.device
        cls = self.device.__class__
        return f'{cls.__module__}.{cls.__name__}' if device else ''

    @Property(str, designable=False)
    def device_name(self):
        """Get the name of the loaded device."""
        device = self.device
        return device.name if device else ''

    @property
    def device(self):
        """Get the device associated with this Device Display."""
        try:
            device, = self.devices
            return device
        except ValueError:
            ...

    def get_best_template(self, display_type, macros):
        """
        Get the best template for the given display type.

        Parameters
        ----------
        display_type : DisplayTypes, str, or int
            The display type.

        macros : dict
            Macros to use when loading the template.
        """
        display_type = normalize_display_type(display_type).name

        templates = self.templates[display_type]
        if templates:
            return templates[0]

        logger.warning("No templates available for display type: %s",
                       self._display_type)

    def _remove_display(self):
        """Remove the display widget, readying for a new template."""
        display_widget = self._display_widget
        if display_widget:
            if self._scroll_area.widget():
                self._scroll_area.takeWidget()
            self.layout().removeWidget(display_widget)
            display_widget.deleteLater()

        self._display_widget = None

    def load_best_template(self):
        """Load the best available template for the current display type."""
        if self.layout() is None:
            # If we are not fully initialized yet do not try and add anything
            # to the layout. This will happen if the QApplication has a
            # stylesheet that forces a template prior to the creation of this
            # display
            return

        if not self._searched:
            self.search_for_templates()

        self._remove_display()

        template = (self._forced_template
                    or self.get_best_template(self._display_type, self.macros))

        if not template:
            widget = QtWidgets.QWidget()
            template = None
        else:
            try:
                widget = self._load_template(template)
            except Exception as ex:
                logger.exception("Unable to load file %r", template)
                # If we have a previously defined template
                if self._current_template is not None:
                    # Fallback to it so users have a choice
                    widget = self._load_template(self._current_template)
                    pydm.exception.raise_to_operator(ex)
                else:
                    widget = QtWidgets.QWidget()
                    template = None

        if widget:
            widget.setObjectName('display_widget')

            if widget.layout() is None and widget.minimumSize().width() == 0:
                # If the widget has no layout, use a fixed size for it.
                # Without this, the widget may not display at all.
                widget.setMinimumSize(widget.size())

        self._display_widget = widget
        self._current_template = template

        def size_hint(*args, **kwargs):
            return widget.size()

        # sizeHint is not defined so we suggest the widget size
        widget.sizeHint = size_hint

        # We should _move_display_to_layout as soon as it is created. This
        # allow us to speed up since if the widget is too complex it takes
        # seconds to set it to the QScrollArea
        self._move_display_to_layout(self._display_widget)

        self._update_children()
        utils.reload_widget_stylesheet(self)

    @property
    def display_widget(self):
        """Get the widget generated from the template."""
        return self._display_widget

    @staticmethod
    def _get_templates_from_macros(macros):
        ret = {}
        paths = cache.get_global_display_path_cache().paths
        for display_type in DisplayTypes.names:
            ret[display_type] = None
            try:
                value = macros[display_type]
            except KeyError:
                ...
            else:
                if not value:
                    continue
                try:
                    value = pathlib.Path(value)
                except ValueError as ex:
                    logger.debug('Invalid path specified in macro: %s=%s',
                                 display_type,
                                 value,
                                 exc_info=ex)
                else:
                    ret[display_type] = list(
                        utils.find_file_in_paths(value, paths=paths))

        return ret

    def _load_template(self, filename):
        """Load template from file and return the widget."""
        loader = (pydm.display.load_py_file
                  if filename.suffix == '.py' else pydm.display.load_ui_file)

        logger.debug('Load template using %s: %r', loader.__name__, filename)
        return loader(str(filename), macros=self._macros)

    def _update_children(self):
        """Notify child widgets of this device display + the device."""
        device = self.device
        display = self._display_widget
        designer = display.findChildren(widgets.TyphosDesignerMixin) or []
        bases = display.findChildren(utils.TyphosBase) or []

        for widget in set(bases + designer):
            if device and hasattr(widget, 'add_device'):
                widget.add_device(device)

            if hasattr(widget, 'set_device_display'):
                widget.set_device_display(self)

    @Property(str)
    def force_template(self):
        """Force a specific template."""
        return self._forced_template

    @force_template.setter
    def force_template(self, value):
        if value != self._forced_template:
            self._forced_template = value
            self.load_best_template()

    @staticmethod
    def _build_macros_from_device(device, macros=None):
        result = {}
        if hasattr(device, 'md'):
            if isinstance(device.md, dict):
                result = dict(device.md)
            else:
                result = dict(device.md.post())

        if 'name' not in result:
            result['name'] = device.name
        if 'prefix' not in result and hasattr(device, 'prefix'):
            result['prefix'] = device.prefix

        result.update(**(macros or {}))
        return result

    def add_device(self, device, macros=None):
        """
        Add a Device and signals to the TyphosDeviceDisplay.

        The full dictionary of macros is built with the following order of
        precedence::

           1. Macros from the device metadata itself.
           2. If available, `name`, and `prefix` will be added from the device.
           3. The argument ``macros`` is then used to fill/update the final
              macro dictionary.

        Parameters
        ----------
        device : ophyd.Device
            The device to add.

        macros : dict, optional
            Additional macros to use/replace the defaults.
        """
        # We only allow one device at a time
        if self.devices:
            logger.debug("Removing devices %r", self.devices)
            self.devices.clear()
        # Add the device to the cache
        super().add_device(device)
        self._searched = False
        self.macros = self._build_macros_from_device(device, macros=macros)
        self.load_best_template()

    def search_for_templates(self):
        """Search the filesystem for device-specific templates."""
        device = self.device
        if not device:
            logger.debug('Cannot search for templates without device')
            return

        self._searched = True
        cls = device.__class__

        logger.debug('Searching for templates for %s', cls.__name__)
        macro_templates = self._get_templates_from_macros(self._macros)

        paths = cache.get_global_display_path_cache().paths
        for display_type in DisplayTypes.names:
            view = display_type
            if view.endswith('_screen'):
                view = view.split('_screen')[0]

            template_list = self.templates[display_type]
            template_list.clear()

            # 1. Highest priority: macros
            for template in set(macro_templates[display_type] or []):
                template_list.append(template)
                logger.debug('Adding macro template %s: %s (total=%d)',
                             display_type, template, len(template_list))

            # 2. Composite heuristics, if enabled
            if self._composite_heuristics and view == 'detailed':
                if self.suggest_composite_screen(cls):
                    template_list.append(DETAILED_TREE_TEMPLATE)

            # 3. Templates based on class hierarchy names
            filenames = utils.find_templates_for_class(cls, view, paths)
            for filename in filenames:
                if filename not in template_list:
                    template_list.append(filename)
                    logger.debug('Found new template %s: %s (total=%d)',
                                 display_type, filename, len(template_list))

            # 4. Default templates
            template_list.extend([
                templ for templ in DEFAULT_TEMPLATES[display_type]
                if templ not in template_list
            ])

    @classmethod
    def suggest_composite_screen(cls, device_cls):
        """
        Suggest to use the composite screen for the given class.

        Returns
        -------
        composite : bool
            If True, favor the composite screen.
        """
        num_devices = 0
        num_signals = 0
        for attr, component in utils._get_top_level_components(device_cls):
            num_devices += issubclass(component.cls, ophyd.Device)
            num_signals += issubclass(component.cls, ophyd.Signal)

        specific_screens = cls._get_specific_screens(device_cls)
        if (len(specific_screens)
                or (num_devices <= cls.device_count_threshold
                    and num_signals >= cls.signal_count_threshold)):
            # 1. There's a custom screen - we probably should use them
            # 2. There aren't many devices, so the composite display isn't
            #    useful
            # 3. There are many signals, which should be broken up somehow
            composite = False
        else:
            # 1. No custom screen, or
            # 2. Many devices or a relatively small number of signals
            composite = True

        logger.debug(
            '%s screens=%s num_signals=%d num_devices=%d -> composite=%s',
            device_cls, specific_screens, num_signals, num_devices, composite)
        return composite

    @classmethod
    def from_device(cls, device, template=None, macros=None, **kwargs):
        """
        Create a new TyphosDeviceDisplay from a Device.

        Loads the signals in to the appropriate positions and sets the title to
        a cleaned version of the device name

        Parameters
        ----------
        device : ophyd.Device

        template : str, optional
            Set the ``display_template``.

        macros : dict, optional
            Macro substitutions to be placed in template.

        **kwargs
            Passed to the class init.
        """
        display = cls(**kwargs)
        # Reset the template if provided
        if template:
            display.force_template = template
        # Add the device
        display.add_device(device, macros=macros)
        return display

    @classmethod
    def from_class(cls, klass, *, template=None, macros=None, **kwargs):
        """
        Create a new TyphosDeviceDisplay from a Device class.

        Loads the signals in to the appropriate positions and sets the title to
        a cleaned version of the device name.

        Parameters
        ----------
        klass : str or class

        template : str, optional
            Set the ``display_template``.

        macros : dict, optional
            Macro substitutions to be placed in template.

        **kwargs
            Extra arguments are used at device instantiation.

        Returns
        -------
        TyphosDeviceDisplay
        """
        try:
            obj = pcdsutils.utils.get_instance_by_name(klass, **kwargs)
        except Exception:
            logger.exception(
                'Failed to generate TyphosDeviceDisplay from '
                'device %s', obj)
            return None

        return cls.from_device(obj, template=template, macros=macros)

    @classmethod
    def _get_specific_screens(cls, device_cls):
        """
        Get the list of specific screens for a given device class.

        That is, screens that are not default Typhos-provided screens.
        """
        return [
            template for template in utils.find_templates_for_class(
                device_cls, 'detailed', utils.DISPLAY_PATHS)
            if not utils.is_standard_template(template)
        ]

    def to_image(self):
        """
        Return the entire display as a QtGui.QImage.

        Returns
        -------
        QtGui.QImage
            The display, as an image.
        """
        if self._display_widget is not None:
            return utils.widget_to_image(self._display_widget)

    @Slot()
    def copy_to_clipboard(self):
        """Copy the display image to the clipboard."""
        image = self.to_image()
        if image is not None:
            clipboard = QtGui.QGuiApplication.clipboard()
            clipboard.setImage(image)

    @Slot(object)
    def _tx(self, value):
        """Receive information from happi channel."""
        self.add_device(value['obj'], macros=value['md'])

    def __repr__(self):
        """Get a custom representation for TyphosDeviceDisplay."""
        return (f'<{self.__class__.__name__} at {hex(id(self))} '
                f'device={self.device_class}[{self.device_name!r}] '
                f'nested={self._nested}'
                f'>')
Ejemplo n.º 4
0
class PyDMLabel(QLabel, TextFormatter, PyDMWidget, DisplayFormat):
    Q_ENUMS(DisplayFormat)
    DisplayFormat = DisplayFormat
    """
    A QLabel with support for Channels and more from PyDM

    Parameters
    ----------
    parent : QWidget
        The parent widget for the Label
    init_channel : str, optional
        The channel to be used by the widget.
    """
    def __init__(self, parent=None, init_channel=None):
        QLabel.__init__(self, parent)
        PyDMWidget.__init__(self, init_channel=init_channel)
        self.app = QApplication.instance()
        self.setTextFormat(Qt.PlainText)
        self.setTextInteractionFlags(Qt.NoTextInteraction)
        self.setText("PyDMLabel")
        self._display_format_type = self.DisplayFormat.Default
        self._string_encoding = "utf_8"
        if is_pydm_app():
            self._string_encoding = self.app.get_string_encoding()

    @Property(DisplayFormat)
    def displayFormat(self):
        return self._display_format_type

    @displayFormat.setter
    def displayFormat(self, new_type):
        if self._display_format_type != new_type:
            self._display_format_type = new_type
            # Trigger the update of display format
            self.value_changed(self.value)

    def value_changed(self, new_value):
        """
        Callback invoked when the Channel value is changed.
        Sets the value of new_value accordingly at the Label.

        Parameters
        ----------
        new_value : str, int, float, bool or np.ndarray
            The new value from the channel. The type depends on the channel.
        """
        super(PyDMLabel, self).value_changed(new_value)
        new_value = parse_value_for_display(
            value=new_value,
            precision=self._prec,
            display_format_type=self._display_format_type,
            string_encoding=self._string_encoding,
            widget=self)
        # If the value is a string, just display it as-is, no formatting
        # needed.
        if isinstance(new_value, str):
            self.setText(new_value)
            return
        # If the value is an enum, display the appropriate enum string for
        # the value.
        if self.enum_strings is not None and isinstance(new_value, int):
            try:
                self.setText(self.enum_strings[new_value])
            except IndexError:
                self.setText("**INVALID**")
            return
        # If the value is a number (float or int), display it using a
        # format string if necessary.
        if isinstance(new_value, (int, float)):
            self.setText(self.format_string.format(new_value))
            return
        # If you made it this far, just turn whatever the heck the value
        # is into a string and display it.
        self.setText(str(new_value))
Ejemplo n.º 5
0
class TyphonSignalPanel(TyphonBase, TyphonDesignerMixin, SignalOrder):
    """
    Panel of Signals for Device
    """
    Q_ENUMS(SignalOrder)  # Necessary for display in Designer
    SignalOrder = SignalOrder  # For convenience
    # From top of page to bottom
    kind_order = (Kind.hinted, Kind.normal, Kind.config, Kind.omitted)

    def __init__(self, parent=None, init_channel=None):
        super().__init__(parent=parent)
        # Create a SignalPanel layout to be modified later
        self.setLayout(SignalPanel())
        # Add default Kind values
        self._kinds = dict.fromkeys([kind.name for kind in Kind], True)
        self._signal_order = SignalOrder.byKind

    def _get_kind(self, kind):
        return self._kinds[kind]

    def _set_kind(self, value, kind):
        # If we have a new value store it
        if value != self._kinds[kind]:
            # Store it internally
            self._kinds[kind] = value
            # Remodify the layout for the new Kind
            self._set_layout()

    # Kind Configuration pyqtProperty
    showHints = Property(bool, partial(_get_kind, kind='hinted'),
                         partial(_set_kind, kind='hinted'))
    showNormal = Property(bool, partial(_get_kind, kind='normal'),
                          partial(_set_kind, kind='normal'))
    showConfig = Property(bool, partial(_get_kind, kind='config'),
                          partial(_set_kind, kind='config'))
    showOmitted = Property(bool, partial(_get_kind, kind='omitted'),
                           partial(_set_kind, kind='omitted'))

    @Property(SignalOrder)
    def sortBy(self):
        """Order signals will be placed in layout"""
        return self._signal_order

    @sortBy.setter
    def sortBy(self, value):
        if value != self._signal_order:
            self._signal_order = value
            self._set_layout()

    def add_device(self, device):
        """Add a device to the widget"""
        # Only allow a single device
        self.devices.clear()
        # Add the new device
        super().add_device(device)
        # Configure the layout for the new device
        self._set_layout()

    def _set_layout(self):
        """Set the layout based on the current device and kind"""
        # We can't set a layout if we don't have any devices
        if not self.devices:
            return
        # Clear our layout
        self.layout().clear()
        shown_kind = [kind for kind in Kind if self._kinds[kind.name]]
        # Iterate through kinds
        signals = list()
        for kind in Kind:
            if kind in shown_kind:
                try:
                    for (attr, signal) in grab_kind(self.devices[0],
                                                    kind.name):
                        label = clean_attr(attr)
                        # Check twice for Kind as signal might have multiple
                        # kinds
                        if signal.kind in shown_kind:
                            signals.append((label, signal))
                except Exception:
                    logger.exception("Unable to add %s signals from %r",
                                     kind.name, self.devices[0])
        # Pick our sorting function
        if self._signal_order == SignalOrder.byKind:

            # Sort by kind
            def sorter(x):
                return self.kind_order.index(x[1].kind)

        elif self._signal_order == SignalOrder.byName:

            # Sort by name
            def sorter(x):
                return x[0]
        else:
            logger.exception("Unknown sorting type %r", self.sortBy)
            return
        # Add to layout
        for (label, signal) in sorted(set(signals), key=sorter):
            self.layout().add_signal(signal, label)

    def sizeHint(self):
        """Default SizeHint"""
        return QSize(240, 140)
Ejemplo n.º 6
0
class RequirementsModel(QAbstractItemModel, RequirementsModelRoles):

    Q_ENUMS(RequirementsModelRoles)

    requirementsChanged = Signal()

    def __init__(self, parent=None):
        super(RequirementsModel, self).__init__(parent)

        self._requirements = OrderedDict()

        self.requirementsChanged.connect(self.modelReset)

    @Property(OrderedDict, notify=requirementsChanged)
    def requirements(self):
        return self._requirements

    @requirements.setter
    def requirements(self, value):
        if value is self._requirements:
            return
        self._requirements = value
        self.requirementsChanged.emit()

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None

        key = list(self._requirements.keys())[index.row()]
        switch = {
            Qt.DisplayRole: lambda: key,
            self.KeyRole: lambda: key,
            self.ValueRole: lambda: self._requirements[key],
        }

        data = switch.get(role, lambda: None)()
        return data

    def roleNames(self):
        return self.role_names()

    def index(self, row, column, parent=QModelIndex()):
        if not self.hasIndex(row, column, parent):
            return QModelIndex()

        return self.createIndex(row, column)

    def flags(self, index):
        if not index.isValid():
            return Qt.NoItemFlags

        return Qt.ItemIsEnabled | Qt.ItemIsSelectable

    def headerData(self, _section, orientation, role=Qt.DisplayRole):
        if not orientation == Qt.Horizontal:
            return None

        return self.role_names().get(role, '').title()

    def parent(self, index):
        return QModelIndex()

    def columnCount(self, _parent=QModelIndex()):
        return len(self.role_names())

    def rowCount(self, parent=QModelIndex()):
        if parent.isValid():
            return 0

        return len(self._requirements)
Ejemplo n.º 7
0
class TyphosSignalPanel(TyphosBase, TyphosDesignerMixin, SignalOrder):
    """
    Panel of Signals for a given device, using :class:`SignalPanel`.

    Parameters
    ----------
    parent : QtWidgets.QWidget, optional
        The parent widget.

    init_channel : str, optional
        The PyDM channel with which to initialize the widget.
    """

    Q_ENUMS(SignalOrder)  # Necessary for display in Designer
    SignalOrder = SignalOrder  # For convenience
    # From top of page to bottom
    kind_order = (Kind.hinted, Kind.normal, Kind.config, Kind.omitted)
    _panel_class = SignalPanel
    updated = QtCore.Signal()

    _kind_to_property = {
        'hinted': 'showHints',
        'normal': 'showNormal',
        'config': 'showConfig',
        'omitted': 'showOmitted',
    }

    def __init__(self, parent=None, init_channel=None):
        super().__init__(parent=parent)
        # Create a SignalPanel layout to be modified later
        self._panel_layout = self._panel_class()
        self.setLayout(self._panel_layout)
        self._name_filter = ''
        # Add default Kind values
        self._kinds = dict.fromkeys([kind.name for kind in Kind], True)
        self._signal_order = SignalOrder.byKind

        self.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)
        self.contextMenuEvent = self.open_context_menu

    def _get_kind(self, kind):
        """Property getter for show[kind]."""
        return self._kinds[kind]

    def _set_kind(self, value, kind):
        """Property setter for show[kind] = value."""
        # If we have a new value store it
        if value != self._kinds[kind]:
            # Store it internally
            self._kinds[kind] = value
            # Remodify the layout for the new Kind
            self._update_panel()

    @property
    def filter_settings(self):
        """Get the filter settings dictionary."""
        return dict(
            name_filter=self.nameFilter,
            kinds=self.show_kinds,
        )

    def _update_panel(self):
        """Apply filters and emit the update signal."""
        self._panel_layout.filter_signals(**self.filter_settings)
        self.updated.emit()

    @property
    def show_kinds(self):
        """Get a list of the :class:`ophyd.Kind` that should be shown."""
        return [kind for kind in Kind if self._kinds[kind.name]]

    # Kind Configuration pyqtProperty
    showHints = Property(bool,
                         partial(_get_kind, kind='hinted'),
                         partial(_set_kind, kind='hinted'),
                         doc='Show ophyd.Kind.hinted signals')
    showNormal = Property(bool,
                          partial(_get_kind, kind='normal'),
                          partial(_set_kind, kind='normal'),
                          doc='Show ophyd.Kind.normal signals')
    showConfig = Property(bool,
                          partial(_get_kind, kind='config'),
                          partial(_set_kind, kind='config'),
                          doc='Show ophyd.Kind.config signals')
    showOmitted = Property(bool,
                           partial(_get_kind, kind='omitted'),
                           partial(_set_kind, kind='omitted'),
                           doc='Show ophyd.Kind.omitted signals')

    @Property(str)
    def nameFilter(self):
        """Get or set the current name filter."""
        return self._name_filter

    @nameFilter.setter
    def nameFilter(self, name_filter):
        if name_filter != self._name_filter:
            self._name_filter = name_filter.strip()
            self._update_panel()

    @Property(SignalOrder)
    def sortBy(self):
        """Get or set the order that the signals will be placed in layout."""
        return self._signal_order

    @sortBy.setter
    def sortBy(self, value):
        if value != self._signal_order:
            self._signal_order = value
            self._update_panel()

    def add_device(self, device):
        """Typhos hook for adding a new device."""
        self.devices.clear()
        super().add_device(device)
        # Configure the layout for the new device
        self._panel_layout.add_device(device)
        self._update_panel()

    def set_device_display(self, display):
        """Typhos hook for when the TyphosDeviceDisplay is associated."""
        self.display = display

    def generate_context_menu(self):
        """Generate a context menu for this TyphosSignalPanel."""
        menu = QtWidgets.QMenu(parent=self)
        menu.addSection('Kinds')
        for kind, property_name in self._kind_to_property.items():

            def selected(new_value, *, name=property_name):
                setattr(self, name, new_value)

            action = menu.addAction('Show &' + kind)
            action.setCheckable(True)
            action.setChecked(getattr(self, property_name))
            action.triggered.connect(selected)
        return menu

    def open_context_menu(self, ev):
        """
        Open a context menu when the Default Context Menu is requested.

        Parameters
        ----------
        ev : QEvent
        """
        menu = self.generate_context_menu()
        menu.exec_(self.mapToGlobal(ev.pos()))
Ejemplo n.º 8
0
class WatchlistModel(QAbstractItemModel, WatchlistModelRoles):

    Q_ENUMS(WatchlistModelRoles)

    appInterfaceChanged = Signal()

    def __init__(self, parent=None):
        super(WatchlistModel, self).__init__(parent)

        self._app_interface = None

    @Property(AppInterface, notify=appInterfaceChanged)
    def appInterface(self):
        return self._app_interface

    @appInterface.setter
    def appInterface(self, new_interface):
        if new_interface == self._app_interface:
            return
        old_interface = self._app_interface
        self._app_interface = new_interface
        self.appInterfaceChanged.emit()

        if old_interface:
            old_interface.watchedChanged.disconnect(self.modelReset)
            old_interface.itemUpdated.disconnect(self._on_item_updated)
        if new_interface:
            new_interface.watchedChanged.connect(self.modelReset)
            new_interface.itemUpdated.connect(self._on_item_updated)
            self.modelReset.emit()

    @property
    def _watched(self):
        if self._app_interface:
            return self._app_interface.watched
        else:
            return []

    def _on_item_updated(self, key):
        for i, item in enumerate(self._watched):
            if item.key == key:
                index = self.index(i, 0)
                self.dataChanged.emit(index, index)
                return

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None

        watched = self._watched
        row = index.row()
        if row >= len(watched):
            return None

        item = watched[row]
        switch = {
            Qt.DisplayRole: lambda: item.key,
            self.KeyRole: lambda: item.key,
            self.ValueRole: lambda: value_to_str(item.value),
        }

        data = switch.get(role, lambda: None)()
        return data

    def setData(self, index, value, role=Qt.EditRole):
        item = index.internalPointer()
        if role == self.ValueRole:
            new_value = str_to_value(value, item.value)
            if new_value is None:
                logger.error(f'Cannot convert value {item.key} {value}')
                changed = False
            else:
                item.value = new_value
                self._app_interface.setValue(item.key, new_value)
                changed = True
        else:
            changed = False

        if changed:
            self.dataChanged.emit(index, index, [role])
        return changed

    def roleNames(self):
        return self.role_names()

    def index(self, row, column, parent=QModelIndex()):
        if not self.hasIndex(row, column, parent=QModelIndex()):
            return QModelIndex()

        item = self._app_interface.watched[row]
        return self.createIndex(row, column, item)

    def flags(self, index):
        if not index.isValid():
            return Qt.NoItemFlags

        return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable

    def parent(self, index):
        return QModelIndex()

    def columnCount(self, _parent=QModelIndex()):
        return len(self.role_names())

    def rowCount(self, parent=QModelIndex()):
        if parent.isValid():
            return 0

        return len(self._app_interface.watched)
Ejemplo n.º 9
0
class PyDMStateButton(QFrame, PyDMWritableWidget):
    """
    A StateButton with support for Channels and much more from PyDM.

    It consists on QPushButton with internal state.

    Parameters
    ----------
    parent : QWidget
        The parent widget for the Label
    init_channel : str, optional
        The channel to be used by the widget.
    """

    class buttonShapeMap:
        """Enum class of shapes of button."""

        locals().update(**BUTTONSHAPE)

    Q_ENUMS(buttonShapeMap)

    # enumMap for buttonShapeMap
    locals().update(**BUTTONSHAPE)

    squaredbuttonstatesdict = dict()
    path = _os.path.abspath(_os.path.dirname(__file__))
    f = QFile(_os.path.join(path, 'resources/but_shapes/squared_on.svg'))
    if f.open(QFile.ReadOnly):
        squaredbuttonstatesdict['On'] = str(f.readAll(), 'utf-8')
    f.close()
    f = QFile(_os.path.join(path, 'resources/but_shapes/squared_off.svg'))
    if f.open(QFile.ReadOnly):
        squaredbuttonstatesdict['Off'] = str(f.readAll(), 'utf-8')
    f.close()
    f = QFile(_os.path.join(path, 'resources/but_shapes/squared_disconn.svg'))
    if f.open(QFile.ReadOnly):
        squaredbuttonstatesdict['Disconnected'] = str(f.readAll(), 'utf-8')
    f.close()

    roundedbuttonstatesdict = dict()
    f = QFile(_os.path.join(path, 'resources/but_shapes/rounded_on.svg'))
    if f.open(QFile.ReadOnly):
        roundedbuttonstatesdict['On'] = str(f.readAll(), 'utf-8')
    f.close()
    f = QFile(_os.path.join(path, 'resources/but_shapes/rounded_off.svg'))
    if f.open(QFile.ReadOnly):
        roundedbuttonstatesdict['Off'] = str(f.readAll(), 'utf-8')
    f.close()
    f = QFile(_os.path.join(path, 'resources/but_shapes/rounded_disconn.svg'))
    if f.open(QFile.ReadOnly):
        roundedbuttonstatesdict['Disconnected'] = str(f.readAll(), 'utf-8')
    f.close()

    clicked = Signal()
    DEFAULT_CONFIRM_MESSAGE = "Are you sure you want to proceed?"

    def __init__(self, parent=None, init_channel=None, invert=False):
        """Initialize all internal states and properties."""
        QFrame.__init__(self, parent)
        PyDMWritableWidget.__init__(self, init_channel=init_channel)

        self._off = 0
        self._on = 1
        self.invert = invert

        self._bit = -1
        self._bit_val = 0
        self.value = 0
        self.clicked.connect(self.send_value)
        self.shape = 0
        self.renderer = QSvgRenderer()

        self._show_confirm_dialog = False
        self._confirm_message = PyDMStateButton.DEFAULT_CONFIRM_MESSAGE
        self._password_protected = False
        self._password = ""
        self._protected_password = ""

    @Property(bool)
    def passwordProtected(self):
        """
        Whether or not this button is password protected.

        Returns
        -------
        bool
        """
        return self._password_protected

    @passwordProtected.setter
    def passwordProtected(self, value):
        """
        Whether or not this button is password protected.

        Parameters
        ----------
        value : bool
        """
        if self._password_protected != value:
            self._password_protected = value

    @Property(str)
    def password(self):
        """
        Password to be encrypted using SHA256.

        .. warning::
            To avoid issues exposing the password this method
            always returns an empty string.

        Returns
        -------
        str
        """
        return ""

    @password.setter
    def password(self, value):
        """
        Password to be encrypted using SHA256.

        Parameters
        ----------
        value : str
            The password to be encrypted
        """
        if value is not None and value != "":
            sha = _hashlib.sha256()
            sha.update(value.encode())
            # Use the setter as it also checks whether the existing password
            # is the same with the new one, and only updates if the new
            # password is different
            self.protectedPassword = sha.hexdigest()

    @Property(str)
    def protectedPassword(self):
        """
        The encrypted password.

        Returns
        -------
        str
        """
        return self._protected_password

    @protectedPassword.setter
    def protectedPassword(self, value):
        if self._protected_password != value:
            self._protected_password = value

    @Property(bool)
    def showConfirmDialog(self):
        """
        Wether or not to display a confirmation dialog.

        Returns
        -------
        bool
        """
        return self._show_confirm_dialog

    @showConfirmDialog.setter
    def showConfirmDialog(self, value):
        """
        Wether or not to display a confirmation dialog.

        Parameters
        ----------
        value : bool
        """
        if self._show_confirm_dialog != value:
            self._show_confirm_dialog = value

    @Property(str)
    def confirmMessage(self):
        """
        Message to be displayed at the Confirmation dialog.

        Returns
        -------
        str
        """
        return self._confirm_message

    @confirmMessage.setter
    def confirmMessage(self, value):
        """
        Message to be displayed at the Confirmation dialog.

        Parameters
        ----------
        value : str
        """
        if self._confirm_message != value:
            self._confirm_message = value

    def mouseReleaseEvent(self, ev):
        """Deal with mouse clicks. Only accept clicks within the figure."""
        cond = ev.button() == Qt.LeftButton
        cond &= ev.x() < self.width()/2+self.height()
        cond &= ev.x() > self.width()/2-self.height()
        if cond:
            self.clicked.emit()

    def value_changed(self, new_val):
        """
        Callback invoked when the Channel value is changed.

        Display the value of new_val accordingly. If :attr:'pvBit' is n>=0 or
        positive the button display the state of the n-th digit of the channel.

        Parameters
        ----------
        new_value : str, int, float, bool or np.ndarray
            The new value from the channel. The type depends on the channel.
        """
        if isinstance(new_val, _np.ndarray):
            _log.warning('PyDMStateButton received a numpy array to ' +
                         self.channel+' ('+str(new_val)+')!')
            return
        super(PyDMStateButton, self).value_changed(new_val)
        value = int(new_val)
        self.value = value
        if self._bit >= 0:
            value = (value >> self._bit) & 1
        self._bit_val = value
        self.update()

    def confirm_dialog(self):
        """
        Show the confirmation dialog with the proper message in case
        ```showConfirmMessage``` is True.

        Returns
        -------
        bool
            True if the message was confirmed or if ```showCofirmMessage```
            is False.
        """

        if not self._show_confirm_dialog:
            return True

        if self._confirm_message == "":
            self._confirm_message = PyDMStateButton.DEFAULT_CONFIRM_MESSAGE
        msg = QMessageBox()
        msg.setIcon(QMessageBox.Question)
        msg.setText(self._confirm_message)
        msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
        msg.setDefaultButton(QMessageBox.No)
        ret = msg.exec_()
        return not ret == QMessageBox.No

    def validate_password(self):
        """
        If the widget is ```passwordProtected```, this method will propmt
        the user for the correct password.

        Returns
        -------
        bool
            True in case the password was correct of if the widget is not
            password protected.
        """
        if not self._password_protected:
            return True

        pwd, ok = QInputDialog().getText(
           None, "Authentication", "Please enter your password:"******"")
        pwd = str(pwd)
        if not ok or pwd == "":
            return False

        sha = _hashlib.sha256()
        sha.update(pwd.encode())
        pwd_encrypted = sha.hexdigest()
        if pwd_encrypted != self._protected_password:
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Critical)
            msg.setText("Invalid password.")
            msg.setWindowTitle("Error")
            msg.setStandardButtons(QMessageBox.Ok)
            msg.setDefaultButton(QMessageBox.Ok)
            msg.setEscapeButton(QMessageBox.Ok)
            msg.exec_()
            return False
        return True

    def send_value(self):
        """
        Emit a :attr:`send_value_signal` to update channel value.

        If :attr:'pvBit' is n>=0 or positive the button toggles the state of
        the n-th digit of the channel. Otherwise it toggles the whole value.
        """
        if not self._connected:
            return None
        if not self.confirm_dialog():
            return None
        if not self.validate_password():
            return None

        checked = not self._bit_val
        val = checked
        if self._bit >= 0:
            val = int(self.value)
            val ^= (-checked ^ val) & (1 << self._bit)
            # For explanation look:
            # https://stackoverflow.com/questions/47981/how-do-you-set-clear-and-toggle-a-single-bit
        self.send_value_signal[self.channeltype].emit(self.channeltype(val))

    def sizeHint(self):
        """Return size hint to define size on initialization."""
        return QSize(72, 36)

    def paintEvent(self, event):
        """Treat appearence changes based on connection state and value."""
        self.style().unpolish(self)
        self.style().polish(self)

        if not self.isEnabled():
            state = 'Disconnected'
        elif self._bit_val == self._on:
            state = 'On'
        elif self._bit_val == self._off:
            state = 'Off'
        else:
            state = 'Disconnected'

        if self.shape == 0:
            shape_dict = PyDMStateButton.squaredbuttonstatesdict
        elif self.shape == 1:
            shape_dict = PyDMStateButton.roundedbuttonstatesdict

        option = QStyleOption()
        option.initFrom(self)
        h = option.rect.height()
        w = option.rect.width()
        aspect = 2.0
        ah = w/aspect
        aw = w
        if ah > h:
            ah = h
            aw = h*aspect
        x = abs(aw-w)/2.0
        y = abs(ah-h)/2.0
        bounds = QRectF(x, y, aw, ah)

        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing, True)

        shape_str = shape_dict[state]
        buttonstate_bytearray = bytes(shape_str, 'utf-8')
        self.renderer.load(QByteArray(buttonstate_bytearray))
        self.renderer.render(painter, bounds)

    @Property(buttonShapeMap)
    def shape(self):
        """
        Property to define the shape of the button.

        Returns
        -------
        int
        """
        return self._shape

    @shape.setter
    def shape(self, new_shape):
        """
        Property to define the shape of the button.

        Parameters
        ----------
        value : int
        """
        if new_shape in [PyDMStateButton.Rounded, PyDMStateButton.Squared]:
            self._shape = new_shape
            self.update()
        else:
            raise ValueError('Button shape not defined!')

    @Property(int)
    def pvbit(self):
        """
        Property to define which PV bit to control.

        Returns
        -------
        int
        """
        return int(self._bit)

    @pvbit.setter
    def pvbit(self, bit):
        """
        Property to define which PV bit to control.

        Parameters
        ----------
        value : int
        """
        if bit >= 0:
            self._bit = int(bit)

    @Property(bool)
    def invert(self):
        """
        Property that indicates whether to invert button on/off representation.

        Return
        ------
        bool
        """
        return self._invert

    @invert.setter
    def invert(self, value):
        """
        Property that indicates whether to invert button on/off representation.

        Parameters
        ----------
        value: bool
        """
        self._invert = value
        if self._invert:
            self._on = 0
            self._off = 1
        else:
            self._on = 1
            self._off = 0
Ejemplo n.º 10
0
class PyDMTemplateRepeater(QFrame, PyDMPrimitiveWidget, LayoutType):
    """
    PyDMTemplateRepeater takes a .ui file with macro variables as a template, and a JSON
    file (or a list of dictionaries) with a list of values to use to fill in
    the macro variables, then creates a layout with one instance of the
    template for each item in the list.

    It can be very convenient if you have displays that repeat the same set of
    widgets over and over - for instance, if you have a standard set of
    controls for a magnet, and want to build a display with a list of controls
    for every magnet, the Template Repeater lets you do that with a minimum
    amount of work: just build a template for a single magnet, and a JSON list
    with the data that describes all of the magnets.

    Parameters
    ----------
    parent : optional
        The parent of this widget.
    """
    Q_ENUMS(LayoutType)
    LayoutType = LayoutType

    def __init__(self, parent=None):
        QFrame.__init__(self, parent)
        PyDMPrimitiveWidget.__init__(self)
        self._template_filename = ""
        self._count_shown_in_designer = 1
        self._data_source = ""
        self._data = []
        self._cached_template = None
        self._parent_macros = None
        self._layout_type = LayoutType.Vertical
        self._temp_layout_spacing = 4
        self.app = QApplication.instance()
        self.rebuild()

    @Property(LayoutType)
    def layoutType(self):
        """
        The layout type to use.

        Returns
        -------
        LayoutType
        """
        return self._layout_type

    @layoutType.setter
    def layoutType(self, new_type):
        """
        The layout type to use.
        Options are:
        - **Vertical**: Instances of the template are laid out vertically, in rows.
        - **Horizontal**: Instances of the template are laid out horizontally, in columns.
        - **Flow**: Instances of the template are laid out horizontally until they reach the edge of the template, at which point they "wrap" into a new row.

        Parameters
        ----------
        new_type : LayoutType
        """
        if new_type != self._layout_type:
            self._layout_type = new_type
            self.rebuild()

    @Property(int)
    def layoutSpacing(self):
        if self.layout():
            return self.layout().spacing()
        return self._temp_layout_spacing

    @layoutSpacing.setter
    def layoutSpacing(self, new_spacing):
        self._temp_layout_spacing = new_spacing
        if self.layout():
            self.layout().setSpacing(new_spacing)

    @Property(int)
    def countShownInDesigner(self):
        """
        The number of instances to show in Qt Designer.  This property has no
        effect outside of Designer.
        
        Returns
        -------
        int
        """
        return self._count_shown_in_designer

    @countShownInDesigner.setter
    def countShownInDesigner(self, new_count):
        """
        The number of instances to show in Qt Designer.  This property has no
        effect outside of Designer.
        
        Parameters
        ----------
        new_count : int
        """
        if not is_qt_designer():
            return
        try:
            new_count = int(new_count)
        except ValueError:
            logger.exception(
                "Couldn't convert {} to integer.".format(new_count))
            return
        new_count = max(new_count, 0)
        if new_count != self._count_shown_in_designer:
            self._count_shown_in_designer = new_count
            self.rebuild()

    @Property(str)
    def templateFilename(self):
        """
        The path to the .ui file to use as a template.
        
        Returns
        -------
        str
        """
        return self._template_filename

    @templateFilename.setter
    def templateFilename(self, new_filename):
        """
        The path to the .ui file to use as a template.
        
        Parameters
        ----------
        new_filename : str
        """
        if new_filename != self._template_filename:
            self._template_filename = new_filename
            self._cached_template = None
            if self._template_filename:
                self.rebuild()
            else:
                self.clear()

    @Property(str)
    def dataSource(self):
        """
        The path to the JSON file to fill in each instance of the template.
        
        Returns
        -------
        str
        """
        return self._data_source

    @dataSource.setter
    def dataSource(self, data_source):
        """
        Sets the path to the JSON file to fill in each instance of the template.
        
        For example, if you build a template that contains two macro variables,
        ${NAME} and ${UNIT}, your JSON file should be a list of dictionaries,
        each with keys for NAME and UNIT, like this:
        
        [{"NAME": "First Device", "UNIT": 1}, {"NAME": "Second Device", "UNIT": 2}]
        
        Parameters
        -------
        data_source : str
        """
        if data_source != self._data_source:
            self._data_source = data_source
            if self._data_source:
                try:
                    parent_display = self.find_parent_display()
                    base_path = None
                    if parent_display:
                        base_path = os.path.dirname(
                            parent_display.loaded_file())
                    fname = find_file(self._data_source, base_path=base_path)

                    if not fname:
                        if not is_qt_designer():
                            logger.error(
                                'Cannot locate data source file {} for PyDMTemplateRepeater.'
                                .format(fname))
                        self.data = []
                    else:
                        with open(fname) as f:
                            try:
                                self.data = json.load(f)
                            except ValueError:
                                logger.error(
                                    'Failed to parse data source file {} for PyDMTemplateRepeater.'
                                    .format(fname))
                                self.data = []
                except IOError as e:
                    self.data = []
            else:
                self.clear()

    def open_template_file(self, variables=None):
        """
        Opens the widget specified in the templateFilename property.
        
        Parameters
        ----------
        variables : dict
            A dictionary of macro variables to apply when loading, in addition
            to all the macros specified on the template repeater widget.
        Returns
        -------
        display : QWidget
        """
        if not variables:
            variables = {}

        parent_display = self.find_parent_display()
        base_path = None
        if parent_display:
            base_path = os.path.dirname(parent_display.loaded_file())
        fname = find_file(self.templateFilename, base_path=base_path)

        if self._parent_macros is None:
            self._parent_macros = {}
            if parent_display:
                self._parent_macros = parent_display.macros()

        parent_macros = copy.copy(self._parent_macros)
        parent_macros.update(variables)
        try:
            w = load_file(fname, macros=parent_macros, target=None)
        except Exception as ex:
            w = QLabel('Error: could not load template: ' + str(ex))
        return w

    def rebuild(self):
        """ Clear out all existing widgets, and populate the list using the
        template file and data source."""
        self.clear()
        if (not self.templateFilename) or (not self.data):
            return
        self.setUpdatesEnabled(False)

        layout_class = layout_class_for_type[self.layoutType]
        if type(self.layout()) != layout_class:
            if self.layout() is not None:
                # Trick to remove the existing layout by re-parenting it in an empty widget.
                QWidget().setLayout(self.layout())
            l = layout_class(self)
            self.setLayout(l)
            self.layout().setSpacing(self._temp_layout_spacing)
        try:
            with pydm.data_plugins.connection_queue(defer_connections=True):
                for i, variables in enumerate(self.data):
                    if is_qt_designer() and i > self.countShownInDesigner - 1:
                        break
                    w = self.open_template_file(variables)
                    if w is None:
                        w = QLabel()
                        w.setText(
                            "No Template Loaded.  Data: {}".format(variables))
                    w.setParent(self)
                    self.layout().addWidget(w)
        except:
            logger.exception('Template repeater failed to rebuild.')
        finally:
            # If issues happen during the rebuild we should still enable
            # updates and establish connection for the widgets added.
            # Moreover, if we dont call establish_queued_connections
            # the queue will never be emptied and new connections will be
            # staled.
            self.setUpdatesEnabled(True)
            pydm.data_plugins.establish_queued_connections()

    def clear(self):
        """ Clear out any existing instances of the template inside
        the widget."""
        if not self.layout():
            return
        while self.layout().count() > 0:
            item = self.layout().takeAt(0)
            item.widget().deleteLater()
            del item

    def count(self):
        if not self.layout():
            return 0
        return self.layout().count()

    @property
    def data(self):
        """
        The dictionary used by the widget to fill in each instance of the template.
        This property will be overwritten if the user changes the dataSource
        property.
        """
        return self._data

    @data.setter
    def data(self, new_data):
        """
        Sets the dictionary used by the widget to fill in each instance of 
        the template.  This property will be overwritten if the user changes
        the dataSource property.  After setting this property, `rebuild` 
        is automatically called to refresh the widget.
        """
        self._data = new_data
        self.rebuild()
Ejemplo n.º 11
0
class HalLabel(QLabel, HALWidget, HalType):
    """HAL Label

    Label for displaying HAL pin values.

    Input pin type is selectable via the :class:`.pinType` property in designer,
    and can be any valid HAL type (bit, u32, s32, float).

    The text format can be specified via the :class:`.textFormat` property in
    designer and can be any valid python style format string.

    .. table:: Generated HAL Pins

        ========================= =========== =========
        HAL Pin Name              Type        Direction
        ========================= =========== =========
        qtpyvcp.label.enable      bit         in
        qtpyvcp.label.in          selecatable in
        ========================= =========== =========
    """
    Q_ENUMS(HalType)

    def __init__(self, parent=None):
        super(HalLabel, self).__init__(parent)

        self._in_pin = None
        self._enable_pin = None

        self._typ = "float"
        self._fmt = "%s"

        self.setValue(0)

    def setValue(self, value):
        self.setText(self._fmt % value)

    @Property(str)
    def textFormat(self):
        """Text Format Property

        Args:
            fmt (str) : A valid python style format string. Defaults to ``%s``.
        """
        return self._fmt

    @textFormat.setter
    def textFormat(self, fmt):
        self._fmt = fmt
        try:
            val = {'bit': False, 'u32': 0, 's32': 0, 'float': 0.0}[self._typ]
            self.setValue(val)
        except:
            pass

    @Property(HalType)
    def pinType(self):
        return getattr(HalType, self._typ)

    @pinType.setter
    def pinType(self, typ_enum):
        self._typ = HalType.toString(typ_enum)
        try:
            val = {'bit': False, 'u32': 0, 's32': 0, 'float': 0.0}[self._typ]
            self.setValue(val)
        except:
            pass

    def initialize(self):
        comp = hal.getComponent()
        obj_name = self.getPinBaseName()

        # add label.enable HAL pin
        self._enable_pin = comp.addPin(obj_name + ".enable", "bit", "in")
        self._enable_pin.value = self.isEnabled()
        self._enable_pin.valueChanged.connect(self.setEnabled)

        # add label.in HAL pin
        self._in_pin = comp.addPin(obj_name + ".in", self._typ, "in")
        self.setValue(self._in_pin.value)
        self._in_pin.valueChanged.connect(self.setValue)
Ejemplo n.º 12
0
class QLed(QFrame, ShapeMap):
    """QLed class."""

    ShapeMap = ShapeMap
    Q_ENUMS(ShapeMap)

    abspath = _os.path.abspath(_os.path.dirname(__file__))
    shapesdict = dict()
    f = QFile(_os.path.join(abspath, 'resources/led_shapes/circle.svg'))
    if f.open(QFile.ReadOnly):
        shapesdict[ShapeMap.Circle] = str(f.readAll(), 'utf-8')
    f.close()
    f = QFile(_os.path.join(abspath, 'resources/led_shapes/round.svg'))
    if f.open(QFile.ReadOnly):
        shapesdict[ShapeMap.Round] = str(f.readAll(), 'utf-8')
    f.close()
    f = QFile(_os.path.join(abspath, 'resources/led_shapes/square.svg'))
    if f.open(QFile.ReadOnly):
        shapesdict[ShapeMap.Square] = str(f.readAll(), 'utf-8')
    f.close()
    f = QFile(_os.path.join(abspath, 'resources/led_shapes/triangle.svg'))
    if f.open(QFile.ReadOnly):
        shapesdict[ShapeMap.Triangle] = str(f.readAll(), 'utf-8')
    f.close()

    Green = QColor(15, 105, 0)
    Red = QColor(207, 0, 0)
    Gray = QColor(90, 90, 90)
    SelColor = QColor(0, 0, 0)
    NotSelColor1 = QColor(251, 244, 252)
    NotSelColor2 = QColor(173, 173, 173)

    clicked = Signal()
    selected = Signal(bool)

    def __init__(self, parent=None, **kwargs):
        """Class constructor."""
        super().__init__(parent, **kwargs)
        self.m_state = 0
        self.m_stateColors = [self.Red, self.Green]

        self.m_dsblColor = self.Gray
        self.m_shape = self.ShapeMap.Circle

        self._pressed = False
        self._isselected = False
        self.renderer = QSvgRenderer()

    def getState(self):
        """Value property getter."""
        return self.m_state

    def setState(self, value):
        """Value property setter."""
        self.m_state = value
        self.update()

    state = Property(bool, getState, setState)

    def getOnColor(self):
        """On color property getter."""
        return self.m_stateColors[1]

    def setOnColor(self, newColor):
        """On color property setter."""
        self.m_stateColors[1] = newColor
        self.update()

    onColor = Property(QColor, getOnColor, setOnColor)

    def getOffColor(self):
        """Off color property getter."""
        return self.m_stateColors[0]

    def setOffColor(self, newColor):
        """Off color property setter."""
        self.m_stateColors[0] = newColor
        self.update()

    offColor = Property(QColor, getOffColor, setOffColor)

    @property
    def stateColors(self):
        """Color list property getter."""
        return list(self.m_stateColors)

    @stateColors.setter
    def stateColors(self, new_colors):
        """Color list property setter."""
        if not isinstance(new_colors, (list, tuple)) or\
                len(new_colors) < 2 or not isinstance(new_colors[0], QColor):
            return
        self.m_stateColors = list(new_colors)

    def getDsblColor(self):
        """Disabled color property getter."""
        return self.m_dsblColor

    def setDsblColor(self, newColor):
        """Disabled color property setter."""
        self.m_dsblColor = newColor
        self.update()

    dsblColor = Property(QColor, getDsblColor, setDsblColor)

    def getShape(self):
        """Shape property getter."""
        return self.m_shape

    def setShape(self, newShape):
        """Shape property setter."""
        self.m_shape = newShape
        self.update()

    shape = Property(ShapeMap, getShape, setShape)

    def sizeHint(self):
        """Return the base size of the widget according to shape."""
        if self.m_shape == self.ShapeMap.Triangle:
            return QSize(48, 36)
        elif self.m_shape == self.ShapeMap.Round:
            return QSize(72, 36)
        return QSize(36, 36)

    def adjust(self, r, g, b):
        """Adjust the color to set on svg code."""
        def normalise(x):
            return x / 255.0

        def denormalise(x):
            if x <= 1:
                return int(x * 255.0)
            else:
                return 255.0

        (h, l, s) = rgb_to_hls(normalise(r), normalise(g), normalise(b))
        (nr, ng, nb) = hls_to_rgb(h, l * 1.5, s)

        return (denormalise(nr), denormalise(ng), denormalise(nb))

    def getRGBfromQColor(self, qcolor):
        """Convert QColors to a tupple of rgb colors to set on svg code."""
        redhex = qcolor.red()
        greenhex = qcolor.green()
        bluehex = qcolor.blue()
        return (redhex, greenhex, bluehex)

    def paintEvent(self, event):
        """Handle appearence of the widget on state updates."""
        self.style().unpolish(self)
        self.style().polish(self)
        option = QStyleOption()
        option.initFrom(self)

        h = option.rect.height()
        w = option.rect.width()
        if self.m_shape in (self.ShapeMap.Triangle, self.ShapeMap.Round):
            aspect = (4 /
                      3.0) if self.m_shape == self.ShapeMap.Triangle else 2.0
            ah = w / aspect
            aw = w
            if ah > h:
                ah = h
                aw = h * aspect
            x = abs(aw - w) / 2.0
            y = abs(ah - h) / 2.0
            bounds = QRectF(x, y, aw, ah)
        else:
            size = min(w, h)
            x = abs(size - w) / 2.0
            y = abs(size - h) / 2.0
            bounds = QRectF(x, y, size, size)

        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing, True)

        ind = self.m_state % len(self.m_stateColors)
        dark_r, dark_g, dark_b = self.getRGBfromQColor(self.m_stateColors[ind])
        if not self.isEnabled():
            dark_r, dark_g, dark_b = self.getRGBfromQColor(self.m_dsblColor)

        sel1_r, sel1_g, sel1_b = self.getRGBfromQColor(self.SelColor)
        sel2_r, sel2_g, sel2_b = self.getRGBfromQColor(self.SelColor)
        opc = '1.000'
        if not self.isSelected():
            sel1_r, sel1_g, sel1_b = self.getRGBfromQColor(self.NotSelColor1)
            sel2_r, sel2_g, sel2_b = self.getRGBfromQColor(self.NotSelColor2)
            opc = '0.145'

        dark_str = "rgb(%d,%d,%d)" % (dark_r, dark_g, dark_b)
        light_str = "rgb(%d,%d,%d)" % self.adjust(dark_r, dark_g, dark_b)
        sel1_str = "rgb(%d,%d,%d)" % (sel1_r, sel1_g, sel1_b)
        sel2_str = "rgb(%d,%d,%d)" % (sel2_r, sel2_g, sel2_b)

        shape_bytes = bytes(
            self.shapesdict[self.m_shape] %
            (sel1_str, opc, sel2_str, dark_str, light_str), 'utf-8')

        self.renderer.load(QByteArray(shape_bytes))
        self.renderer.render(painter, bounds)

    def mousePressEvent(self, event):
        """Handle mouse press event."""
        self._pressed = True
        super().mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        """Handle mouse release event."""
        if self._pressed:
            self._pressed = False
            self.clicked.emit()
        super().mouseReleaseEvent(event)

    def toggleState(self):
        """Toggle state property."""
        self.m_state = 0 if self.m_state else 1
        self.update()

    def isSelected(self):
        """Return selected state of object."""
        return self._isselected

    def setSelected(self, sel):
        """Configure selected state of object."""
        self._isselected = bool(sel)
        self.selected.emit(self._isselected)
        self.update()

    def toggleSelected(self):
        """Toggle isSelected property."""
        self.setSelected(not self.isSelected())
Ejemplo n.º 13
0
class PyDMLogLabel(QListWidget, TextFormatter, PyDMWidget, DisplayFormat):
    """
    A QListWidget with support for Channels and more from PyDM.

    Parameters
    ----------
    parent : QWidget
        The parent widget for the Label
    init_channel : str, optional
        The channel to be used by the widget.
    """

    DisplayFormat = DisplayFormat
    Q_ENUMS(DisplayFormat)

    errorcolor = QColor(255, 0, 0)
    warncolor = QColor(200, 200, 0)

    def __init__(self, parent=None, init_channel=None, replace=None):
        QListWidget.__init__(self, parent)
        PyDMWidget.__init__(self, init_channel=init_channel)
        self._buffer_size = 1000
        self._prepend_date_time = True
        self._display_format_type = DisplayFormat.String
        self._string_encoding = "utf_8"
        self._date_time_fmt = '%Y/%m/%d-%H:%M:%S'
        self._replace = list() if replace is None else replace

        channel = '' if init_channel is None else init_channel
        self._plugin_conns = plugin_for_address(channel).connections

    def value_changed(self, new_value):
        """
        Callback invoked when the Channel value is changed.

        Sets the value of new_value accordingly at the Label.

        Parameters
        ----------
        new_value : str, int, float, bool or np.ndarray
            The new value from the channel. The type depends on the channel.
        """
        super(PyDMLogLabel, self).value_changed(new_value)

        new_value = parse_value_for_display(
            value=new_value,
            precision=self._prec,
            display_format_type=self._display_format_type,
            string_encoding=self._string_encoding,
            widget=self)

        if self.count() > self._buffer_size:
            self.clear()

        prefix = ''
        if self._prepend_date_time:
            timestamp = self._plugin_conns[self.channel].pv.timestamp
            prefix += _Time(timestamp).strftime(self._date_time_fmt)
            prefix += ' '
        # If the value is a string, just display it as-is, no formatting
        # needed.
        item = None
        if isinstance(new_value, str):
            if self._replace:
                last_item = self.item(self.count() - 1)
                if last_item is not None:
                    last_text = last_item.text().lower()
                    for r in self._replace:
                        if r.lower() in new_value.lower() and \
                                r.lower() in last_text.lower():
                            item = last_item
                            item.setText(prefix + new_value)
                            return
            if item is None:
                item = QListWidgetItem(prefix + new_value)
            if new_value.lower().startswith(('err', 'fatal')):
                item.setForeground(self.errorcolor)
            elif new_value.lower().startswith('warn'):
                item.setForeground(self.warncolor)
        # If the value is an enum, display the appropriate enum string for
        # the value.
        elif self.enum_strings is not None and isinstance(new_value, int):
            try:
                item = QListWidgetItem(prefix + self.enum_strings[new_value])
            except IndexError:
                item = QListWidgetItem("**INVALID**")
        # If the value is a number (float or int), display it using a
        # format string if necessary.
        elif isinstance(new_value, (int, float)):
            item = QListWidgetItem(prefix +
                                   self.format_string.format(new_value))
        # If you made it this far, just turn whatever the heck the value
        # is into a string and display it.
        else:
            item = QListWidgetItem(prefix + str(new_value))

        if item is not None:
            self.addItem(item)
            self.scrollToBottom()

    @Property(DisplayFormat)
    def displayFormat(self):
        """
        The format to display data.

        Returns
        -------
        int
        """
        return self._display_format_type

    @displayFormat.setter
    def displayFormat(self, new_type):
        """
        The maximum number of entries to show.

        When maximum is exceeded the widget is cleared.

        Returns
        -------
        int
        """
        if self._display_format_type != new_type:
            self._display_format_type = new_type
            # Trigger the update of display format
            if self.value is not None:
                self.value_changed(self.value)

    @Property(int)
    def bufferSize(self):
        """
        The maximum number of entries to show.

        When maximum is exceeded the widget is cleared.

        Returns
        -------
        int
        """
        return int(self._buffer_size)

    @bufferSize.setter
    def bufferSize(self, value):
        """
        The maximum number of entries to show.

        When maximum is exceeded the widget is cleared.

        Parameters
        ----------
        value : int
        """
        self._buffer_size = int(value)

    @Property(bool)
    def prependDateTime(self):
        """
        Define if the date and time information will be prepended to the text.

        Returns
        -------
        bool
        """
        return self._prepend_date_time

    @prependDateTime.setter
    def prependDateTime(self, value):
        """
        Define if the date and time information will be prepended to the text.

        Parameters
        ----------
        value : bool
        """
        self._prepend_date_time = bool(value)

    @Property(str)
    def dateTimeFmt(self):
        """
        Define the format of the datetime information to be prepended.

        Returns
        -------
        str
        """
        return self._date_time_fmt

    @dateTimeFmt.setter
    def dateTimeFmt(self, value):
        """
        Define the format of the datetime information to be prepended.

        Parameters
        ----------
        value : str
        """
        self._date_time_fmt = str(value)
Ejemplo n.º 14
0
class ProclogModel(QStandardItemModel, ProclogModelRoles):

    Q_ENUMS(ProclogModelRoles)

    appInterfaceChanged = Signal()

    def __init__(self, parent=None):
        super(ProclogModel, self).__init__(parent)
        self._app_interface = None
        self.setSortRole(self.IdRole)

    @Property(AppInterface, notify=appInterfaceChanged)
    def appInterface(self):
        return self._app_interface

    @appInterface.setter
    def appInterface(self, new_interface):
        if new_interface == self._app_interface:
            return
        old_interface = self._app_interface
        self._app_interface = new_interface
        self.appInterfaceChanged.emit()

        if old_interface:
            old_interface.proclogChanged.disconnect(self.refresh)
        if new_interface:
            new_interface.proclogChanged.connect(self.refresh)
            self.refresh()

    @Slot()
    def refresh(self):
        self.clear()

        if not self._app_interface:
            logger.warning('cannot refresh without an appInterface')
            return

        proclog = self._app_interface.proclog
        previous_level = 0
        item_stack = deque([self])
        for i, log in enumerate(proclog):
            level = 0
            name = ''
            info = {}
            for item in log:
                if item == '->':
                    level += 1
                else:
                    name = item['name']
                    info = item['information']
            item = QStandardItem(name)
            item.setData(True, self.IsProcedureRole)
            item.setData('', self.ValueRole)
            item.setData(i + 1, self.IdRole)
            if isinstance(info, dict) and len(info):
                data_item = QStandardItem(self.tr('data'))
                data_item.setData(False, self.IsProcedureRole)
                data_item.setData('', self.ValueRole)
                for k, v in info.items():
                    entry_item = QStandardItem(k)
                    if isinstance(v, dict) and 'address' in v and 'value' in v:
                        value_item = QStandardItem('value')
                        value_item.setData(str(v['value']), self.ValueRole)
                        value_item.setData(False, self.IsProcedureRole)
                        entry_item.appendRow(value_item)
                        value_item = QStandardItem('address')
                        value_item.setData(str(v['address']), self.ValueRole)
                        value_item.setData(False, self.IsProcedureRole)
                        entry_item.appendRow(value_item)
                        value = ''
                    else:
                        value = str(v)
                    entry_item.setData(value, self.ValueRole)
                    entry_item.setData(False, self.IsProcedureRole)
                    data_item.appendRow(entry_item)
                item.appendRow(data_item)
            elif isinstance(info, str):
                data_item = QStandardItem(self.tr('info'))
                data_item.setData(False, self.IsProcedureRole)
                data_item.setData(info, self.ValueRole)
                item.appendRow(data_item)

            if level > previous_level:
                previous_level = level
            elif level == previous_level:
                item_stack.pop()
            else:
                item_stack.pop()
                while previous_level > level and previous_level > 0:
                    item_stack.pop()
                    previous_level -= 1
            item_stack[-1].appendRow(item)
            item_stack.append(item)

    def roleNames(self):
        return self.role_names()
Ejemplo n.º 15
0
class PyDMLogDisplay(QWidget, LogLevels):
    """
    Standard display for Log Output

    This widget handles instantating a ``GuiHandler`` and displaying log
    messages to a ``QPlainTextEdit``. The level of the log can be changed from
    inside the widget itself, allowing users to select from any of the
    ``.levels`` specified by the widget.

    Parameters
    ----------
    parent : QObject, optional

    logname : str
        Name of log to display in widget

    level : logging.Level
        Initial level of log display

    """
    Q_ENUMS(LogLevels)
    LogLevels = LogLevels
    terminator = '\n'
    default_format = '%(asctime)s %(message)s'
    default_level = logging.INFO

    def __init__(self, parent=None, logname=None, level=logging.NOTSET):
        QWidget.__init__(self, parent=parent)
        # Create Widgets
        self.label = QLabel('Minimum displayed log level: ', parent=self)
        self.combo = QComboBox(parent=self)
        self.text = QPlainTextEdit(parent=self)
        self.text.setReadOnly(True)
        self.clear_btn = QPushButton("Clear", parent=self)
        # Create layout
        layout = QVBoxLayout()
        level_control = QHBoxLayout()
        level_control.addWidget(self.label)
        level_control.addWidget(self.combo)
        layout.addLayout(level_control)
        layout.addWidget(self.text)
        layout.addWidget(self.clear_btn)
        self.setLayout(layout)
        # Allow QCombobox to control log level
        for log_level, value in LogLevels.as_dict().items():
            self.combo.addItem(log_level, value)
        self.combo.currentIndexChanged[str].connect(self.setLevel)
        # Allow QPushButton to clear log text
        self.clear_btn.clicked.connect(self.clear)
        # Create a handler with the default format
        self.handler = GuiHandler(level=level, parent=self)
        self.logFormat = self.default_format
        self.handler.message.connect(self.write)
        # Create logger. Either as a root or given logname
        self.log = None
        self.level = None
        self.logName = logname or ''
        self.logLevel = level
        self.destroyed.connect(functools.partial(logger_destroyed, self.log))

    def sizeHint(self):
        return QSize(400, 300)

    @Property(LogLevels)
    def logLevel(self):
        return self.level

    @logLevel.setter
    def logLevel(self, level):
        if level != self.level:
            self.level = level
            idx = self.combo.findData(level)
            self.combo.setCurrentIndex(idx)

    @Property(str)
    def logName(self):
        """Name of associated log"""
        return self.log.name

    @logName.setter
    def logName(self, name):
        # Disconnect prior log from handler
        if self.log:
            self.log.removeHandler(self.handler)
        # Reattach handler to new handler
        self.log = logging.getLogger(name)
        # Ensure that the log matches level of handler
        # only if the handler level is less than the log.
        if self.log.level < self.handler.level:
            self.log.setLevel(self.handler.level)
        # Attach preconfigured handler
        self.log.addHandler(self.handler)

    @Property(str)
    def logFormat(self):
        """Format for log messages"""
        return self.handler.formatter._fmt

    @logFormat.setter
    def logFormat(self, fmt):
        self.handler.setFormatter(logging.Formatter(fmt))

    @Slot(str)
    def write(self, message):
        """Write a message to the log display"""
        # We split the incoming message by new lines. In prior iterations of
        # this widget it was discovered that large blocks of text cause issues
        # at the Qt level.
        for msg in message.split(self.terminator):
            self.text.appendPlainText(msg)

    @Slot()
    def clear(self):
        """Clear the text area."""
        self.text.clear()

    @Slot(str)
    def setLevel(self, level):
        """Set the level of the contained logger"""
        # Get the level from the incoming string specification
        try:
            level = getattr(logging, level.upper())
        except AttributeError as exc:
            logger.exception("Invalid logging level specified %s",
                             level.upper())
        else:
            # Set the existing handler and logger to this level
            self.handler.setLevel(level)
            if self.log.level > self.handler.level or self.log.level == logging.NOTSET:
                self.log.setLevel(self.handler.level)
Ejemplo n.º 16
0
class SiriusSpectrogramView(GraphicsLayoutWidget, PyDMWidget, PyDMColorMap,
                            ReadingOrder):
    """
    A SpectrogramView with support for Channels and more from PyDM.

    If there is no :attr:`channelWidth` it is possible to define the width of
    the image with the :attr:`width` property.

    The :attr:`normalizeData` property defines if the colors of the images are
    relative to the :attr:`colorMapMin` and :attr:`colorMapMax` property or to
    the minimum and maximum values of the image.

    Use the :attr:`newImageSignal` to hook up to a signal that is emitted when
    a new image is rendered in the widget.

    Parameters
    ----------
    parent : QWidget
        The parent widget for the Label
    image_channel : str, optional
        The channel to be used by the widget for the image data.
    xaxis_channel : str, optional
        The channel to be used by the widget to receive the image width
        (if ReadingOrder == Clike), and to set the xaxis values
    yaxis_channel : str, optional
        The channel to be used by the widget to receive the image width
        (if ReadingOrder == Fortranlike), and to set the yaxis values
    background : QColor, optional
        QColor to set the background color of the GraphicsView
    """

    Q_ENUMS(PyDMColorMap)
    Q_ENUMS(ReadingOrder)

    color_maps = cmaps

    def __init__(self,
                 parent=None,
                 image_channel=None,
                 xaxis_channel=None,
                 yaxis_channel=None,
                 roioffsetx_channel=None,
                 roioffsety_channel=None,
                 roiwidth_channel=None,
                 roiheight_channel=None,
                 title='',
                 background='w',
                 image_width=0,
                 image_height=0):
        """Initialize widget."""
        GraphicsLayoutWidget.__init__(self, parent)
        PyDMWidget.__init__(self)
        self.thread = None
        self._imagechannel = None
        self._xaxischannel = None
        self._yaxischannel = None
        self._roioffsetxchannel = None
        self._roioffsetychannel = None
        self._roiwidthchannel = None
        self._roiheightchannel = None
        self._channels = 7 * [
            None,
        ]
        self.image_waveform = np.zeros(0)
        self._image_width = image_width if not xaxis_channel else 0
        self._image_height = image_height if not yaxis_channel else 0
        self._roi_offsetx = 0
        self._roi_offsety = 0
        self._roi_width = 0
        self._roi_height = 0
        self._normalize_data = False
        self._auto_downsample = True
        self._last_yaxis_data = None
        self._last_xaxis_data = None
        self._auto_colorbar_lims = True
        self.format_tooltip = '{0:.4g}, {1:.4g}'

        # ViewBox and imageItem.
        self._view = ViewBox()
        self._image_item = ImageItem()
        self._view.addItem(self._image_item)

        # ROI
        self.ROICurve = PlotCurveItem([0, 0, 0, 0, 0], [0, 0, 0, 0, 0])
        self.ROIColor = QColor('red')
        pen = mkPen()
        pen.setColor(QColor('transparent'))
        pen.setWidth(1)
        self.ROICurve.setPen(pen)
        self._view.addItem(self.ROICurve)

        # Axis.
        self.xaxis = AxisItem('bottom')
        self.xaxis.setPen(QColor(0, 0, 0))
        if not xaxis_channel:
            self.xaxis.setVisible(False)
        self.yaxis = AxisItem('left')
        self.yaxis.setPen(QColor(0, 0, 0))
        if not yaxis_channel:
            self.yaxis.setVisible(False)

        # Colorbar legend.
        self.colorbar = _GradientLegend()

        # Title.
        start_row = 0
        if title:
            self.title = LabelItem(text=title, color='#000000')
            self.addItem(self.title, 0, 0, 1, 3)
            start_row = 1

        # Set layout.
        self.addItem(self._view, start_row, 1)
        self.addItem(self.yaxis, start_row, 0)
        self.addItem(self.colorbar, start_row, 2)
        self.addItem(self.xaxis, start_row + 1, 1)
        self.setBackground(background)
        self.ci.layout.setColumnSpacing(0, 0)
        self.ci.layout.setRowSpacing(start_row, 0)

        # Set color map limits.
        self.cm_min = 0.0
        self.cm_max = 255.0

        # Set default reading order of numpy array data to Clike.
        self._reading_order = ReadingOrder.Clike

        # Make a right-click menu for changing the color map.
        self.cm_group = QActionGroup(self)
        self.cmap_for_action = {}
        for cm in self.color_maps:
            action = self.cm_group.addAction(cmap_names[cm])
            action.setCheckable(True)
            self.cmap_for_action[action] = cm

        # Set the default colormap.
        self._cm_colors = None
        self.colorMap = PyDMColorMap.Inferno

        # Setup the redraw timer.
        self.needs_redraw = False
        self.redraw_timer = QTimer(self)
        self.redraw_timer.timeout.connect(self.redrawImage)
        self._redraw_rate = 30
        self.maxRedrawRate = self._redraw_rate
        self.newImageSignal = self._image_item.sigImageChanged

        # Set Channels.
        self.imageChannel = image_channel
        self.xAxisChannel = xaxis_channel
        self.yAxisChannel = yaxis_channel
        self.ROIOffsetXChannel = roioffsetx_channel
        self.ROIOffsetYChannel = roioffsety_channel
        self.ROIWidthChannel = roiwidth_channel
        self.ROIHeightChannel = roiheight_channel

    # --- Context menu ---
    def widget_ctx_menu(self):
        """
        Fetch the Widget specific context menu.

        It will be populated with additional tools by `assemble_tools_menu`.

        Returns
        -------
        QMenu or None
            If the return of this method is None a new QMenu will be created by
            `assemble_tools_menu`.
        """
        self.menu = ViewBoxMenu(self._view)
        cm_menu = self.menu.addMenu("Color Map")
        for act in self.cmap_for_action.keys():
            cm_menu.addAction(act)
        cm_menu.triggered.connect(self._changeColorMap)
        return self.menu

    # --- Colormap methods ---
    def _changeColorMap(self, action):
        """
        Method invoked by the colormap Action Menu.

        Changes the current colormap used to render the image.

        Parameters
        ----------
        action : QAction
        """
        self.colorMap = self.cmap_for_action[action]

    @Property(float)
    def colorMapMin(self):
        """
        Minimum value for the colormap.

        Returns
        -------
        float
        """
        return self.cm_min

    @colorMapMin.setter
    @Slot(float)
    def colorMapMin(self, new_min):
        """
        Set the minimum value for the colormap.

        Parameters
        ----------
        new_min : float
        """
        if self.cm_min != new_min:
            self.cm_min = new_min
            if self.cm_min > self.cm_max:
                self.cm_max = self.cm_min

    @Property(float)
    def colorMapMax(self):
        """
        Maximum value for the colormap.

        Returns
        -------
        float
        """
        return self.cm_max

    @colorMapMax.setter
    @Slot(float)
    def colorMapMax(self, new_max):
        """
        Set the maximum value for the colormap.

        Parameters
        ----------
        new_max : float
        """
        if self.cm_max != new_max:
            self.cm_max = new_max
            if self.cm_max < self.cm_min:
                self.cm_min = self.cm_max

    def setColorMapLimits(self, mn, mx):
        """
        Set the limit values for the colormap.

        Parameters
        ----------
        mn : int
            The lower limit
        mx : int
            The upper limit
        """
        if mn >= mx:
            return
        self.cm_max = mx
        self.cm_min = mn

    @Property(PyDMColorMap)
    def colorMap(self):
        """
        Return the color map used by the SpectrogramView.

        Returns
        -------
        PyDMColorMap
        """
        return self._colormap

    @colorMap.setter
    def colorMap(self, new_cmap):
        """
        Set the color map used by the SpectrogramView.

        Parameters
        -------
        new_cmap : PyDMColorMap
        """
        self._colormap = new_cmap
        self._cm_colors = self.color_maps[new_cmap]
        self.setColorMap()
        for action in self.cm_group.actions():
            if self.cmap_for_action[action] == self._colormap:
                action.setChecked(True)
            else:
                action.setChecked(False)

    def setColorMap(self, cmap=None):
        """
        Update the image colormap.

        Parameters
        ----------
        cmap : ColorMap
        """
        if not cmap:
            if not self._cm_colors.any():
                return
            # Take default values
            pos = np.linspace(0.0, 1.0, num=len(self._cm_colors))
            cmap = ColorMap(pos, self._cm_colors)
        self._view.setBackgroundColor(cmap.map(0))
        lut = cmap.getLookupTable(0.0, 1.0, alpha=False)
        self.colorbar.setIntColorScale(colors=lut)
        self._image_item.setLookupTable(lut)

    # --- Connection Slots ---
    @Slot(bool)
    def image_connection_state_changed(self, conn):
        """
        Callback invoked when the Image Channel connection state is changed.

        Parameters
        ----------
        conn : bool
            The new connection state.
        """
        if conn:
            self.redraw_timer.start()
        else:
            self.redraw_timer.stop()

    @Slot(bool)
    def yaxis_connection_state_changed(self, connected):
        """
        Callback invoked when the TimeAxis Channel connection state is changed.

        Parameters
        ----------
        conn : bool
            The new connection state.
        """
        self._timeaxis_connected = connected

    @Slot(bool)
    def roioffsetx_connection_state_changed(self, conn):
        """
        Run when the ROIOffsetX Channel connection state changes.

        Parameters
        ----------
        conn : bool
            The new connection state.

        """
        if not conn:
            self._roi_offsetx = 0

    @Slot(bool)
    def roioffsety_connection_state_changed(self, conn):
        """
        Run when the ROIOffsetY Channel connection state changes.

        Parameters
        ----------
        conn : bool
            The new connection state.

        """
        if not conn:
            self._roi_offsety = 0

    @Slot(bool)
    def roiwidth_connection_state_changed(self, conn):
        """
        Run when the ROIWidth Channel connection state changes.

        Parameters
        ----------
        conn : bool
            The new connection state.

        """
        if not conn:
            self._roi_width = 0

    @Slot(bool)
    def roiheight_connection_state_changed(self, conn):
        """
        Run when the ROIHeight Channel connection state changes.

        Parameters
        ----------
        conn : bool
            The new connection state.

        """
        if not conn:
            self._roi_height = 0

    # --- Value Slots ---
    @Slot(np.ndarray)
    def image_value_changed(self, new_image):
        """
        Callback invoked when the Image Channel value is changed.

        We try to do as little as possible in this method, because it
        gets called every time the image channel updates, which might
        be extremely often.  Basically just store the data, and set
        a flag requesting that the image be redrawn.

        Parameters
        ----------
        new_image : np.ndarray
            The new image data.  This can be a flat 1D array, or a 2D array.
        """
        if new_image is None or new_image.size == 0:
            return
        logging.debug("SpectrogramView Received New Image: Needs Redraw->True")
        self.image_waveform = new_image
        self.needs_redraw = True
        if not self._image_height and self._image_width:
            self._image_height = new_image.size / self._image_width
        elif not self._image_width and self._image_height:
            self._image_width = new_image.size / self._image_height

    @Slot(np.ndarray)
    @Slot(float)
    def xaxis_value_changed(self, new_array):
        """
        Callback invoked when the Image Width Channel value is changed.

        Parameters
        ----------
        new_array : np.ndarray
            The new x axis array
        """
        if new_array is None:
            return
        if isinstance(new_array, float):
            new_array = np.array([
                new_array,
            ])
        self._last_xaxis_data = new_array
        if self._reading_order == self.Clike:
            self._image_width = new_array.size
        else:
            self._image_height = new_array.size
        self.needs_redraw = True

    @Slot(np.ndarray)
    @Slot(float)
    def yaxis_value_changed(self, new_array):
        """
        Callback invoked when the TimeAxis Channel value is changed.

        Parameters
        ----------
        new_array : np.array
            The new y axis array
        """
        if new_array is None:
            return
        if isinstance(new_array, float):
            new_array = np.array([
                new_array,
            ])
        self._last_yaxis_data = new_array
        if self._reading_order == self.Fortranlike:
            self._image_width = new_array.size
        else:
            self._image_height = new_array.size
        self.needs_redraw = True

    @Slot(int)
    def roioffsetx_value_changed(self, new_offset):
        """
        Run when the ROIOffsetX Channel value changes.

        Parameters
        ----------
        new_offsetx : int
            The new image ROI horizontal offset

        """
        if new_offset is None:
            return
        self._roi_offsetx = new_offset
        self.redrawROI()

    @Slot(int)
    def roioffsety_value_changed(self, new_offset):
        """
        Run when the ROIOffsetY Channel value changes.

        Parameters
        ----------
        new_offsety : int
            The new image ROI vertical offset

        """
        if new_offset is None:
            return
        self._roi_offsety = new_offset
        self.redrawROI()

    @Slot(int)
    def roiwidth_value_changed(self, new_width):
        """
        Run when the ROIWidth Channel value changes.

        Parameters
        ----------
        new_width : int
            The new image ROI width

        """
        if new_width is None:
            return
        self._roi_width = int(new_width)
        self.redrawROI()

    @Slot(int)
    def roiheight_value_changed(self, new_height):
        """
        Run when the ROIHeight Channel value changes.

        Parameters
        ----------
        new_height : int
            The new image ROI height

        """
        if new_height is None:
            return
        self._roi_height = int(new_height)
        self.redrawROI()

    # --- Image update methods ---
    def process_image(self, image):
        """
        Boilerplate method.

        To be used by applications in order to add calculations and also modify
        the image before it is displayed at the widget.

        .. warning::
           This code runs in a separated QThread so it **MUST** not try to
           write to QWidgets.

        Parameters
        ----------
        image : np.ndarray
            The Image Data as a 2D numpy array

        Returns
        -------
        np.ndarray
            The Image Data as a 2D numpy array after processing.
        """
        return image

    def redrawImage(self):
        """
        Set the image data into the ImageItem, if needed.

        If necessary, reshape the image to 2D first.
        """
        if self.thread is not None and not self.thread.isFinished():
            logger.warning(
                "Image processing has taken longer than the refresh rate.")
            return
        self.thread = SpectrogramUpdateThread(self)
        self.thread.updateSignal.connect(self._updateDisplay)
        logging.debug("SpectrogramView RedrawImage Thread Launched")
        self.thread.start()

    @Slot(list)
    def _updateDisplay(self, data):
        logging.debug("SpectrogramView Update Display with new image")

        # Update axis
        if self._last_xaxis_data is not None:
            szx = self._last_xaxis_data.size
            xMin = self._last_xaxis_data.min()
            xMax = self._last_xaxis_data.max()
        else:
            szx = self.imageWidth if self.readingOrder == self.Clike \
                else self.imageHeight
            xMin = 0
            xMax = szx

        if self._last_yaxis_data is not None:
            szy = self._last_yaxis_data.size
            yMin = self._last_yaxis_data.min()
            yMax = self._last_yaxis_data.max()
        else:
            szy = self.imageHeight if self.readingOrder == self.Clike \
                else self.imageWidth
            yMin = 0
            yMax = szy

        self.xaxis.setRange(xMin, xMax)
        self.yaxis.setRange(yMin, yMax)
        self._view.setLimits(xMin=0,
                             xMax=szx,
                             yMin=0,
                             yMax=szy,
                             minXRange=szx,
                             maxXRange=szx,
                             minYRange=szy,
                             maxYRange=szy)

        # Update image
        if self.autoSetColorbarLims:
            self.colorbar.setLimits(data)
        mini, maxi = data[0], data[1]
        img = data[2]
        self._image_item.setLevels([mini, maxi])
        self._image_item.setImage(img,
                                  autoLevels=False,
                                  autoDownsample=self.autoDownsample)

    # ROI update methods
    def redrawROI(self):
        startx = self._roi_offsetx
        endx = self._roi_offsetx + self._roi_width
        starty = self._roi_offsety
        endy = self._roi_offsety + self._roi_height
        self.ROICurve.setData([startx, startx, endx, endx, startx],
                              [starty, endy, endy, starty, starty])

    def showROI(self, show):
        """Set ROI visibility."""
        pen = mkPen()
        if show:
            pen.setColor(self.ROIColor)
        else:
            pen.setColor(QColor('transparent'))
        self.ROICurve.setPen(pen)

    # --- Properties ---
    @Property(bool)
    def autoDownsample(self):
        """
        Return if we should or not apply the autoDownsample option.

        Return
        ------
        bool
        """
        return self._auto_downsample

    @autoDownsample.setter
    def autoDownsample(self, new_value):
        """
        Whether we should or not apply the autoDownsample option.

        Parameters
        ----------
        new_value: bool
        """
        if new_value != self._auto_downsample:
            self._auto_downsample = new_value

    @Property(bool)
    def autoSetColorbarLims(self):
        """
        Return if we should or not auto set colorbar limits.

        Return
        ------
        bool
        """
        return self._auto_colorbar_lims

    @autoSetColorbarLims.setter
    def autoSetColorbarLims(self, new_value):
        """
        Whether we should or not auto set colorbar limits.

        Parameters
        ----------
        new_value: bool
        """
        if new_value != self._auto_colorbar_lims:
            self._auto_colorbar_lims = new_value

    @Property(int)
    def imageWidth(self):
        """
        Return the width of the image.

        Return
        ------
        int
        """
        return self._image_width

    @imageWidth.setter
    def imageWidth(self, new_width):
        """
        Set the width of the image.

        Can be overridden by :attr:`xAxisChannel` and :attr:`yAxisChannel`.

        Parameters
        ----------
        new_width: int
        """
        boo = self._image_width != int(new_width)
        boo &= not self._xaxischannel
        boo &= not self._yaxischannel
        if boo:
            self._image_width = int(new_width)

    @Property(int)
    def imageHeight(self):
        """
        Return the height of the image.

        Return
        ------
        int
        """
        return self._image_height

    @Property(int)
    def ROIOffsetX(self):
        """
        Return the ROI offset in X axis in pixels.

        Return
        ------
        int
        """
        return self._roi_offsetx

    @ROIOffsetX.setter
    def ROIOffsetX(self, new_offset):
        """
        Set the ROI offset in X axis in pixels.

        Can be overridden by :attr:`ROIOffsetXChannel`.

        Parameters
        ----------
        new_offset: int
        """
        if new_offset is None:
            return
        boo = self._roi_offsetx != int(new_offset)
        boo &= not self._roioffsetxchannel
        if boo:
            self._roi_offsetx = int(new_offset)
            self.redrawROI()

    @Property(int)
    def ROIOffsetY(self):
        """
        Return the ROI offset in Y axis in pixels.

        Return
        ------
        int
        """
        return self._roi_offsety

    @ROIOffsetY.setter
    def ROIOffsetY(self, new_offset):
        """
        Set the ROI offset in Y axis in pixels.

        Can be overridden by :attr:`ROIOffsetYChannel`.

        Parameters
        ----------
        new_offset: int
        """
        if new_offset is None:
            return
        boo = self._roi_offsety != int(new_offset)
        boo &= not self._roioffsetychannel
        if boo:
            self._roi_offsety = int(new_offset)
            self.redrawROI()

    @Property(int)
    def ROIWidth(self):
        """
        Return the ROI width in pixels.

        Return
        ------
        int
        """
        return self._roi_width

    @ROIWidth.setter
    def ROIWidth(self, new_width):
        """
        Set the ROI width in pixels.

        Can be overridden by :attr:`ROIWidthChannel`.

        Parameters
        ----------
        new_width: int
        """
        if new_width is None:
            return
        boo = self._roi_width != int(new_width)
        boo &= not self._roiwidthchannel
        if boo:
            self._roi_width = int(new_width)
            self.redrawROI()

    @Property(int)
    def ROIHeight(self):
        """
        Return the ROI height in pixels.

        Return
        ------
        int
        """
        return self._roi_height

    @ROIHeight.setter
    def ROIHeight(self, new_height):
        """
        Set the ROI height in pixels.

        Can be overridden by :attr:`ROIHeightChannel`.

        Parameters
        ----------
        new_height: int
        """
        if new_height is None:
            return
        boo = self._roi_height != int(new_height)
        boo &= not self._roiheightchannel
        if boo:
            self._roi_height = int(new_height)
            self.redrawROI()

    @Property(bool)
    def normalizeData(self):
        """
        Return True if the colors are relative to data maximum and minimum.

        Returns
        -------
        bool
        """
        return self._normalize_data

    @normalizeData.setter
    @Slot(bool)
    def normalizeData(self, new_norm):
        """
        Define if the colors are relative to minimum and maximum of the data.

        Parameters
        ----------
        new_norm: bool
        """
        if self._normalize_data != new_norm:
            self._normalize_data = new_norm

    @Property(ReadingOrder)
    def readingOrder(self):
        """
        Return the reading order of the :attr:`imageChannel` array.

        Returns
        -------
        ReadingOrder
        """
        return self._reading_order

    @readingOrder.setter
    def readingOrder(self, order):
        """
        Set reading order of the :attr:`imageChannel` array.

        Parameters
        ----------
        order: ReadingOrder
        """
        if self._reading_order != order:
            self._reading_order = order

        if order == self.Clike:
            if self._last_xaxis_data is not None:
                self._image_width = self._last_xaxis_data.size
            if self._last_yaxis_data is not None:
                self._image_height = self._last_yaxis_data.size
        elif order == self.Fortranlike:
            if self._last_yaxis_data is not None:
                self._image_width = self._last_yaxis_data.size
            if self._last_xaxis_data is not None:
                self._image_height = self._last_xaxis_data.size

    @Property(int)
    def maxRedrawRate(self):
        """
        The maximum rate (in Hz) at which the plot will be redrawn.

        The plot will not be redrawn if there is not new data to draw.

        Returns
        -------
        int
        """
        return self._redraw_rate

    @maxRedrawRate.setter
    def maxRedrawRate(self, redraw_rate):
        """
        The maximum rate (in Hz) at which the plot will be redrawn.

        The plot will not be redrawn if there is not new data to draw.

        Parameters
        -------
        redraw_rate : int
        """
        self._redraw_rate = redraw_rate
        self.redraw_timer.setInterval(int((1.0 / self._redraw_rate) * 1000))

    # --- Events rederivations ---
    def keyPressEvent(self, ev):
        """Handle keypress events."""
        return

    def mouseMoveEvent(self, ev):
        if not self._image_item.width() or not self._image_item.height():
            super().mouseMoveEvent(ev)
            return
        pos = ev.pos()
        posaux = self._image_item.mapFromDevice(ev.pos())
        if posaux.x() < 0 or posaux.x() >= self._image_item.width() or \
                posaux.y() < 0 or posaux.y() >= self._image_item.height():
            super().mouseMoveEvent(ev)
            return

        pos_scene = self._view.mapSceneToView(pos)
        x = round(pos_scene.x())
        y = round(pos_scene.y())

        if self.xAxisChannel and self._last_xaxis_data is not None:
            maxx = len(self._last_xaxis_data) - 1
            x = x if x < maxx else maxx
            valx = self._last_xaxis_data[x]
        else:
            valx = x

        if self.yAxisChannel and self._last_yaxis_data is not None:
            maxy = len(self._last_yaxis_data) - 1
            y = y if y < maxy else maxy
            valy = self._last_yaxis_data[y]
        else:
            valy = y

        txt = self.format_tooltip.format(valx, valy)
        QToolTip.showText(self.mapToGlobal(pos), txt, self, self.geometry(),
                          5000)
        super().mouseMoveEvent(ev)

    # --- Channels ---
    @Property(str)
    def imageChannel(self):
        """
        The channel address in use for the image data .

        Returns
        -------
        str
            Channel address
        """
        if self._imagechannel:
            return str(self._imagechannel.address)
        else:
            return ''

    @imageChannel.setter
    def imageChannel(self, value):
        """
        The channel address in use for the image data .

        Parameters
        ----------
        value : str
            Channel address
        """
        if self._imagechannel != value:
            # Disconnect old channel
            if self._imagechannel:
                self._imagechannel.disconnect()
            # Create and connect new channel
            self._imagechannel = PyDMChannel(
                address=value,
                connection_slot=self.image_connection_state_changed,
                value_slot=self.image_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[0] = self._imagechannel
            self._imagechannel.connect()

    @Property(str)
    def xAxisChannel(self):
        """
        The channel address in use for the x-axis of image.

        Returns
        -------
        str
            Channel address
        """
        if self._xaxischannel:
            return str(self._xaxischannel.address)
        else:
            return ''

    @xAxisChannel.setter
    def xAxisChannel(self, value):
        """
        The channel address in use for the x-axis of image.

        Parameters
        ----------
        value : str
            Channel address
        """
        if self._xaxischannel != value:
            # Disconnect old channel
            if self._xaxischannel:
                self._xaxischannel.disconnect()
            # Create and connect new channel
            self._xaxischannel = PyDMChannel(
                address=value,
                connection_slot=self.connectionStateChanged,
                value_slot=self.xaxis_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[1] = self._xaxischannel
            self._xaxischannel.connect()

    @Property(str)
    def yAxisChannel(self):
        """
        The channel address in use for the time axis.

        Returns
        -------
        str
            Channel address
        """
        if self._yaxischannel:
            return str(self._yaxischannel.address)
        else:
            return ''

    @yAxisChannel.setter
    def yAxisChannel(self, value):
        """
        The channel address in use for the time axis.

        Parameters
        ----------
        value : str
            Channel address
        """
        if self._yaxischannel != value:
            # Disconnect old channel
            if self._yaxischannel:
                self._yaxischannel.disconnect()
            # Create and connect new channel
            self._yaxischannel = PyDMChannel(
                address=value,
                connection_slot=self.yaxis_connection_state_changed,
                value_slot=self.yaxis_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[2] = self._yaxischannel
            self._yaxischannel.connect()

    @Property(str)
    def ROIOffsetXChannel(self):
        """
        Return the channel address in use for the image ROI horizontal offset.

        Returns
        -------
        str
            Channel address

        """
        if self._roioffsetxchannel:
            return str(self._roioffsetxchannel.address)
        else:
            return ''

    @ROIOffsetXChannel.setter
    def ROIOffsetXChannel(self, value):
        """
        Return the channel address in use for the image ROI horizontal offset.

        Parameters
        ----------
        value : str
            Channel address

        """
        if self._roioffsetxchannel != value:
            # Disconnect old channel
            if self._roioffsetxchannel:
                self._roioffsetxchannel.disconnect()
            # Create and connect new channel
            self._roioffsetxchannel = PyDMChannel(
                address=value,
                connection_slot=self.roioffsetx_connection_state_changed,
                value_slot=self.roioffsetx_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[3] = self._roioffsetxchannel
            self._roioffsetxchannel.connect()

    @Property(str)
    def ROIOffsetYChannel(self):
        """
        Return the channel address in use for the image ROI vertical offset.

        Returns
        -------
        str
            Channel address

        """
        if self._roioffsetychannel:
            return str(self._roioffsetychannel.address)
        else:
            return ''

    @ROIOffsetYChannel.setter
    def ROIOffsetYChannel(self, value):
        """
        Return the channel address in use for the image ROI vertical offset.

        Parameters
        ----------
        value : str
            Channel address

        """
        if self._roioffsetychannel != value:
            # Disconnect old channel
            if self._roioffsetychannel:
                self._roioffsetychannel.disconnect()
            # Create and connect new channel
            self._roioffsetychannel = PyDMChannel(
                address=value,
                connection_slot=self.roioffsety_connection_state_changed,
                value_slot=self.roioffsety_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[4] = self._roioffsetychannel
            self._roioffsetychannel.connect()

    @Property(str)
    def ROIWidthChannel(self):
        """
        Return the channel address in use for the image ROI width.

        Returns
        -------
        str
            Channel address

        """
        if self._roiwidthchannel:
            return str(self._roiwidthchannel.address)
        else:
            return ''

    @ROIWidthChannel.setter
    def ROIWidthChannel(self, value):
        """
        Return the channel address in use for the image ROI width.

        Parameters
        ----------
        value : str
            Channel address

        """
        if self._roiwidthchannel != value:
            # Disconnect old channel
            if self._roiwidthchannel:
                self._roiwidthchannel.disconnect()
            # Create and connect new channel
            self._roiwidthchannel = PyDMChannel(
                address=value,
                connection_slot=self.roiwidth_connection_state_changed,
                value_slot=self.roiwidth_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[5] = self._roiwidthchannel
            self._roiwidthchannel.connect()

    @Property(str)
    def ROIHeightChannel(self):
        """
        Return the channel address in use for the image ROI height.

        Returns
        -------
        str
            Channel address

        """
        if self._roiheightchannel:
            return str(self._roiheightchannel.address)
        else:
            return ''

    @ROIHeightChannel.setter
    def ROIHeightChannel(self, value):
        """
        Return the channel address in use for the image ROI height.

        Parameters
        ----------
        value : str
            Channel address

        """
        if self._roiheightchannel != value:
            # Disconnect old channel
            if self._roiheightchannel:
                self._roiheightchannel.disconnect()
            # Create and connect new channel
            self._roiheightchannel = PyDMChannel(
                address=value,
                connection_slot=self.roiheight_connection_state_changed,
                value_slot=self.roiheight_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[6] = self._roiheightchannel
            self._roiheightchannel.connect()

    def channels(self):
        """
        Return the channels being used for this Widget.

        Returns
        -------
        channels : list
            List of PyDMChannel objects
        """
        return self._channels

    def channels_for_tools(self):
        """Return channels for tools."""
        return [self._imagechannel]
Ejemplo n.º 17
0
class TyphonDeviceDisplay(TyphonBase, TyphonDesignerMixin, DisplayTypes):
    """
    Main Panel display for a signal Ophyd Device

    This widget lays out all of the architecture for a single Ophyd display.
    The structure matches an ophyd Device, but for this specific instantation,
    one is not required to be given. There are four main panels available;
    :attr:`.read_panel`, :attr:`.config_panel`, :attr:`.method_panel`. These
    each provide a quick way to organize signals and methods by their
    importance to an operator. Because each panel can be hidden interactively,
    the screen works as both an expert and novice entry point for users. By
    default, widgets are hidden until contents are added. For instance, if you
    do not add any methods to the main panel it will not be visible.

    This contains the widgets for all of the root devices signals, any methods
    you would like to display, and an optional image. As with ``typhon``
    convention, the base initialization sets up the widgets and the
    ``.from_device`` class method will automatically populate them.

    Parameters
    ----------
    name: str, optional
        Name to displayed at the top of the panel

    image: str, optional
        Path to image file to displayed at the header

    parent: QWidget, optional
    """
    # Template types and defaults
    Q_ENUMS(DisplayTypes)
    TemplateEnum = DisplayTypes.to_enum()  # For convenience

    def __init__(self,  parent=None, **kwargs):
        # Intialize background variable
        self._forced_template = ''
        self._last_macros = dict()
        self._main_widget = None
        self._display_type = DisplayTypes.detailed_screen
        self.templates = dict((_typ.name, os.path.join(ui_dir,
                                                       _typ.name + '.ui'))
                              for _typ in self.TemplateEnum)
        # Set this to None first so we don't render
        super().__init__(parent=parent)
        # Initialize blank UI
        self.setLayout(QHBoxLayout())
        self.layout().setContentsMargins(0, 0, 0, 0)
        # Load template
        self.load_template()

    @property
    def current_template(self):
        """Current template being rendered"""
        if self._forced_template:
            return self._forced_template
        # Search in the last macros, maybe our device told us what to do
        template_key = self.TemplateEnum(self._display_type).name
        return self.templates[template_key]

    @Property(DisplayTypes)
    def display_type(self):
        return self._display_type

    @display_type.setter
    def display_type(self, value):
        # Store our new value
        if self._display_type != value:
            self._display_type = value
            self.load_template(macros=self._last_macros)

    @Property(str, designable=False)
    def device_class(self):
        """Full class with module name of loaded device"""
        if getattr(self, 'devices', []):
            device_class = self.devices[0].__class__
            return '.'.join((device_class.__module__,
                             device_class.__name__))
        return ''

    @Property(str, designable=False)
    def device_name(self):
        "Name of loaded device"
        if getattr(self, 'devices', []):
            return self.devices[0].name
        return ''

    def load_template(self, macros=None):
        """
        Load a new template

        Parameters
        ----------
        template: str
            Absolute path to template location

        macros: dict, optional
            Macro substitutions to be made in the file
        """
        # If we are not fully initialized yet do not try and add anything to
        # the layout. This will happen if the QApplication has a stylesheet
        # that forces a template, prior to the creation of this display
        if self.layout() is None:
            logger.debug("Widget not initialized, do not load template")
            return
        # Clear anything that exists in the current layout
        if self._main_widget:
            logger.debug("Clearing existing layout ...")
            clear_layout(self.layout())
        # Assemble our macros
        macros = macros or dict()
        for display_type in self.templates:
            value = macros.get(display_type)
            if value:
                logger.debug("Found new template %r for %r",
                             value, display_type)
                self.templates[display_type] = value
        # Store macros
        self._last_macros = macros
        try:
            self._main_widget = Display(ui_filename=self.current_template,
                                        macros=macros)
            # Add device to all children widgets
            if self.devices:
                for widget in self._main_widget.findChildren(TyphonBase):
                    widget.add_device(self.devices[0])
        except (FileNotFoundError, IsADirectoryError):
            logger.exception("Unable to load file %r", self.current_template)
            self._main_widget = QWidget()
        finally:
            self.layout().addWidget(self._main_widget)
            reload_widget_stylesheet(self)

    @Property(str)
    def force_template(self):
        """Force a specific template"""
        return self._forced_template

    @force_template.setter
    def force_template(self, value):
        if value != self._forced_template:
            self._forced_template = value
            self.load_template(macros=self._last_macros)

    def add_device(self, device, macros=None):
        """
        Add a Device and signals to the TyphonDeviceDisplay

        Parameters
        ----------
        device: ophyd.Device

        macros: dict, optional
            Set of macros to reload the template with. There are two fallback
            options attempted if no information is passed in. First, if the
            device has an ``md`` attribute after being loaded from a ``happi``
            database, that information will be passed in as macros. Finally, if
            no ``name`` field is passed in, we ensure the ``device.name`` is
            entered as well.
        """
        # We only allow one device at a time
        if self.devices:
            logger.debug("Removing devices %r", self.devices)
            self.devices.clear()
        # Add the device to the cache
        super().add_device(device)
        # Try and collect macros from device
        if not macros:
            if hasattr(device, 'md'):
                macros = device.md.post()
            else:
                macros = dict()
        # Ensure we at least pass in the device name
        if 'name' not in macros:
            macros['name'] = device.name
        # Reload template
        self.load_template(macros=macros)

    @classmethod
    def from_device(cls, device, template=None, macros=None):
        """
        Create a new TyphonDeviceDisplay from a Device

        Loads the signals in to the appropriate positions and sets the title to
        a cleaned version of the device name

        Parameters
        ----------
        device: ophyd.Device

        template :str, optional
            Set the ``display_template``
        macros: dict, optional
            Macro substitutions to be placed in template
        """
        display = cls()
        # Reset the template if provided
        if template:
            display.force_template = template
        # Add the device
        display.add_device(device, macros=macros)
        return display

    @Slot(object)
    def _tx(self, value):
        """Receive information from happi channel"""
        self.add_device(value['obj'], macros=value['md'])
Ejemplo n.º 18
0
class PyDMImageView(ImageView, PyDMWidget, PyDMColorMap, ReadingOrder):
    """
    A PyQtGraph ImageView with support for Channels and more from PyDM.

    If there is no :attr:`channelWidth` it is possible to define the width of
    the image with the :attr:`width` property.

    The :attr:`normalizeData` property defines if the colors of the images are
    relative to the :attr:`colorMapMin` and :attr:`colorMapMax` property or to
    the minimum and maximum values of the image.

    Use the :attr:`newImageSignal` to hook up to a signal that is emitted when a new
    image is rendered in the widget.

    Parameters
    ----------
    parent : QWidget
        The parent widget for the Label
    image_channel : str, optional
        The channel to be used by the widget for the image data.
    width_channel : str, optional
        The channel to be used by the widget to receive the image width
        information
    """

    ReadingOrder = ReadingOrder

    Q_ENUMS(ReadingOrder)
    Q_ENUMS(PyDMColorMap)

    color_maps = cmaps

    def __init__(self, parent=None, image_channel=None, width_channel=None):
        """Initialize widget."""
        # Set the default colormap.
        self._colormap = PyDMColorMap.Inferno
        self._cm_colors = None
        self._imagechannel = None
        self._widthchannel = None
        self.image_waveform = np.zeros(0)
        self._image_width = 0
        self._normalize_data = False
        self._auto_downsample = True
        self._show_axes = False

        # Set default reading order of numpy array data to Fortranlike.
        self._reading_order = ReadingOrder.Fortranlike

        self._redraw_rate = 30

        # Set color map limits.
        self.cm_min = 0.0
        self.cm_max = 255.0

        plot_item = PlotItem()
        ImageView.__init__(self, parent, view=plot_item)
        PyDMWidget.__init__(self)
        self._channels = [None, None]
        self.thread = None
        self.axes = dict({'t': None, "x": 0, "y": 1, "c": None})
        self.showAxes = self._show_axes

        # Hide some itens of the widget.
        self.ui.histogram.hide()
        self.getImageItem().sigImageChanged.disconnect(
            self.ui.histogram.imageChanged)
        self.ui.roiBtn.hide()
        self.ui.menuBtn.hide()

        # Make a right-click menu for changing the color map.
        self.cm_group = QActionGroup(self)
        self.cmap_for_action = {}
        for cm in self.color_maps:
            action = self.cm_group.addAction(cmap_names[cm])
            action.setCheckable(True)
            self.cmap_for_action[action] = cm

        self.colorMap = self._colormap

        # Setup the redraw timer.
        self.needs_redraw = False
        self.redraw_timer = QTimer(self)
        self.redraw_timer.timeout.connect(self.redrawImage)
        self.maxRedrawRate = self._redraw_rate
        self.newImageSignal = self.getImageItem().sigImageChanged
        # Set live channels if requested on initialization
        if image_channel:
            self.imageChannel = image_channel or ''
        if width_channel:
            self.widthChannel = width_channel or ''

    @Property(str, designable=False)
    def channel(self):
        return

    @channel.setter
    def channel(self, ch):
        if not ch:
            return
        logger.info("Use the imageChannel property with the ImageView widget.")
        return

    def widget_ctx_menu(self):
        """
        Fetch the Widget specific context menu.

        It will be populated with additional tools by `assemble_tools_menu`.

        Returns
        -------
        QMenu or None
            If the return of this method is None a new QMenu will be created by
            `assemble_tools_menu`.
        """
        self.menu = ViewBoxMenu(self.getView().getViewBox())
        cm_menu = self.menu.addMenu("Color Map")
        for act in self.cmap_for_action.keys():
            cm_menu.addAction(act)
        cm_menu.triggered.connect(self._changeColorMap)
        return self.menu

    def _changeColorMap(self, action):
        """
        Method invoked by the colormap Action Menu.

        Changes the current colormap used to render the image.

        Parameters
        ----------
        action : QAction
        """
        self.colorMap = self.cmap_for_action[action]

    @Property(float)
    def colorMapMin(self):
        """
        Minimum value for the colormap.

        Returns
        -------
        float
        """
        return self.cm_min

    @colorMapMin.setter
    @Slot(float)
    def colorMapMin(self, new_min):
        """
        Set the minimum value for the colormap.

        Parameters
        ----------
        new_min : float
        """
        if self.cm_min != new_min:
            self.cm_min = new_min
            if self.cm_min > self.cm_max:
                self.cm_max = self.cm_min

    @Property(float)
    def colorMapMax(self):
        """
        Maximum value for the colormap.

        Returns
        -------
        float
        """
        return self.cm_max

    @colorMapMax.setter
    @Slot(float)
    def colorMapMax(self, new_max):
        """
        Set the maximum value for the colormap.

        Parameters
        ----------
        new_max : float
        """
        if self.cm_max != new_max:
            self.cm_max = new_max
            if self.cm_max < self.cm_min:
                self.cm_min = self.cm_max

    def setColorMapLimits(self, mn, mx):
        """
        Set the limit values for the colormap.

        Parameters
        ----------
        mn : int
            The lower limit
        mx : int
            The upper limit
        """
        if mn >= mx:
            return
        self.cm_max = mx
        self.cm_min = mn

    @Property(PyDMColorMap)
    def colorMap(self):
        """
        Return the color map used by the ImageView.

        Returns
        -------
        PyDMColorMap
        """
        return self._colormap

    @colorMap.setter
    def colorMap(self, new_cmap):
        """
        Set the color map used by the ImageView.

        Parameters
        -------
        new_cmap : PyDMColorMap
        """
        self._colormap = new_cmap
        self._cm_colors = self.color_maps[new_cmap]
        self.setColorMap()
        for action in self.cm_group.actions():
            if self.cmap_for_action[action] == self._colormap:
                action.setChecked(True)
            else:
                action.setChecked(False)

    def setColorMap(self, cmap=None):
        """
        Update the image colormap.

        Parameters
        ----------
        cmap : ColorMap
        """
        if not cmap:
            if not self._cm_colors.any():
                return
            # Take default values
            pos = np.linspace(0.0, 1.0, num=len(self._cm_colors))
            cmap = ColorMap(pos, self._cm_colors)
        self.getView().getViewBox().setBackgroundColor(cmap.map(0))
        lut = cmap.getLookupTable(0.0, 1.0, alpha=False)
        self.getImageItem().setLookupTable(lut)

    @Slot(bool)
    def image_connection_state_changed(self, conn):
        """
        Callback invoked when the Image Channel connection state is changed.

        Parameters
        ----------
        conn : bool
            The new connection state.
        """
        if conn:
            self.redraw_timer.start()
        else:
            self.redraw_timer.stop()

    @Slot(np.ndarray)
    def image_value_changed(self, new_image):
        """
        Callback invoked when the Image Channel value is changed.

        We try to do as little as possible in this method, because it
        gets called every time the image channel updates, which might
        be extremely often.  Basically just store the data, and set
        a flag requesting that the image be redrawn.

        Parameters
        ----------
        new_image : np.ndarray
            The new image data.  This can be a flat 1D array, or a 2D array.
        """
        if new_image is None or new_image.size == 0:
            return
        logging.debug("ImageView Received New Image - Needs Redraw -> True")
        self.image_waveform = new_image
        self.needs_redraw = True

    @Slot(int)
    def image_width_changed(self, new_width):
        """
        Callback invoked when the Image Width Channel value is changed.

        Parameters
        ----------
        new_width : int
            The new image width
        """
        if new_width is None:
            return
        self._image_width = int(new_width)

    def process_image(self, image):
        """
        Boilerplate method to be used by applications in order to
        add calculations and also modify the image before it is
        displayed at the widget.

        .. warning::
           This code runs in a separated QThread so it **MUST** not try to write
           to QWidgets.

        Parameters
        ----------
        image : np.ndarray
            The Image Data as a 2D numpy array

        Returns
        -------
        np.ndarray
            The Image Data as a 2D numpy array after processing.
        """
        return image

    def redrawImage(self):
        """
        Set the image data into the ImageItem, if needed.

        If necessary, reshape the image to 2D first.
        """
        if self.thread is not None and not self.thread.isFinished():
            logger.warning(
                "Image processing has taken longer than the refresh rate.")
            return
        self.thread = ImageUpdateThread(self)
        self.thread.updateSignal.connect(self.__updateDisplay)
        logging.debug("ImageView RedrawImage Thread Launched")
        self.thread.start()

    @Slot(list)
    def __updateDisplay(self, data):
        logging.debug("ImageView Update Display with new image")
        mini, maxi = data[0], data[1]
        img = data[2]
        self.getImageItem().setLevels([mini, maxi])
        self.getImageItem().setImage(
            img,
            autoLevels=False,
            autoDownsample=self.autoDownsample)

    @Property(bool)
    def autoDownsample(self):
        """
        Return if we should or not apply the
        autoDownsample option to PyQtGraph.

        Return
        ------
        bool
        """
        return self._auto_downsample

    @autoDownsample.setter
    def autoDownsample(self, new_value):
        """
        Whether we should or not apply the
        autoDownsample option to PyQtGraph.

        Parameters
        ----------
        new_value: bool
        """
        if new_value != self._auto_downsample:
            self._auto_downsample = new_value

    @Property(int)
    def imageWidth(self):
        """
        Return the width of the image.

        Return
        ------
        int
        """
        return self._image_width

    @imageWidth.setter
    def imageWidth(self, new_width):
        """
        Set the width of the image.

        Can be overridden by :attr:`widthChannel`.

        Parameters
        ----------
        new_width: int
        """
        if (self._image_width != int(new_width) and
                (self._widthchannel is None or self._widthchannel == '')):
            self._image_width = int(new_width)

    @Property(bool)
    def normalizeData(self):
        """
        Return True if the colors are relative to data maximum and minimum.

        Returns
        -------
        bool
        """
        return self._normalize_data

    @normalizeData.setter
    @Slot(bool)
    def normalizeData(self, new_norm):
        """
        Define if the colors are relative to minimum and maximum of the data.

        Parameters
        ----------
        new_norm: bool
        """
        if self._normalize_data != new_norm:
            self._normalize_data = new_norm

    @Property(ReadingOrder)
    def readingOrder(self):
        """
        Return the reading order of the :attr:`imageChannel` array.

        Returns
        -------
        ReadingOrder
        """
        return self._reading_order

    @readingOrder.setter
    def readingOrder(self, new_order):
        """
        Set reading order of the :attr:`imageChannel` array.

        Parameters
        ----------
        new_order: ReadingOrder
        """
        if self._reading_order != new_order:
            self._reading_order = new_order

    def keyPressEvent(self, ev):
        """Handle keypress events."""
        return

    @Property(str)
    def imageChannel(self):
        """
        The channel address in use for the image data .

        Returns
        -------
        str
            Channel address
        """
        if self._imagechannel:
            return str(self._imagechannel.address)
        else:
            return ''

    @imageChannel.setter
    def imageChannel(self, value):
        """
        The channel address in use for the image data .

        Parameters
        ----------
        value : str
            Channel address
        """
        if self._imagechannel != value:
            # Disconnect old channel
            if self._imagechannel:
                self._imagechannel.disconnect()
            # Create and connect new channel
            self._imagechannel = PyDMChannel(
                            address=value,
                            connection_slot=self.image_connection_state_changed,
                            value_slot=self.image_value_changed,
                            severity_slot=self.alarmSeverityChanged)
            self._channels[0] = self._imagechannel
            self._imagechannel.connect()

    @Property(str)
    def widthChannel(self):
        """
        The channel address in use for the image width .

        Returns
        -------
        str
            Channel address
        """
        if self._widthchannel:
            return str(self._widthchannel.address)
        else:
            return ''

    @widthChannel.setter
    def widthChannel(self, value):
        """
        The channel address in use for the image width .

        Parameters
        ----------
        value : str
            Channel address
        """
        if self._widthchannel != value:
            # Disconnect old channel
            if self._widthchannel:
                self._widthchannel.disconnect()
            # Create and connect new channel
            self._widthchannel = PyDMChannel(
                            address=value,
                            connection_slot=self.connectionStateChanged,
                            value_slot=self.image_width_changed,
                            severity_slot=self.alarmSeverityChanged)
            self._channels[1] = self._widthchannel
            self._widthchannel.connect()


    def channels(self):
        """
        Return the channels being used for this Widget.

        Returns
        -------
        channels : list
            List of PyDMChannel objects
        """
        return self._channels

    def channels_for_tools(self):
        """Return channels for tools."""
        return [self._imagechannel]

    @Property(int)
    def maxRedrawRate(self):
        """
        The maximum rate (in Hz) at which the plot will be redrawn.

        The plot will not be redrawn if there is not new data to draw.

        Returns
        -------
        int
        """
        return self._redraw_rate

    @maxRedrawRate.setter
    def maxRedrawRate(self, redraw_rate):
        """
        The maximum rate (in Hz) at which the plot will be redrawn.

        The plot will not be redrawn if there is not new data to draw.

        Parameters
        -------
        redraw_rate : int
        """
        self._redraw_rate = redraw_rate
        self.redraw_timer.setInterval(int((1.0 / self._redraw_rate) * 1000))

    @Property(bool)
    def showAxes(self):
        """
        Whether or not axes should be shown on the widget.
        """
        return self._show_axes

    @showAxes.setter
    def showAxes(self, show):
        self._show_axes = show
        self.getView().showAxis('left', show=show)
        self.getView().showAxis('bottom', show=show)

    @Property(float)
    def scaleXAxis(self):
        """
        Sets the scale for the X Axis.

        For example, if your image has 100 pixels per millimeter, you can set
        xAxisScale to 1/100 = 0.01 to make the X Axis report in millimeter units.
        """
        # protect against access to not yet initialized view
        if hasattr(self, 'view'):
            return self.getView().getAxis('bottom').scale
        return None

    @scaleXAxis.setter
    def scaleXAxis(self, new_scale):
        self.getView().getAxis('bottom').setScale(new_scale)

    @Property(float)
    def scaleYAxis(self):
        """
        Sets the scale for the Y Axis.

        For example, if your image has 100 pixels per millimeter, you can set
        yAxisScale to 1/100 = 0.01 to make the Y Axis report in millimeter units.
        """
        # protect against access to not yet initialized view
        if hasattr(self, 'view'):
            return self.getView().getAxis('left').scale
        return None

    @scaleYAxis.setter
    def scaleYAxis(self, new_scale):
        self.getView().getAxis('left').setScale(new_scale)
Ejemplo n.º 19
0
class PyDMLabel(QLabel, TextFormatter, PyDMWidget, DisplayFormat):
    Q_ENUMS(DisplayFormat)
    DisplayFormat = DisplayFormat
    """
    A QLabel with support for setting the text via a PyDM Channel, or
    through the PyDM Rules system.
    
    Note: If a PyDMLabel is configured to use a Channel, and also with a rule
    which changes the 'Text' property, the behavior is undefined.  Use either
    the Channel *or* a text rule, but not both.

    Parameters
    ----------
    parent : QWidget
        The parent widget for the Label
    init_channel : str, optional
        The channel to be used by the widget.
    """

    def __init__(self, parent=None, init_channel=None):
        QLabel.__init__(self, parent)
        PyDMWidget.__init__(self, init_channel=init_channel)
        if 'Text' not in PyDMLabel.RULE_PROPERTIES:
            PyDMLabel.RULE_PROPERTIES = PyDMWidget.RULE_PROPERTIES.copy()
            PyDMLabel.RULE_PROPERTIES.update(
                {'Text': ['value_changed', str]})
        self.app = QApplication.instance()
        self.setTextFormat(Qt.PlainText)
        self.setTextInteractionFlags(Qt.NoTextInteraction)
        self.setText("PyDMLabel")
        self._display_format_type = self.DisplayFormat.Default
        self._string_encoding = "utf_8"
        if is_pydm_app():
            self._string_encoding = self.app.get_string_encoding()

    @Property(DisplayFormat)
    def displayFormat(self):
        return self._display_format_type

    @displayFormat.setter
    def displayFormat(self, new_type):
        if self._display_format_type == new_type:
            return
        self._display_format_type = new_type
        if not is_qt_designer() or config.DESIGNER_ONLINE:
            # Trigger the update of display format
            self.value_changed(self.value)

    def value_changed(self, new_value):
        """
        Callback invoked when the Channel value is changed.
        Sets the value of new_value accordingly at the Label.

        Parameters
        ----------
        new_value : str, int, float, bool or np.ndarray
            The new value from the channel. The type depends on the channel.
        """
        super(PyDMLabel, self).value_changed(new_value)
        new_value = parse_value_for_display(value=new_value, precision=self.precision,
                                            display_format_type=self._display_format_type,
                                            string_encoding=self._string_encoding,
                                            widget=self)
        # If the value is a string, just display it as-is, no formatting
        # needed.
        if isinstance(new_value, str):
            if self._show_units and self._unit != "":
                new_value = "{} {}".format(new_value, self._unit)
            self.setText(new_value)
            return
        # If the value is an enum, display the appropriate enum string for
        # the value.
        if self.enum_strings is not None and isinstance(new_value, int):
            try:
                self.setText(self.enum_strings[new_value])
            except IndexError:
                self.setText("**INVALID**")
            return
        # If the value is a number (float or int), display it using a
        # format string if necessary.
        if isinstance(new_value, (int, float)):
            self.setText(self.format_string.format(new_value))
            return
        # If you made it this far, just turn whatever the heck the value
        # is into a string and display it.
        self.setText(str(new_value))
Ejemplo n.º 20
0
class PCDSSymbolBase(QWidget, PyDMPrimitiveWidget, ContentLocation):
    """
    Base class to be used for all PCDS Symbols.

    Parameters
    ----------
    parent : QWidget
        The parent widget for this symbol.
    """
    EXPERT_OPHYD_CLASS = ""

    Q_ENUMS(ContentLocation)
    ContentLocation = ContentLocation

    def __init__(self, parent=None, **kwargs):
        super(PCDSSymbolBase, self).__init__(parent=parent, **kwargs)
        self._expert_display = None
        self.interlock = None
        self._channels_prefix = None
        self._rotate_icon = False

        self._show_icon = True
        self._show_status_tooltip = True
        self._icon_size = -1
        self._icon = None

        self._expert_ophyd_class = self.EXPERT_OPHYD_CLASS or ""

        self.interlock = QFrame(self)
        self.interlock.setObjectName("interlock")
        self.interlock.setSizePolicy(QSizePolicy.Expanding,
                                     QSizePolicy.Expanding)

        self.controls_frame = QFrame(self)
        self.controls_frame.setObjectName("controls")
        self.controls_frame.setSizePolicy(QSizePolicy.Maximum,
                                          QSizePolicy.Maximum)
        self.setLayout(QVBoxLayout())
        self.layout().setSpacing(0)
        self.layout().setContentsMargins(0, 0, 0, 0)
        self.layout().addWidget(self.interlock)
        if not hasattr(self, '_controls_location'):
            self._controls_location = ContentLocation.Bottom
        self.setup_icon()
        self.assemble_layout()
        self.update_status_tooltip()

    def sizeHint(self):
        """
        Suggested initial size for the widget.

        Returns
        -------
        size : QSize
        """
        return QSize(200, 200)

    @Property(ContentLocation)
    def controlsLocation(self):
        """
        Property controlling where the controls frame will be displayed.

        Returns
        -------
        location : ContentLocation
        """
        return self._controls_location

    @controlsLocation.setter
    def controlsLocation(self, location):
        """
        Property controlling where the controls frame will be displayed.

        Parameters
        ----------
        location : ContentLocation
        """
        if location != self._controls_location:
            self._controls_location = location
            self.assemble_layout()

    @Property(str)
    def channelsPrefix(self):
        """
        The prefix to be used when composing the channels for each of the
        elements of the symbol widget.

        The prefix must include the protocol as well. E.g.: ca://VALVE

        Returns
        -------
        str
        """
        return self._channels_prefix

    @channelsPrefix.setter
    def channelsPrefix(self, prefix):
        """
        The prefix to be used when composing the channels for each of the
        elements of the symbol widget.

        The prefix must include the protocol as well. E.g.: ca://VALVE

        Parameters
        ----------
        prefix : str
            The prefix to be used for the channels.
        """
        if prefix != self._channels_prefix:
            self._channels_prefix = prefix
            self.destroy_channels()
            self.create_channels()

    @property
    def icon(self):
        return self._icon

    @icon.setter
    def icon(self, icon):
        if self._icon != icon:
            self._icon = icon
            self.setup_icon()
            self.iconSize = self.iconSize
            self.assemble_layout()

    @Property(bool)
    def showIcon(self):
        """
        Whether or not to show the symbol icon when rendering the widget.

        Returns
        -------
        bool
        """
        return self._show_icon

    @showIcon.setter
    def showIcon(self, value):
        """
        Whether or not to show the symbol icon when rendering the widget.

        Parameters
        ----------
        value : bool
            Shows the Icon if True, hides it otherwise.
        """
        if value != self._show_icon:
            self._show_icon = value
            if self.icon:
                self.icon.setVisible(self._show_icon)
            self.assemble_layout()

    @Property(bool)
    def showStatusTooltip(self):
        """
        Whether or not to show a detailed status tooltip including the state
        of the widget components such as Interlock, Error, State and more.

        Returns
        -------
        bool
        """
        return self._show_status_tooltip

    @showStatusTooltip.setter
    def showStatusTooltip(self, value):
        """
        Whether or not to show a detailed status tooltip including the state
        of the widget components such as Interlock, Error, State and more.

        Parameters
        ----------
        value : bool
            Displays the tooltip if True.

        """
        if value != self._show_status_tooltip:
            self._show_status_tooltip = value

    @Property(int)
    def iconSize(self):
        """
        The size of the icon in pixels.

        Returns
        -------
        int
        """
        return self._icon_size

    @iconSize.setter
    def iconSize(self, size):
        """
        The size of the icon in pixels.

        Parameters
        ----------
        size : int
            A value > 0 will constrain the size of the icon to the defined
            value.
            If the value is <= 0 it will expand to fill the space available.

        """
        if not self.icon:
            return

        if size <= 0:
            size = - 1
            min_size = 1
            max_size = 999999
            self.icon.setSizePolicy(QSizePolicy.Expanding,
                                    QSizePolicy.Expanding)
            self.icon.setMinimumSize(min_size, min_size)
            self.icon.setMaximumSize(max_size, max_size)

        else:
            self.icon.setFixedSize(size, size)
            self.icon.setSizePolicy(QSizePolicy.Fixed,
                                    QSizePolicy.Fixed)

        self._icon_size = size
        self.icon.update()

    @Property(bool)
    def rotateIcon(self):
        """
        Rotate the icon 90 degrees clockwise

        Returns
        -------
        rotate : bool
        """
        return self._rotate_icon

    @rotateIcon.setter
    def rotateIcon(self, rotate):
        """
        Rotate the icon 90 degrees clockwise

        Parameters
        ----------
        rotate : bool
        """
        self._rotate_icon = rotate
        angle = 90 if self._rotate_icon else 0
        if self.icon:
            self.icon.rotation = angle

    @Property(str)
    def expertOphydClass(self):
        """
        The full qualified name of the Ophyd class to be used for the Expert
        screen to be generated using Typhos.

        Returns
        -------
        str
        """
        klass = self._expert_ophyd_class
        if isinstance(klass, type):
            return f"{klass.__module__}.{klass.__name__}"
        return klass

    @expertOphydClass.setter
    def expertOphydClass(self, klass):
        """
        The full qualified name of the Ophyd class to be used for the Expert
        screen to be generated using Typhos.

        Parameters
        ----------
        klass : bool
        """
        if self.ophydClass != klass:
            self._expert_ophyd_class = klass

    def paintEvent(self, evt):
        """
        Paint events are sent to widgets that need to update themselves,
        for instance when part of a widget is exposed because a covering
        widget was moved.

        This method handles the painting with parameters from the stylesheet.

        Parameters
        ----------
        evt : QPaintEvent
        """
        painter = QPainter(self)
        opt = QStyleOption()
        opt.initFrom(self)
        self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self)
        painter.setRenderHint(QPainter.Antialiasing)
        super(PCDSSymbolBase, self).paintEvent(evt)

    def clear(self):
        """
        Remove all inner widgets from the interlock frame layout.
        """
        if not self.interlock:
            return
        layout = self.interlock.layout()
        if layout is None:
            return
        while layout.count() != 0:
            item = layout.itemAt(0)
            if item is not None:
                layout.removeItem(item)

        # Trick to remove the existing layout by re-parenting it in an
        # empty widget.
        QWidget().setLayout(self.interlock.layout())

    def assemble_layout(self):
        """
        Assembles the widget's inner layout depending on the ContentLocation
        and other configurations set.

        """
        if not self.interlock:
            return
        self.clear()

        # (Layout, items)
        widget_map = {
            ContentLocation.Hidden: (QVBoxLayout,
                                     [self.icon]),
            ContentLocation.Top: (QVBoxLayout,
                                  [self.controls_frame,
                                   self.icon]),
            ContentLocation.Bottom: (QVBoxLayout,
                                     [self.icon,
                                      self.controls_frame]),
            ContentLocation.Left: (QHBoxLayout,
                                   [self.controls_frame,
                                    self.icon]),
            ContentLocation.Right: (QHBoxLayout,
                                    [self.icon,
                                     self.controls_frame]),
        }

        layout = widget_map[self._controls_location][0]()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)

        self.interlock.setLayout(layout)

        widgets = widget_map[self._controls_location][1]

        # Hide the controls box if they are not going to be included in layout
        controls_visible = self._controls_location != ContentLocation.Hidden
        self.controls_frame.setVisible(controls_visible)

        for widget in widgets:
            if widget is None:
                continue
            # Each widget is in a separate layout to help with expansion rules
            box_layout = QHBoxLayout()
            box_layout.addWidget(widget)
            layout.addLayout(box_layout)

    def setup_icon(self):
        if not self.icon:
            return
        self.icon.setMinimumSize(16, 16)
        self.icon.setSizePolicy(QSizePolicy.Expanding,
                                QSizePolicy.Expanding)
        self.icon.setVisible(self._show_icon)
        self.iconSize = 32
        if hasattr(self.icon, 'clicked'):
            self.icon.clicked.connect(self._handle_icon_click)

    def _handle_icon_click(self):
        if not self.channelsPrefix:
            logger.error('No channel prefix specified.'
                         'Cannot proceed with opening expert screen for %s.',
                         self.__class__.__name__)
            return

        if self._expert_display is not None:
            logger.debug('Bringing existing display to front.')
            self._expert_display.show()
            self._expert_display.raise_()
            return

        prefix = remove_protocol(self.channelsPrefix)
        klass = self.expertOphydClass
        if not klass:
            logger.error('No ophydClass specified for pcdswidgets %s',
                         self.__class__.__name__)
            return
        name = prefix.replace(':', '_')

        try:
            import typhos
        except ImportError:
            logger.error('Typhos not installed. Cannot create display.')
            return

        kwargs = {"name": name, "prefix": prefix}
        display = typhos.TyphosDeviceDisplay.from_class(klass, **kwargs)
        self._expert_display = display
        display.destroyed.connect(self._cleanup_expert_display)

        if display:
            display.show()

    def _cleanup_expert_display(self, *args, **kwargs):
        self._expert_display = None

    def status_tooltip(self):
        """
        Assemble and returns the status tooltip for the symbol.

        Returns
        -------
        str
        """
        status = ""
        if hasattr(self, 'NAME'):
            status = self.NAME
        if status:
            status += os.linesep
        status += "PV Prefix: {}".format(self.channelsPrefix)
        return status

    def destroy_channels(self):
        """
        Method invoked when the channels associated with the widget must be
        destroyed.
        """
        for v in self.__dict__.values():
            if isinstance(v, PyDMChannel):
                v.disconnect()

    def create_channels(self):
        """
        Method invoked when the channels associated with the widget must be
        created.
        This method must be implemented on the subclasses and mixins as needed.
        By default this method does nothing.
        """
        pass

    def update_stylesheet(self):
        """
        Invoke the stylesheet update process on the widget and child widgets to
        reflect changes on the properties.
        """
        refresh_style(self)

    def update_status_tooltip(self):
        """
        Set the tooltip on the symbol to the content of status_tooltip.
        """
        self.setToolTip(self.status_tooltip())
Ejemplo n.º 21
0
class PyDMTemplateRepeater(QFrame, PyDMPrimitiveWidget, LayoutType):
    """
    PyDMTemplateRepeater takes a .ui file with macro variables as a template, and a JSON
    file (or a list of dictionaries) with a list of values to use to fill in
    the macro variables, then creates a layout with one instance of the
    template for each item in the list.

    It can be very convenient if you have displays that repeat the same set of
    widgets over and over - for instance, if you have a standard set of
    controls for a magnet, and want to build a display with a list of controls
    for every magnet, the Template Repeater lets you do that with a minimum
    amount of work: just build a template for a single magnet, and a JSON list
    with the data that describes all of the magnets.

    Parameters
    ----------
    parent : optional
        The parent of this widget.
    """
    Q_ENUMS(LayoutType)
    LayoutType = LayoutType

    def __init__(self, parent=None):
        QFrame.__init__(self, parent)
        PyDMPrimitiveWidget.__init__(self)
        self._template_filename = ""
        self._count_shown_in_designer = 1
        self._data_source = ""
        self._data = []
        self._cached_template = None
        self._layout_type = LayoutType.Vertical
        self.app = QApplication.instance()
        self.rebuild()

    @Property(LayoutType)
    def layoutType(self):
        """
        The layout type to use.

        Returns
        -------
        LayoutType
        """
        return self._layout_type

    @layoutType.setter
    def layoutType(self, new_type):
        """
        The layout type to use.
        Options are:
        - **Vertical**: Instances of the template are laid out vertically, in rows.
        - **Horizontal**: Instances of the template are laid out horizontally, in columns.
        - **Flow**: Instances of the template are laid out horizontally until they reach the edge of the template, at which point they "wrap" into a new row.

        Parameters
        ----------
        new_type : LayoutType
        """
        if new_type != self._layout_type:
            self._layout_type = new_type
            self.rebuild()

    @Property(int)
    def countShownInDesigner(self):
        """
        The number of instances to show in Qt Designer.  This property has no
        effect outside of Designer.
        
        Returns
        -------
        int
        """
        return self._count_shown_in_designer

    @countShownInDesigner.setter
    def countShownInDesigner(self, new_count):
        """
        The number of instances to show in Qt Designer.  This property has no
        effect outside of Designer.
        
        Parameters
        ----------
        new_count : int
        """
        if not is_qt_designer():
            return
        try:
            new_count = int(new_count)
        except ValueError:
            logger.exception(
                "Couldn't convert {} to integer.".format(new_count))
            return
        new_count = max(new_count, 0)
        if new_count != self._count_shown_in_designer:
            self._count_shown_in_designer = new_count
            self.rebuild()

    @Property(str)
    def templateFilename(self):
        """
        The path to the .ui file to use as a template.
        
        Returns
        -------
        str
        """
        return self._template_filename

    @templateFilename.setter
    def templateFilename(self, new_filename):
        """
        The path to the .ui file to use as a template.
        
        Parameters
        ----------
        new_filename : str
        """
        if new_filename != self._template_filename:
            self._template_filename = new_filename
            self._cached_template = None
            if self._template_filename:
                self.rebuild()
            else:
                self.clear()

    @Property(str)
    def dataSource(self):
        """
        The path to the JSON file to fill in each instance of the template.
        
        Returns
        -------
        str
        """
        return self._data_source

    @dataSource.setter
    def dataSource(self, data_source):
        """
        Sets the path to the JSON file to fill in each instance of the template.
        
        For example, if you build a template that contains two macro variables,
        ${NAME} and ${UNIT}, your JSON file should be a list of dictionaries,
        each with keys for NAME and UNIT, like this:
        
        [{"NAME": "First Device", "UNIT": 1}, {"NAME": "Second Device", "UNIT": 2}]
        
        Parameters
        -------
        data_source : str
        """
        if data_source != self._data_source:
            self._data_source = data_source
            if self._data_source:
                try:
                    # Expand user (~ or ~user) and environment variables.
                    fname = os.path.expanduser(
                        os.path.expandvars(self._data_source))
                    if is_pydm_app():
                        # If we're running this inside the PyDM app, we can
                        # make sure the path is relative to the currently loaded
                        # display (.ui or .py file).
                        fname = self.app.get_path(fname)
                    with open(fname) as f:
                        self.data = json.load(f)
                except IOError as e:
                    self.data = []
            else:
                self.clear()

    def open_template_file(self, variables=None):
        """
        Opens the widget specified in the templateFilename property.
        
        Parameters
        ----------
        variables : dict
            A dictionary of macro variables to apply when loading, in addition
            to all the macros specified on the template repeater widget.
        Returns
        -------
        display : QWidget
        """
        if not variables:
            variables = {}
        # Expand user (~ or ~user) and environment variables.
        fname = os.path.expanduser(os.path.expandvars(self.templateFilename))
        if is_pydm_app():
            if not self._cached_template:
                self._cached_template = self.app.open_template(fname)
            return self.app.widget_from_template(self._cached_template,
                                                 variables)
        else:
            try:
                f = macro.substitute_in_file(fname, variables)
                return uic.loadUi(f)
            except Exception as e:
                logger.exception("Exception while opening template file.")
                return None

    def rebuild(self):
        """ Clear out all existing widgets, and populate the list using the
        template file and data source."""
        self.clear()
        if (not self.templateFilename) or (not self.data):
            return
        self.setUpdatesEnabled(False)

        layout_class = layout_class_for_type[self.layoutType]
        if type(self.layout()) != layout_class:
            if self.layout() is not None:
                # Trick to remove the existing layout by re-parenting it in an empty widget.
                QWidget().setLayout(self.layout())
            l = layout_class(self)
            self.setLayout(l)
        with pydm.data_plugins.connection_queue(defer_connections=True):
            for i, variables in enumerate(self.data):
                if is_qt_designer() and i > self.countShownInDesigner - 1:
                    break
                w = self.open_template_file(variables)
                if w is None:
                    w = QLabel()
                    w.setText(
                        "No Template Loaded.  Data: {}".format(variables))
                w.setParent(self)
                self.layout().addWidget(w)
        self.setUpdatesEnabled(True)
        pydm.data_plugins.establish_queued_connections()

    def clear(self):
        """ Clear out any existing instances of the template inside
        the widget."""
        if not self.layout():
            return
        while self.layout().count() > 0:
            item = self.layout().takeAt(0)
            item.widget().deleteLater()
            del item

    def count(self):
        if not self.layout():
            return 0
        return self.layout().count()

    @property
    def data(self):
        """
        The dictionary used by the widget to fill in each instance of the template.
        This property will be overwritten if the user changes the dataSource
        property.
        """
        return self._data

    @data.setter
    def data(self, new_data):
        """
        Sets the dictionary used by the widget to fill in each instance of 
        the template.  This property will be overwritten if the user changes
        the dataSource property.  After setting this property, `rebuild` 
        is automatically called to refresh the widget.
        """
        self._data = new_data
        self.rebuild()
Ejemplo n.º 22
0
class SiriusLabel(QLabel, TextFormatter, PyDMWidget, DisplayFormat):
    """Sirius Label."""
    Q_ENUMS(DisplayFormat)
    DisplayFormat = DisplayFormat
    DisplayFormat.Time = 6
    """
    A QLabel with support for Channels and more from PyDM

    Parameters
    ----------
    parent : QWidget
        The parent widget for the Label
    init_channel : str, optional
        The channel to be used by the widget.
    """
    def __init__(self, parent=None, init_channel=None, **kws):
        """Init."""
        QLabel.__init__(self, parent, **kws)
        PyDMWidget.__init__(self, init_channel=init_channel)
        self.app = QApplication.instance()
        self.setTextFormat(Qt.PlainText)
        self.setTextInteractionFlags(Qt.NoTextInteraction)
        self.setText("PyDMLabel")
        self._display_format_type = self.DisplayFormat.Default
        self._string_encoding = "utf_8"
        self._conv = 1
        if is_pydm_app():
            self._string_encoding = self.app.get_string_encoding()

    @Property(DisplayFormat)
    def displayFormat(self):
        """Display Format."""
        return self._display_format_type

    @displayFormat.setter
    def displayFormat(self, new_type):
        if self._display_format_type != new_type:
            self._display_format_type = new_type
            # Trigger the update of display format
            self.value_changed(self.value)

    def update_format_string(self):
        """
        Reconstruct the format string to be used when representing the
        output value.

        Returns
        -------
        format_string : str
            The format string to be used including or not the precision
            and unit
        """
        self.format_string = "{}"
        if isinstance(self.value, (int, float)):
            self.format_string = "{:." + str(self.precision) + "f}"
        if self._show_units and self._unit != "":
            unt_opt = units.find_unit_options(self._unit)
            if unt_opt:
                unt_si = [un for un in unt_opt if units.find_unit(un) == 1][0]
                self._conv = units.convert(self._unit, unt_si)
            else:
                self._conv = 1
                unt_si = self._unit
            self.format_string += " {}" + "{}".format(unt_si)
        return self.format_string

    def value_changed(self, new_value):
        """
        Callback invoked when the Channel value is changed.
        Sets the value of new_value accordingly at the Label.

        Parameters
        ----------
        new_value : str, int, float, bool or np.ndarray
            The new value from the channel. The type depends on the channel.
        """
        super(SiriusLabel, self).value_changed(new_value)
        # If it is a DiaplayFormat.Time, parse with siriuspy.clientarch.Time
        if self._display_format_type == self.DisplayFormat.Time:
            time = _Time(int(new_value)).time().isoformat() \
                if new_value is not None else ''
            self.setText(time)
            return

        new_value = parse_value_for_display(
            value=new_value,
            precision=self.precision,
            display_format_type=self._display_format_type,
            string_encoding=self._string_encoding,
            widget=self)
        # If the value is a string, just display it as-is, no formatting
        # needed.
        if isinstance(new_value, str):
            self.setText(new_value)
            return
        # If the value is an enum, display the appropriate enum string for
        # the value.
        if self.enum_strings and isinstance(new_value, (int, float)):
            try:
                self.setText(self.enum_strings[int(new_value)])
            except IndexError:
                self.setText(f'Index Overflow [{new_value}]')
            return
        # If the value is a number (float or int), display it using a
        # format string if necessary.
        if isinstance(new_value, (int, float)):
            if self._show_units and self._unit != '':
                new_value *= self._conv
                sc, prf = func.siScale(new_value)
                self.setText(self.format_string.format(sc * new_value, prf))
            else:
                self.setText(self.format_string.format(new_value))
            return
        # If you made it this far, just turn whatever the heck the value
        # is into a string and display it.
        self.setText(str(new_value))
Ejemplo n.º 23
0
class PyDMLineEdit(QLineEdit, TextFormatter, PyDMWritableWidget, DisplayFormat):
    Q_ENUMS(DisplayFormat)
    DisplayFormat = DisplayFormat
    """
    A QLineEdit (writable text field) with support for Channels and more
    from PyDM.
    This widget offers an unit conversion menu when users Right Click
    into it.

    Parameters
    ----------
    parent : QWidget
        The parent widget for the Label
    init_channel : str, optional
        The channel to be used by the widget.
    """

    def __init__(self, parent=None, init_channel=None):
        QLineEdit.__init__(self, parent)
        PyDMWritableWidget.__init__(self, init_channel=init_channel)
        self.app = QApplication.instance()
        self._display = None
        self._scale = 1

        self.returnPressed.connect(self.send_value)
        self.unitMenu = QMenu('Convert Units', self)
        self.create_unit_options()
        self._display_format_type = self.DisplayFormat.Default
        self._string_encoding = "utf_8"
        if utilities.is_pydm_app():
            self._string_encoding = self.app.get_string_encoding()

    @Property(DisplayFormat)
    def displayFormat(self):
        return self._display_format_type

    @displayFormat.setter
    def displayFormat(self, new_type):
        if self._display_format_type != new_type:
            self._display_format_type = new_type
            # Trigger the update of display format
            self.value_changed(self.value)

    def value_changed(self, new_val):
        """
        Receive and update the PyDMLineEdit for a new channel value

        The actual value of the input is saved as well as the type received.
        This also resets the PyDMLineEdit display text using
        :meth:`.set_display`

        Parameters
        ----------
        value: str, float or int
            The new value of the channel
        """
        super(PyDMLineEdit, self).value_changed(new_val)
        self.set_display()

    def send_value(self):
        """
        Emit a :attr:`send_value_signal` to update channel value.

        The text is cleaned of all units, user-formatting and scale values
        before being sent back to the channel. This function is attached the
        ReturnPressed signal of the PyDMLineEdit
        """
        send_value = str(self.text())
        # Clean text of unit string
        if self._show_units and self._unit and self._unit in send_value:
            send_value = send_value[:-len(self._unit)].strip()
        try:
            if self.channeltype not in [str, np.ndarray]:
                scale = self._scale
                if scale is None or scale == 0:
                    scale = 1.0

                if self._display_format_type in [DisplayFormat.Default, DisplayFormat.String]:
                    if self.channeltype == float:
                        num_value = locale.atof(send_value)
                    else:
                        num_value = self.channeltype(send_value)
                    scale = self.channeltype(scale)
                elif self._display_format_type == DisplayFormat.Hex:
                    num_value = int(send_value, 16)
                elif self._display_format_type == DisplayFormat.Binary:
                    num_value = int(send_value, 2)
                elif self._display_format_type in [DisplayFormat.Exponential, DisplayFormat.Decimal]:
                    num_value = locale.atof(send_value)

                num_value = num_value / scale
                self.send_value_signal[self.channeltype].emit(num_value)
            elif self.channeltype == np.ndarray:
                # Arrays will be in the [1.2 3.4 22.214] format
                if self._display_format_type == DisplayFormat.String:
                    self.send_value_signal[str].emit(send_value)
                else:
                    arr_value = list(filter(None, send_value.replace("[", "").replace("]", "").split(" ")))
                    arr_value = np.array(arr_value, dtype=self.subtype)
                    self.send_value_signal[np.ndarray].emit(arr_value)
            else:
                # Channel Type is String
                # Lets just send what we have after all
                self.send_value_signal[str].emit(send_value)
        except ValueError:
            logger.exception("Error trying to set data '{0}' with type '{1}' and format '{2}' at widget '{3}'."
                         .format(self.text(), self.channeltype, self._display_format_type, self.objectName()))

        self.clearFocus()
        self.set_display()

    def write_access_changed(self, new_write_access):
        """
        Change the PyDMLineEdit to read only if write access is denied
        """
        super(PyDMLineEdit, self).write_access_changed(new_write_access)
        self.setReadOnly(not new_write_access)

    def unit_changed(self, new_unit):
        """
        Accept a unit to display with a channel's value

        The unit may or may not be displayed based on the :attr:`showUnits`
        attribute. Receiving a new value for the unit causes the display to
        reset.
        """
        super(PyDMLineEdit, self).unit_changed(new_unit)
        self._scale = 1
        self.create_unit_options()

    def create_unit_options(self):
        """
        Create the menu for displaying possible unit values

        The menu is filled with possible unit conversions based on the
        current PyDMLineEdit. If either the unit is not found in the by
        the :func:`utilities.find_unit_options` function, or, the
        :attr:`.showUnits` attribute is set to False, the menu will tell
        the user that there are no available conversions
        """
        self.unitMenu.clear()
        units = utilities.find_unit_options(self._unit)
        if units and self._show_units:
            for choice in units:
                self.unitMenu.addAction(choice,
                                        partial(
                                            self.apply_conversion,
                                            choice
                                            )
                                        )
        else:
            self.unitMenu.addAction('No Unit Conversions found')

    def apply_conversion(self, unit):
        """
        Convert the current unit to a different one

        This function will attempt to find a scalar to convert the current
        unit type to the desired one and reset the display with the new
        conversion.

        Parameters
        ----------
        unit : str
            String name of desired units
        """
        if not self._unit:
            logger.warning("Warning: Attempting to convert PyDMLineEdit unit, but no initial units supplied.")
            return None

        scale = utilities.convert(str(self._unit), unit)
        if scale:
            self._scale = scale * float(self._scale)
            self._unit = unit
            self.update_format_string()
            self.clearFocus()
            self.set_display()
        else:
            logging.warning("Warning: Attempting to convert PyDMLineEdit unit, but '{0}' can not be converted to '{1}'."
                            .format(self._unit, unit))

    def widget_ctx_menu(self):
        """
        Fetch the Widget specific context menu which will be populated with additional tools by `assemble_tools_menu`.

        Returns
        -------
        QMenu or None
            If the return of this method is None a new QMenu will be created by `assemble_tools_menu`.
        """
        menu = self.createStandardContextMenu()
        menu.addSeparator()
        menu.addMenu(self.unitMenu)
        return menu

    def set_display(self):
        """
        Set the text display of the PyDMLineEdit.

        The original value given by the PV is converted to a text entry based
        on the current settings for scale value, precision, a user-defined
        format, and the current units. If the user is currently entering a
        value in the PyDMLineEdit the text will not be changed.
        """
        if self.value is None:
            return

        if self.hasFocus():
            return

        new_value = self.value

        if self._display_format_type in [DisplayFormat.Default,
                                         DisplayFormat.Decimal,
                                         DisplayFormat.Exponential,
                                         DisplayFormat.Hex,
                                         DisplayFormat.Binary]:
            if self.channeltype not in (str, np.ndarray):
                try:
                    new_value *= self.channeltype(self._scale)
                except TypeError:
                    logger.error("Cannot convert the value '{0}', for channel '{1}', to type '{2}'. ".format(
                        self._scale, self._channel, self.channeltype))

        new_value = parse_value_for_display(value=new_value,  precision=self.precision,
                                            display_format_type=self._display_format_type,
                                            string_encoding=self._string_encoding,
                                            widget=self)

        if type(new_value) in str_types:
            self._display = new_value
        else:
            self._display = str(new_value)

        if self._display_format_type == DisplayFormat.Default:
            if isinstance(new_value, (int, float)):
                self._display = str(self.format_string.format(new_value))
                self.setText(self._display)
                return

        if self._show_units:
            self._display = "{} {}".format(self._display, self._unit)

        self.setText(self._display)

    def focusOutEvent(self, event):
        """
        Overwrites the function called when a user leaves a PyDMLineEdit
        without pressing return.  Resets the value of the text field to the
        current channel value.
        """
        if self._display is not None:
            self.setText(self._display)
        super(PyDMLineEdit, self).focusOutEvent(event)
Ejemplo n.º 24
0
class AppImageTreeModel(QAbstractItemModel, AppImageTreeModelRoles):

    Q_ENUMS(AppImageTreeModelRoles)

    appInterfaceChanged = Signal()

    def __init__(self, parent=None):
        super(AppImageTreeModel, self).__init__(parent)

        self._app_image = None
        self._data = AppImageData()
        self._app_interface = None
        self._watched = []

    @Property(AppInterface, notify=appInterfaceChanged)
    def appInterface(self):
        return self._app_interface

    @appInterface.setter
    def appInterface(self, new_interface):
        if new_interface == self._app_interface:
            return
        old_interface = self._app_interface
        self._app_interface = new_interface
        self.appInterfaceChanged.emit()

        if old_interface:
            old_interface.appImageChanged.disconnect(self.refresh)
            old_interface.itemUpdated.disconnect(self._on_item_updated)
        if new_interface:
            new_interface.appImageChanged.connect(self.refresh)
            new_interface.itemUpdated.connect(self._on_item_updated)
            self.refresh()

    def _on_item_updated(self, key):
        levels = key.split('/')
        item = self._data
        for level in levels:
            item = item.get(level, None)
            if item is None:
                return

        if item.parent is None:
            row = 0
        else:
            row = list(item.parent.values()).index(item)
        index = self.createIndex(row, 0, item)
        self.dataChanged.emit(index, index)

    @Slot()
    def refresh(self):
        if not self._app_interface:
            logger.warning('cannot refresh without an appInterface')
            return

        self.beginResetModel()
        self._data = self._app_interface.app_image
        self.endResetModel()

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None

        item = index.internalPointer()
        switch = {
            Qt.DisplayRole: lambda: item.name,
            self.NameRole: lambda: item.name,
            self.ValueRole: lambda: value_to_str(item.value),
            self.WatchRole: lambda: item.watch,
            self.KeyRole: lambda: item.key,
        }

        data = switch.get(role, lambda: None)()
        return data

    def setData(self, index, value, role=Qt.EditRole):
        item = index.internalPointer()
        if role in (Qt.EditRole, self.NameRole):
            item.name = value
            changed = True
        elif role == self.ValueRole:
            new_value = str_to_value(value, item.value)
            if new_value is None:
                logger.error(f'Cannot convert value {item.key} {value}')
                changed = False
            else:
                item.value = new_value
                self._app_interface.setValue(item.key, new_value)
                changed = True
        elif role == self.WatchRole:
            if value:
                self._app_interface.append_watched(item)
            else:
                self._app_interface.remove_watched(item)
            changed = True
        else:
            changed = False

        if changed:
            self.dataChanged.emit(index, index, [role])
        return changed

    def roleNames(self):
        return self.role_names()

    def flags(self, index):
        if not index.isValid():
            return Qt.NoItemFlags

        flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
        item = index.internalPointer()
        if not index.parent().isValid() and item.name not in ('proclog',
                                                              'eproclist'):
            flags |= Qt.ItemIsTristate
        elif len(item) == 0:
            flags |= Qt.ItemIsEditable
        elif index.parent().isValid():
            flags |= Qt.ItemIsTristate

        return flags

    def headerData(self, _section, orientation, role=Qt.DisplayRole):
        if not orientation == Qt.Horizontal:
            return None

        return self.role_names().get(role, '').title()

    def _get_item(self, index):
        if index and index.isValid():
            item = index.internalPointer()
            if item:
                return item

        return self._data

    def index(self, row, column, parent=QModelIndex()):
        if not self.hasIndex(row, column, parent):
            return QModelIndex()

        parent_item = self._get_item(parent)
        try:
            child_item = list(parent_item.values())[row]
        except IndexError:
            return QModelIndex()
        else:
            return self.createIndex(row, column, child_item)

    def parent(self, index):
        if not index.isValid():
            return QModelIndex()

        child_item = index.internalPointer()
        parent_item = child_item.parent

        if parent_item == self._data:
            return QModelIndex()

        row = list(parent_item.parent.values()).index(parent_item)
        return self.createIndex(row, 0, parent_item)

    def columnCount(self, _parent=QModelIndex()):
        return len(self.role_names())

    def rowCount(self, parent=QModelIndex()):
        if parent.column() > 0:
            return 0

        if not parent.isValid():
            parent_item = self._data
        else:
            parent_item = parent.internalPointer()

        return len(parent_item) if parent_item else 0