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()
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'])
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()