Beispiel #1
0
class QtActivityDialog(QDialog):
    """Activity Dialog for Napari progress bars."""

    MIN_WIDTH = 250
    MIN_HEIGHT = 185

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

        self.setObjectName('Activity')
        self.setMinimumWidth(self.MIN_WIDTH)
        self.setMinimumHeight(self.MIN_HEIGHT)
        self.setMaximumHeight(self.MIN_HEIGHT)
        self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
        self.setWindowFlags(Qt.SubWindow | Qt.WindowStaysOnTopHint)
        self.setModal(False)

        opacityEffect = QGraphicsOpacityEffect(self)
        opacityEffect.setOpacity(0.8)
        self.setGraphicsEffect(opacityEffect)

        self._baseWidget = QWidget()

        self._activityLayout = QVBoxLayout()
        self._activityLayout.addStretch()
        self._baseWidget.setLayout(self._activityLayout)
        self._baseWidget.layout().setContentsMargins(0, 0, 0, 0)

        self._scrollArea = QScrollArea()
        self._scrollArea.setWidgetResizable(True)
        self._scrollArea.setWidget(self._baseWidget)

        self._titleBar = QLabel()

        title = QLabel('activity', self)
        title.setObjectName('QtCustomTitleLabel')
        title.setSizePolicy(
            QSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Maximum)
        )
        line = QFrame(self)
        line.setObjectName("QtCustomTitleBarLine")
        titleLayout = QHBoxLayout()
        titleLayout.setSpacing(4)
        titleLayout.setContentsMargins(8, 1, 8, 0)
        line.setFixedHeight(1)
        titleLayout.addWidget(line)
        titleLayout.addWidget(title)
        self._titleBar.setLayout(titleLayout)

        self._baseLayout = QVBoxLayout()
        self._baseLayout.addWidget(self._titleBar)
        self._baseLayout.addWidget(self._scrollArea)
        self.setLayout(self._baseLayout)
        self.resize(520, self.MIN_HEIGHT)
        self.move_to_bottom_right()

        # TODO: what do we do with any existing progress objects in action?
        # connect callback to handle new progress objects being added/removed
        progress._all_instances.events.changed.connect(
            self.handle_progress_change
        )

    def handle_progress_change(self, event):
        """Handle addition and/or removal of new progress objects

        Parameters
        ----------
        event : Event
            EventedSet `changed` event with `added` and `removed` objects
        """
        for prog in event.removed:
            self.close_progress_bar(prog)
        for prog in event.added:
            self.make_new_pbar(prog)

    def make_new_pbar(self, prog):
        """Make new `QtLabeledProgressBar` for this `progress` object and add to viewer.

        Parameters
        ----------
        prog : progress
            progress object to associated with new progress bar
        """
        prog.gui = True
        prog.leave = False

        # make and add progress bar
        pbar = QtLabeledProgressBar(prog=prog)
        self.add_progress_bar(pbar, nest_under=prog.nest_under)

        # connect progress object events to updating progress bar
        prog.events.value.connect(pbar._set_value)
        prog.events.description.connect(pbar._set_description)
        prog.events.overflow.connect(pbar._make_indeterminate)
        prog.events.eta.connect(pbar._set_eta)

        # connect pbar close method if we're closed
        self.destroyed.connect(prog.close)

        # set its range etc. based on progress object
        if prog.total is not None:
            pbar.setRange(prog.n, prog.total)
            pbar.setValue(prog.n)
        else:
            pbar.setRange(0, 0)
            prog.total = 0
        pbar.setDescription(prog.desc)

    def add_progress_bar(self, pbar, nest_under=None):
        """Add progress bar to activity_dialog,in QtProgressBarGroup if needed.

        Check if pbar needs nesting and create QtProgressBarGroup, removing
        existing separators and creating new ones. Show and start
        inProgressIndicator to highlight the existence of a progress bar
        in the dock even when the dock is hidden.

        Parameters
        ----------
        pbar : QtLabeledProgressBar
            progress bar to add to activity dialog
        nest_under : Optional[progress]
            parent `progress` whose QtLabeledProgressBar we need to nest under
        """
        if nest_under is None:
            self._activityLayout.addWidget(pbar)
        else:
            # TODO: can parent be non gui pbar?
            parent_pbar = self.get_pbar_from_prog(nest_under)
            current_pbars = [parent_pbar, pbar]
            remove_separators(current_pbars)

            parent_widg = parent_pbar.parent()
            # if we are already in a group, add pbar to existing group
            if isinstance(parent_widg, QtProgressBarGroup):
                nested_layout = parent_widg.layout()
            # create QtProgressBarGroup for this pbar
            else:
                new_group = QtProgressBarGroup(parent_pbar)
                new_group.destroyed.connect(self.maybe_hide_progress_indicator)
                nested_layout = new_group.layout()
                self._activityLayout.addWidget(new_group)
            # progress bar needs to go before separator
            new_pbar_index = nested_layout.count() - 1
            nested_layout.insertWidget(new_pbar_index, pbar)

        # show progress indicator and start gif
        self._toggleButton._inProgressIndicator.movie().start()
        self._toggleButton._inProgressIndicator.show()
        pbar.destroyed.connect(self.maybe_hide_progress_indicator)
        QApplication.processEvents()

    def get_pbar_from_prog(self, prog):
        """Given prog `progress` object, find associated `QtLabeledProgressBar`

        Parameters
        ----------
        prog : progress
            progress object with associated progress bar

        Returns
        -------
        QtLabeledProgressBar
            QtLabeledProgressBar widget associated with this progress object
        """
        pbars = self._baseWidget.findChildren(QtLabeledProgressBar)
        if pbars:
            for potential_parent in pbars:
                if potential_parent.progress is prog:
                    return potential_parent

    def close_progress_bar(self, prog):
        """Close `QtLabeledProgressBar` and parent `QtProgressBarGroup` if needed

        Parameters
        ----------
        prog : progress
            progress object whose QtLabeledProgressBar to close
        """
        current_pbar = self.get_pbar_from_prog(prog)
        if not current_pbar:
            return
        parent_widget = current_pbar.parent()
        current_pbar.close()
        current_pbar.deleteLater()
        if isinstance(parent_widget, QtProgressBarGroup):
            pbar_children = [
                child
                for child in parent_widget.children()
                if isinstance(child, QtLabeledProgressBar)
            ]
            # only close group if it has no visible progress bars
            if not any(child.isVisible() for child in pbar_children):
                parent_widget.close()

    def move_to_bottom_right(self, offset=(8, 8)):
        """Position widget at the bottom right edge of the parent."""
        if not self.parent():
            return
        sz = self.parent().size() - self.size() - QSize(*offset)
        self.move(QPoint(sz.width(), sz.height()))

    def maybe_hide_progress_indicator(self):
        """Hide progress indicator when all progress bars have finished."""
        pbars = self._baseWidget.findChildren(QtLabeledProgressBar)
        pbar_groups = self._baseWidget.findChildren(QtProgressBarGroup)

        progress_visible = any([pbar.isVisible() for pbar in pbars])
        progress_group_visible = any(
            [pbar_group.isVisible() for pbar_group in pbar_groups]
        )
        if not progress_visible and not progress_group_visible:
            self._toggleButton._inProgressIndicator.movie().stop()
            self._toggleButton._inProgressIndicator.hide()
