def paramtree_get_root_parameters(pt: ParameterTree) -> Sequence[Parameter]: root = pt.invisibleRootItem() return [root.child(i).param for i in range(root.childCount())]
class TyphonSuite(TyphonBase): """ Complete Typhon Window This contains all the neccesities to load tools and devices into a Typhon window. Parameters ---------- parent : QWidget, optional """ default_tools = { 'Log': TyphonLogDisplay, 'StripTool': TyphonTimePlot, 'Console': TyphonConsole } def __init__(self, parent=None): super().__init__(parent=parent) # Setup parameter tree self._tree = ParameterTree(parent=self, showHeader=False) self._tree.setAlternatingRowColors(False) # Setup layout self._layout = QHBoxLayout() self._layout.setSizeConstraint(QHBoxLayout.SetFixedSize) self._layout.addWidget(self._tree) self.setLayout(self._layout) self.embedded_dock = None def add_subdisplay(self, name, display, category): """ Add an arbitrary widget to the tree of available widgets and tools Parameters ---------- name : str Name to be displayed in the tree display : QWidget QWidget to show in the dock when expanded. category : str The top level group to place the controls under in the tree. If the category does not exist, a new one will be made """ logger.debug("Adding widget %r with %r to %r ...", name, display, category) # Create our parameter parameter = SidebarParameter(value=display, name=name) self._add_to_sidebar(parameter, category) @property def top_level_groups(self): """All top-level groups as name, ``QGroupParameterItem`` pairs""" root = self._tree.invisibleRootItem() return dict((root.child(idx).param.name(), root.child(idx).param) for idx in range(root.childCount())) def add_tool(self, name, tool): """ Add a widget to the toolbar Shortcut for: .. code:: python suite.add_subdisplay(name, tool, category='Tools') Parameters ---------- name :str Name of tool to be displayed in sidebar tool: QWidget Widget to be added to ``.ui.subdisplay`` """ self.add_subdisplay(name, tool, 'Tools') def get_subdisplay(self, display): """ Get a subdisplay by name or contained device Parameters ---------- display :str or Device Name of screen or device Returns ------- widget : QWidget Widget that is a member of the :attr:`.ui.subdisplay` Example ------- .. code:: python suite.get_subdisplay(my_device.x) suite.get_subdisplay('My Tool') """ if isinstance(display, SidebarParameter): return display.value() for group in self.top_level_groups.values(): tree = flatten_tree(group) for param in tree: match = (display in getattr(param.value(), 'devices', []) or param.name() == display) if match: return param.value() # If we got here we can't find the subdisplay raise ValueError(f"Unable to find subdisplay {display}") @Slot(str) @Slot(object) def show_subdisplay(self, widget): """ Open a display in the dock system Parameters ---------- widget: QWidget, SidebarParameter or str If given a ``SidebarParameter`` from the tree, the widget will be shown and the sidebar item update. Otherwise, the information is passed to :meth:`.get_subdisplay` """ # Grab true widget if not isinstance(widget, QWidget): widget = self.get_subdisplay(widget) # Setup the dock dock = SubDisplay(self) # Set sidebar properly self._show_sidebar(widget, dock) # Add the widget to the dock logger.debug("Showing widget %r ...", widget) if hasattr(widget, 'display_type'): widget.display_type = widget.detailed_screen widget.setVisible(True) dock.setWidget(widget) # Add to layout self.layout().addWidget(dock) @Slot(str) @Slot(object) def embed_subdisplay(self, widget): """Embed a display in the dock system""" # Grab the relevant display if not self.embedded_dock: self.embedded_dock = SubDisplay() self.embedded_dock.setWidget(QWidget()) self.embedded_dock.widget().setLayout(QVBoxLayout()) self.embedded_dock.widget().layout().addStretch(1) self.layout().addWidget(self.embedded_dock) if not isinstance(widget, QWidget): widget = self.get_subdisplay(widget) # Set sidebar properly self._show_sidebar(widget, self.embedded_dock) # Set our widget to be embedded widget.setVisible(True) widget.display_type = widget.embedded_screen widget_count = self.embedded_dock.widget().layout().count() self.embedded_dock.widget().layout().insertWidget( widget_count - 1, widget) @Slot() @Slot(object) def hide_subdisplay(self, widget): """ Hide a visible subdisplay Parameters ---------- widget: SidebarParameter or Subdisplay If you give a SidebarParameter, we will find the corresponding widget and hide it. If the widget provided to us is inside a DockWidget we will close that, otherwise the widget is just hidden. """ if not isinstance(widget, QWidget): widget = self.get_subdisplay(widget) sidebar = self._get_sidebar(widget) if sidebar: for item in sidebar.items: item._mark_hidden() else: logger.warning("Unable to find sidebar item for %r", widget) # Make sure the actual widget is hidden logger.debug("Hiding widget %r ...", widget) if isinstance(widget.parent(), QDockWidget): logger.debug("Closing dock ...") widget.parent().close() # Hide the full dock if this is the last widget elif (self.embedded_dock and widget.parent() == self.embedded_dock.widget()): logger.debug("Removing %r from embedded widget layout ...", widget) self.embedded_dock.widget().layout().removeWidget(widget) widget.hide() if self.embedded_dock.widget().layout().count() == 1: logger.debug("Closing embedded layout ...") self.embedded_dock.close() self.embedded_dock = None else: widget.hide() @Slot() def hide_subdisplays(self): """ Hide all open displays """ # Grab children from devices for group in self.top_level_groups.values(): for param in flatten_tree(group)[1:]: self.hide_subdisplay(param) @property def tools(self): """Tools loaded into the TyphonDeviceDisplay""" if 'Tools' in self.top_level_groups: return [ param.value() for param in self.top_level_groups['Tools'].childs ] return [] def add_device(self, device, children=True, category='Devices'): """ Add a device to the ``TyphonSuite`` Parameters ---------- device: ophyd.Device children: bool, optional Also add any ``subdevices`` of this device to the suite as well. category: str, optional Category of device. By default, all devices will just be added to the "Devices" group """ super().add_device(device) # Create DeviceParameter and add to top level category dev_param = DeviceParameter(device, subdevices=children) self._add_to_sidebar(dev_param, category) # Grab children for child in flatten_tree(dev_param)[1:]: self._add_to_sidebar(child) # Add a device to all the tool displays for tool in self.tools: try: tool.add_device(device) except Exception: logger.exception("Unable to add %s to tool %s", device.name, type(tool)) @classmethod def from_device(cls, device, parent=None, tools=dict(), **kwargs): """ Create a new TyphonDeviceDisplay from an ophyd.Device Parameters ---------- device: ophyd.Device children: bool, optional Choice to include child Device components parent: QWidgets tools: dict, optional Tools to load for the object. ``dict`` should be name, class pairs. By default these will be ``.default_tools``, but ``None`` can be passed to avoid tool loading completely. kwargs: Passed to :meth:`TyphonSuite.add_device` """ display = cls(parent=parent) if tools is not None: if not tools: logger.debug("Using default TyphonSuite tools ...") tools = cls.default_tools for name, tool in tools.items(): try: display.add_tool(name, tool()) except Exception: logger.exception("Unable to load %s", type(tool)) display.add_device(device, **kwargs) display.show_subdisplay(device) return display def _get_sidebar(self, widget): items = {} for group in self.top_level_groups.values(): for item in flatten_tree(group): items[item.value()] = item return items.get(widget) def _show_sidebar(self, widget, dock): sidebar = self._get_sidebar(widget) if sidebar: for item in sidebar.items: item._mark_shown() # Make sure we react if the dock is closed outside of our menu dock.closing.connect(partial(self.hide_subdisplay, sidebar)) else: logger.warning("Unable to find sidebar item for %r", widget) def _add_to_sidebar(self, parameter, category=None): """Add an item to the sidebar, connecting necessary signals""" if category: # Create or grab our category if category in self.top_level_groups: group = self.top_level_groups[category] else: logger.debug("Creating new category %r ...", category) group = ptypes.GroupParameter(name=category) self._tree.addParameters(group) self._tree.sortItems(0, Qt.AscendingOrder) logger.debug("Adding %r to category %r ...", parameter.name(), group.name()) group.addChild(parameter) # Setup window to have a parent parameter.value().setParent(self) parameter.value().setHidden(True) logger.debug("Connecting parameter signals ...") parameter.sigOpen.connect(partial(self.show_subdisplay, parameter)) parameter.sigHide.connect(partial(self.hide_subdisplay, parameter)) if parameter.embeddable: parameter.sigEmbed.connect( partial(self.embed_subdisplay, parameter)) return parameter
class TyphosSuite(TyphosBase): """ This suite combines tools and devices into a single widget. A :class:`ParameterTree` is contained in a :class:`~pcdsutils.qt.QPopBar` which shows tools and the hierarchy of a device along with options to show or hide them. Parameters ---------- parent : QWidget, optional pin : bool, optional Pin the parameter tree on startup. Attributes ---------- default_tools : dict The default tools to use in the suite. In the form of ``{'tool_name': ToolClass}``. """ DEFAULT_TITLE = 'Typhos Suite' DEFAULT_TITLE_DEVICE = 'Typhos Suite - {device.name}' default_tools = { 'Log': TyphosLogDisplay, 'StripTool': TyphosTimePlot, 'Console': TyphosConsole } def __init__(self, parent=None, *, pin=False): super().__init__(parent=parent) self._update_title() self._tree = ParameterTree(parent=self, showHeader=False) self._tree.setAlternatingRowColors(False) self._save_action = ptypes.ActionParameter(name='Save Suite') self._tree.addParameters(self._save_action) self._save_action.sigActivated.connect(self.save) self._bar = pcdsutils.qt.QPopBar(title='Suite', parent=self, widget=self._tree, pin=pin) self._content_frame = QtWidgets.QFrame(self) self._content_frame.setObjectName("content") self._content_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) self._content_frame.setLayout(QtWidgets.QHBoxLayout()) # Horizontal box layout: [PopBar] [Content Frame] layout = QtWidgets.QHBoxLayout() self.setLayout(layout) layout.setSpacing(1) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self._bar) layout.addWidget(self._content_frame) self.embedded_dock = None def add_subdisplay(self, name, display, category): """ Add an arbitrary widget to the tree of available widgets and tools. Parameters ---------- name : str Name to be displayed in the tree display : QWidget QWidget to show in the dock when expanded. category : str The top level group to place the controls under in the tree. If the category does not exist, a new one will be made """ logger.debug("Adding widget %r with %r to %r ...", name, display, category) # Create our parameter parameter = SidebarParameter(value=display, name=name) self._add_to_sidebar(parameter, category) @property def top_level_groups(self): """ Get top-level groups. This is of the form: .. code:: python {'name': QGroupParameterItem} """ root = self._tree.invisibleRootItem() return dict((root.child(idx).param.name(), root.child(idx).param) for idx in range(root.childCount())) def add_tool(self, name, tool): """ Add a widget to the toolbar. Shortcut for: .. code:: python suite.add_subdisplay(name, tool, category='Tools') Parameters ---------- name :str Name of tool to be displayed in sidebar tool: QWidget Widget to be added to ``.ui.subdisplay`` """ self.add_subdisplay(name, tool, 'Tools') def get_subdisplay(self, display): """ Get a subdisplay by name or contained device. Parameters ---------- display :str or Device Name of screen or device Returns ------- widget : QWidget Widget that is a member of the :attr:`.ui.subdisplay` Example ------- .. code:: python suite.get_subdisplay(my_device.x) suite.get_subdisplay('My Tool') """ if not isinstance(display, SidebarParameter): for group in self.top_level_groups.values(): tree = flatten_tree(group) matches = [ param for param in tree if hasattr(param, 'has_device') and param.has_device(display) ] if matches: display = matches[0] break if not isinstance(display, SidebarParameter): # If we got here we can't find the subdisplay raise ValueError(f"Unable to find subdisplay {display}") subdisplay = display.value() if isinstance(subdisplay, partial): subdisplay = subdisplay() display.setValue(subdisplay) return subdisplay @QtCore.Slot(str) @QtCore.Slot(object) def show_subdisplay(self, widget): """ Open a display in the dock system. Parameters ---------- widget: QWidget, SidebarParameter or str If given a ``SidebarParameter`` from the tree, the widget will be shown and the sidebar item update. Otherwise, the information is passed to :meth:`.get_subdisplay` """ # Grab true widget if not isinstance(widget, QtWidgets.QWidget): widget = self.get_subdisplay(widget) # Setup the dock dock = widgets.SubDisplay(self) # Set sidebar properly self._show_sidebar(widget, dock) # Add the widget to the dock logger.debug("Showing widget %r ...", widget) if hasattr(widget, 'display_type'): widget.display_type = widget.detailed_screen widget.setVisible(True) dock.setWidget(widget) # Add to layout self._content_frame.layout().addWidget(dock) @QtCore.Slot(str) @QtCore.Slot(object) def embed_subdisplay(self, widget): """Embed a display in the dock system.""" # Grab the relevant display if not self.embedded_dock: self.embedded_dock = widgets.SubDisplay() self.embedded_dock.setWidget(QtWidgets.QWidget()) self.embedded_dock.widget().setLayout(QtWidgets.QVBoxLayout()) self.embedded_dock.widget().layout().addStretch(1) self._content_frame.layout().addWidget(self.embedded_dock) if not isinstance(widget, QtWidgets.QWidget): widget = self.get_subdisplay(widget) # Set sidebar properly self._show_sidebar(widget, self.embedded_dock) # Set our widget to be embedded widget.setVisible(True) widget.display_type = widget.embedded_screen widget_count = self.embedded_dock.widget().layout().count() self.embedded_dock.widget().layout().insertWidget( widget_count - 1, widget) @QtCore.Slot() @QtCore.Slot(object) def hide_subdisplay(self, widget): """ Hide a visible subdisplay. Parameters ---------- widget: SidebarParameter or Subdisplay If you give a SidebarParameter, we will find the corresponding widget and hide it. If the widget provided to us is inside a DockWidget we will close that, otherwise the widget is just hidden. """ if not isinstance(widget, QtWidgets.QWidget): widget = self.get_subdisplay(widget) sidebar = self._get_sidebar(widget) if sidebar: for item in sidebar.items: item._mark_hidden() else: logger.warning("Unable to find sidebar item for %r", widget) # Make sure the actual widget is hidden logger.debug("Hiding widget %r ...", widget) if isinstance(widget.parent(), QtWidgets.QDockWidget): logger.debug("Closing dock ...") widget.parent().close() # Hide the full dock if this is the last widget elif (self.embedded_dock and widget.parent() == self.embedded_dock.widget()): logger.debug("Removing %r from embedded widget layout ...", widget) self.embedded_dock.widget().layout().removeWidget(widget) widget.hide() if self.embedded_dock.widget().layout().count() == 1: logger.debug("Closing embedded layout ...") self.embedded_dock.close() self.embedded_dock = None else: widget.hide() @QtCore.Slot() def hide_subdisplays(self): """Hide all open displays.""" # Grab children from devices for group in self.top_level_groups.values(): for param in flatten_tree(group)[1:]: self.hide_subdisplay(param) @property def tools(self): """Tools loaded into the suite.""" if 'Tools' in self.top_level_groups: return [ param.value() for param in self.top_level_groups['Tools'].childs ] return [] def _update_title(self, device=None): """ Update the window title, optionally with a device. Parameters ---------- device : ophyd.Device, optional Device to indicate in the title. """ title_fmt = (self.DEFAULT_TITLE if device is None else self.DEFAULT_TITLE_DEVICE) self.setWindowTitle(title_fmt.format(self=self, device=device)) def add_device(self, device, children=True, category='Devices'): """ Add a device to the suite. Parameters ---------- device: ophyd.Device The device to add. children: bool, optional Also add any ``subdevices`` of this device to the suite as well. category: str, optional Category of device. By default, all devices will just be added to the "Devices" group """ super().add_device(device) self._update_title(device) # Create DeviceParameter and add to top level category dev_param = DeviceParameter(device, subdevices=children) self._add_to_sidebar(dev_param, category) # Grab children for child in flatten_tree(dev_param)[1:]: self._add_to_sidebar(child) # Add a device to all the tool displays for tool in self.tools: try: tool.add_device(device) except Exception: logger.exception("Unable to add %s to tool %s", device.name, type(tool)) @classmethod def from_device(cls, device, parent=None, tools=DEFAULT_TOOLS, pin=False, **kwargs): """ Create a new :class:`TyphosSuite` from an :class:`ophyd.Device`. Parameters ---------- device : ophyd.Device The device to use. children : bool, optional Choice to include child Device components parent : QWidget tools : dict, optional Tools to load for the object. ``dict`` should be name, class pairs. By default these will be ``.default_tools``, but ``None`` can be passed to avoid tool loading completely. **kwargs : Passed to :meth:`TyphosSuite.add_device` """ return cls.from_devices([device], parent=parent, tools=tools, pin=pin, **kwargs) @classmethod def from_devices(cls, devices, parent=None, tools=DEFAULT_TOOLS, pin=False, **kwargs): """ Create a new TyphosSuite from an iterator of :class:`ophyd.Device` Parameters ---------- device : ophyd.Device children : bool, optional Choice to include child Device components parent : QWidget tools : dict, optional Tools to load for the object. ``dict`` should be name, class pairs. By default these will be ``.default_tools``, but ``None`` can be passed to avoid tool loading completely. **kwargs : Passed to :meth:`TyphosSuite.add_device` """ suite = cls(parent=parent, pin=pin) if tools is not None: logger.info("Loading Tools ...") if tools is DEFAULT_TOOLS: logger.debug("Using default TyphosSuite tools ...") tools = cls.default_tools for name, tool in tools.items(): try: suite.add_tool(name, tool()) except Exception: logger.exception("Unable to load %s", type(tool)) logger.info("Adding devices ...") for device in devices: try: suite.add_device(device, **kwargs) suite.show_subdisplay(device) except Exception: logger.exception("Unable to add %r to TyphosSuite", device.name) return suite def save(self): """ Save suite settings to a file using :meth:`typhos.utils.save_suite`. A ``QFileDialog`` will be used to query the user for the desired location of the created Python file The template will be of the form: .. code:: """ # Note: the above docstring is appended below logger.debug("Requesting file location for saved TyphosSuite") root_dir = os.getcwd() filename = QtWidgets.QFileDialog.getSaveFileName( self, 'Save TyphosSuite', root_dir, "Python (*.py)") if filename: try: with open(filename[0], 'w+') as handle: save_suite(self, handle) except Exception as exc: logger.exception("Failed to save TyphosSuite") utils.raise_to_operator(exc) else: logger.debug("No filename chosen") # Add the template to the docstring save.__doc__ += textwrap.indent('\n' + utils.saved_template, '\t\t') def _get_sidebar(self, widget): items = {} for group in self.top_level_groups.values(): for item in flatten_tree(group): items[item.value()] = item return items.get(widget) def _show_sidebar(self, widget, dock): sidebar = self._get_sidebar(widget) if sidebar: for item in sidebar.items: item._mark_shown() # Make sure we react if the dock is closed outside of our menu dock.closing.connect(partial(self.hide_subdisplay, sidebar)) else: logger.warning("Unable to find sidebar item for %r", widget) def _add_to_sidebar(self, parameter, category=None): """Add an item to the sidebar, connecting necessary signals.""" if category: # Create or grab our category if category in self.top_level_groups: group = self.top_level_groups[category] else: logger.debug("Creating new category %r ...", category) group = ptypes.GroupParameter(name=category) self._tree.addParameters(group) self._tree.sortItems(0, QtCore.Qt.AscendingOrder) logger.debug("Adding %r to category %r ...", parameter.name(), group.name()) group.addChild(parameter) widget = parameter.value() if isinstance(widget, QtWidgets.QWidget): # Setup window to have a parent widget.setParent(self) widget.setHidden(True) logger.debug("Connecting parameter signals ...") parameter.sigOpen.connect(partial(self.show_subdisplay, parameter), QtCore.Qt.QueuedConnection) parameter.sigHide.connect(partial(self.hide_subdisplay, parameter), QtCore.Qt.QueuedConnection) if parameter.embeddable: parameter.sigEmbed.connect( partial(self.embed_subdisplay, parameter), QtCore.Qt.QueuedConnection) return parameter