Beispiel #2
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'])
Beispiel #3
0
class ActivityDialog(QDialog):
    """Activity Dialog for Napari progress bars."""

    MIN_WIDTH = 250
    MIN_HEIGHT = 185

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

        self.setObjectName('Activity')
        self.setMinimumWidth(self.MIN_WIDTH)
        self.setMinimumHeight(self.MIN_HEIGHT)
        self.setMaximumHeight(self.MIN_HEIGHT)
        self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
        self.setWindowFlags(Qt.SubWindow | Qt.WindowStaysOnTopHint)
        self.setModal(False)

        opacityEffect = QGraphicsOpacityEffect(self)
        opacityEffect.setOpacity(0.8)
        self.setGraphicsEffect(opacityEffect)

        self._baseWidget = QWidget()

        self._activityLayout = QVBoxLayout()
        self._activityLayout.addStretch()
        self._baseWidget.setLayout(self._activityLayout)
        self._baseWidget.layout().setContentsMargins(0, 0, 0, 0)

        self._scrollArea = QScrollArea()
        self._scrollArea.setWidgetResizable(True)
        self._scrollArea.setWidget(self._baseWidget)

        self._titleBar = QLabel()

        title = QLabel('activity', self)
        title.setObjectName('QtCustomTitleLabel')
        title.setSizePolicy(
            QSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Maximum)
        )
        line = QFrame(self)
        line.setObjectName("QtCustomTitleBarLine")
        titleLayout = QHBoxLayout()
        titleLayout.setSpacing(4)
        titleLayout.setContentsMargins(8, 1, 8, 0)
        line.setFixedHeight(1)
        titleLayout.addWidget(line)
        titleLayout.addWidget(title)
        self._titleBar.setLayout(titleLayout)

        self._baseLayout = QVBoxLayout()
        self._baseLayout.addWidget(self._titleBar)
        self._baseLayout.addWidget(self._scrollArea)
        self.setLayout(self._baseLayout)

    def add_progress_bar(self, pbar, nest_under=None):
        """Add progress bar to the activity_dialog, making ProgressBarGroup if needed.

        Check whether pbar is nested and create ProgressBarGroup if it is, removing
        existing separators and creating new ones. Show and start inProgressIndicator
        to highlight to user the existence of a progress bar in the dock even when
        the dock is hidden.

        Parameters
        ----------
        pbar : ProgressBar
            progress bar to add to activity dialog
        nest_under : Optional[ProgressBar]
            parent progress bar pbar should be nested under, by default None
        """
        if nest_under is None:
            self._activityLayout.addWidget(pbar)
        else:
            # this is going to be nested, remove separators
            # as the group will have its own
            parent_pbar = nest_under._pbar
            current_pbars = [parent_pbar, pbar]
            remove_separators(current_pbars)

            parent_widg = parent_pbar.parent()
            # if we are already in a group, add pbar to existing group
            if isinstance(parent_widg, ProgressBarGroup):
                nested_layout = parent_widg.layout()
            # create ProgressBarGroup for this pbar
            else:
                new_group = ProgressBarGroup(nest_under._pbar)
                new_group.destroyed.connect(self.maybe_hide_progress_indicator)
                nested_layout = new_group.layout()
                self._activityLayout.addWidget(new_group)
            new_pbar_index = nested_layout.count() - 1
            nested_layout.insertWidget(new_pbar_index, pbar)

        # show progress indicator and start gif
        self._toggleButton._inProgressIndicator.movie().start()
        self._toggleButton._inProgressIndicator.show()
        pbar.destroyed.connect(self.maybe_hide_progress_indicator)

    def move_to_bottom_right(self, offset=(8, 8)):
        """Position widget at the bottom right edge of the parent."""
        if not self.parent():
            return
        sz = self.parent().size() - self.size() - QSize(*offset)
        self.move(QPoint(sz.width(), sz.height()))

    def maybe_hide_progress_indicator(self):
        """Hide progress indicator when all progress bars have finished."""
        pbars = self._baseWidget.findChildren(ProgressBar)
        pbar_groups = self._baseWidget.findChildren(ProgressBarGroup)

        progress_visible = any([pbar.isVisible() for pbar in pbars])
        progress_group_visible = any(
            [pbar_group.isVisible() for pbar_group in pbar_groups]
        )
        if not progress_visible and not progress_group_visible:
            self._toggleButton._inProgressIndicator.movie().stop()
            self._toggleButton._inProgressIndicator.hide()