From 37409746f4cf2017e91b5c3dfef93a34ad8fc570 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 20 Apr 2023 21:40:03 +0000 Subject: [PATCH] Auto-update from Github Actions Workflow Deployed from commit c1a964aa29e67d35c1ed3ff7c58158a9325e642a (refs/heads/master) Deployed from commit 9f898ae69ec8d20f4fb6e376af07eba6ee1db542 (refs/heads/master) Deployed from commit ee4da324072e90850aff7f7560e219610fb27737 (refs/heads/master) Deployed from commit 4e8675a6afdeb1a9939c3b0ac2f1747681a69675 (refs/heads/master) Deployed from commit 7fa80c54d8985fdd924d9c1c1c9009c6477ad2ff (refs/heads/master) Deployed from commit 30586731310b3719b020b9939ad2bc46e62b240c (refs/heads/master) Deployed from commit be1a7b7a7f662342e4abf7738d0f6e9e8d20905b (refs/heads/master) Deployed from commit 1fb9d4fe24b410f8be8fff4362db99e002482657 (refs/heads/master) Deployed from commit 191b0e479ee73bc01d697f12007bf5680d3fb9a5 (refs/heads/master) Deployed from commit 2f73bd3c09ef91ec3ad95566b718a1f4a8f784e9 (refs/heads/master) Deployed from commit 49d81272046ed285445e9c5e9cf674725cfe9086 (refs/heads/master) Deployed from commit 249468cac77a2296a202362daca8f36bb46a0c61 (refs/heads/master) Deployed from commit 811cd87a32ca07058caa26fdad767f7e6ce08817 (refs/heads/master) Deployed from commit 45331c1097c6aec6d11e8e7af98987d7cb9a88d9 (refs/heads/master) Deployed from commit 8e9dcdfadd15362d64c4ec2237f537c2957e209d (refs/heads/master) Deployed from commit 8f689874ba325ac5bd97c3bce7765eb36f08ebe9 (refs/heads/master) Deployed from commit e767bb46bee2956812507ec7c10b48e8a54b6034 (refs/heads/master) Deployed from commit 55352823e41f7f6998052d9eaa54aa52bdc9c2dd (refs/heads/master) Deployed from commit 5f6f5ee38826ff5e0d01e209be7a972996985d67 (refs/heads/master) Deployed from commit 3ecbd3e78e9e6749f9ac299663c3f49aea8df70f (refs/heads/master) Deployed from commit 6d193ea053389875dcb78304f8f95b52949c1736 (refs/heads/master) --- master/.buildinfo | 2 +- master/_modules/index.html | 18 +- master/_modules/typhos/alarm.html | 18 +- master/_modules/typhos/cache.html | 97 +- master/_modules/typhos/display.html | 579 +++++--- master/_modules/typhos/func.html | 51 +- master/_modules/typhos/panel.html | 221 ++- master/_modules/typhos/plugins/core.html | 115 +- master/_modules/typhos/plugins/happi.html | 44 +- master/_modules/typhos/positioner.html | 520 +++++++- master/_modules/typhos/suite.html | 212 ++- master/_modules/typhos/textedit.html | 42 +- master/_modules/typhos/tools/console.html | 321 ----- master/_modules/typhos/tools/log.html | 27 +- master/_modules/typhos/tools/plot.html | 27 +- master/_modules/typhos/tweakable.html | 39 +- master/_modules/typhos/utils.html | 510 ++++++- master/_modules/typhos/widgets.html | 281 +++- .../typhos.tools.TyphosConsole.rst.txt | 362 ----- master/_sources/index.rst.txt | 1 + master/_sources/release_notes.rst.txt | 109 +- master/_sources/templates.rst.txt | 17 + master/_sources/tools.rst.txt | 1 - master/_sources/upcoming_changes.rst.txt | 8 + .../template-full.rst.txt | 36 + .../template-short.rst.txt | 22 + master/_static/basic.css | 22 + master/_static/css/theme.css | 2 +- master/_static/documentation_options.js | 5 +- master/_static/pygments.css | 1 + master/_static/searchtools.js | 26 +- master/_static/sphinx_highlight.js | 16 +- master/basic_usage.html | 47 +- master/cli.html | 50 +- master/connections.html | 25 +- master/display.html | 216 +-- .../generated/typhos.tools.TyphosConsole.html | 1180 ----------------- .../typhos.tools.TyphosLogDisplay.html | 28 +- .../typhos.tools.TyphosTimePlot.html | 24 +- master/genindex.html | 75 +- master/index.html | 21 +- master/objects.inv | Bin 3264 -> 3443 bytes master/plugins.html | 55 +- master/py-modindex.html | 17 +- master/python_methods.html | 21 +- master/release_notes.html | 629 +++++---- master/save.html | 19 +- master/search.html | 17 +- master/searchindex.js | 2 +- master/templates.html | 43 +- master/tools.html | 33 +- master/upcoming_changes.html | 133 ++ .../upcoming_release_notes/template-full.html | 166 +++ .../template-short.html | 154 +++ master/utils.html | 271 ++-- master/widgets.html | 376 +++--- 56 files changed, 4095 insertions(+), 3259 deletions(-) delete mode 100644 master/_modules/typhos/tools/console.html delete mode 100644 master/_sources/generated/typhos.tools.TyphosConsole.rst.txt create mode 100644 master/_sources/upcoming_changes.rst.txt create mode 100644 master/_sources/upcoming_release_notes/template-full.rst.txt create mode 100644 master/_sources/upcoming_release_notes/template-short.rst.txt delete mode 100644 master/generated/typhos.tools.TyphosConsole.html create mode 100644 master/upcoming_changes.html create mode 100644 master/upcoming_release_notes/template-full.html create mode 100644 master/upcoming_release_notes/template-short.html diff --git a/master/.buildinfo b/master/.buildinfo index 9045ebf5c..d3127e43b 100644 --- a/master/.buildinfo +++ b/master/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: 6669a80fb98cbe78ffb6124512fb3cfd +config: c5ca92e50b022ddff3934fbf14fa6420 tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/master/_modules/index.html b/master/_modules/index.html index 7f08653e3..1b9cbe487 100644 --- a/master/_modules/index.html +++ b/master/_modules/index.html @@ -3,19 +3,19 @@ - Overview: module code — Typhos 2.4.1 documentation + Overview: module code — Typhos 3.0.0 documentation - - - - - - + + + + + + @@ -33,7 +33,7 @@ Typhos
- 2.4.1 + 3.0.0
@@ -52,6 +52,7 @@
  • Including Python Code
  • Custom Templates
  • Release History
  • +
  • Upcoming Changes
  • Developer Documentation

    Developer Documentation

      @@ -419,7 +420,6 @@

      Source code for typhos.alarm

                           f'Updated alarm from {self.alarm_summary} to {new_alarm} '
                           f'on alarm widget with channel {self.channels()[0]}'
                       )
      -
               self.alarm_summary = new_alarm
       
           def set_alarm_color(self, alarm_level):
      diff --git a/master/_modules/typhos/cache.html b/master/_modules/typhos/cache.html
      index 913f9a7b6..59f0475eb 100644
      --- a/master/_modules/typhos/cache.html
      +++ b/master/_modules/typhos/cache.html
      @@ -3,19 +3,19 @@
       
         
         
      -  typhos.cache — Typhos 2.4.1 documentation
      +  typhos.cache — Typhos 3.0.0 documentation
             
             
         
         
      -        
      -        
      -        
      -        
      -        
      -        
      +        
      +        
      +        
      +        
      +        
      +        
           
           
            
      @@ -33,7 +33,7 @@
                   Typhos
                 
                     
      - 2.4.1 + 3.0.0

    Developer Documentation

      @@ -111,7 +112,9 @@

      Source code for typhos.cache

       _GLOBAL_DISPLAY_PATH_CACHE = None
       
       
      -
      [docs]def get_global_describe_cache(): +
      +[docs] +def get_global_describe_cache(): """Get the _GlobalDescribeCache singleton.""" global _GLOBAL_DESCRIBE_CACHE if _GLOBAL_DESCRIBE_CACHE is None: @@ -119,7 +122,10 @@

      Source code for typhos.cache

           return _GLOBAL_DESCRIBE_CACHE
      -
      [docs]def get_global_widget_type_cache(): + +
      +[docs] +def get_global_widget_type_cache(): """Get the _GlobalWidgetTypeCache singleton.""" global _GLOBAL_WIDGET_TYPE_CACHE if _GLOBAL_WIDGET_TYPE_CACHE is None: @@ -127,7 +133,10 @@

      Source code for typhos.cache

           return _GLOBAL_WIDGET_TYPE_CACHE
      -
      [docs]def get_global_display_path_cache(): + +
      +[docs] +def get_global_display_path_cache(): """Get the _GlobalDisplayPathCache singleton.""" global _GLOBAL_DISPLAY_PATH_CACHE if _GLOBAL_DISPLAY_PATH_CACHE is None: @@ -135,7 +144,10 @@

      Source code for typhos.cache

           return _GLOBAL_DISPLAY_PATH_CACHE
      -
      [docs]class _GlobalDescribeCache(QtCore.QObject): + +
      +[docs] +class _GlobalDescribeCache(QtCore.QObject): """ Cache of ophyd object descriptions. @@ -167,12 +179,15 @@

      Source code for typhos.cache

       
               self.connect_thread.start()
       
      -
      [docs] def clear(self): +
      +[docs] + def clear(self): """Clear the cache.""" self.connect_thread.clear() self.cache.clear() self._in_process.clear()
      + def _describe(self, obj): """Retrieve the description of ``obj``.""" try: @@ -190,13 +205,23 @@

      Source code for typhos.cache

       
               It calls describe, updates the cache, and emits a signal when done.
               """
      +        if obj not in self._in_process:
      +            # Cache was cleared before the signal was needed. Discard.
      +            return
      +
               try:
                   self.cache[obj] = desc = self._describe(obj)
      -            self.new_description.emit(obj, desc)
      +            if obj in self._in_process:
      +                self.new_description.emit(obj, desc)
               except Exception as ex:
                   logger.exception('Worker describe failed: %s', ex)
               finally:
      -            self._in_process.remove(obj)
      +            try:
      +                self._in_process.remove(obj)
      +            except KeyError:
      +                # The cache can be cleared externally. Don't fail if the object
      +                # is already gone.
      +                ...
       
           @QtCore.Slot(object, bool, dict)
           def _connection_update(self, obj, connected, metadata):
      @@ -214,7 +239,9 @@ 

      Source code for typhos.cache

                   utils.ThreadPoolWorker(func)
               )
       
      -
      [docs] def get(self, obj): +
      +[docs] + def get(self, obj): """ To access a description, call this method. If available, it will be returned immediately. Otherwise, upon connection and successful @@ -235,10 +262,14 @@

      Source code for typhos.cache

               except KeyError:
                   # Add the object, waiting for a connection update to determine
                   # widget types
      -            self.connect_thread.add_object(obj)
      + self.connect_thread.add_object(obj)
      +
      + -
      [docs]class _GlobalWidgetTypeCache(QtCore.QObject): +
      +[docs] +class _GlobalWidgetTypeCache(QtCore.QObject): """ Cache of ophyd object Typhos widget types. @@ -269,10 +300,13 @@

      Source code for typhos.cache

               self.describe_cache.new_description.connect(self._new_description,
                                                           QtCore.Qt.QueuedConnection)
       
      -
      [docs] def clear(self): +
      +[docs] + def clear(self): """Clear the cache.""" self.cache.clear()
      + @QtCore.Slot(object, dict) def _new_description(self, obj, desc): """New description: determine widget types and update the cache.""" @@ -286,7 +320,9 @@

      Source code for typhos.cache

               self.cache[obj] = item
               self.widgets_determined.emit(obj, item)
       
      -
      [docs] def get(self, obj): +
      +[docs] + def get(self, obj): """ To access widget types, call this method. If available, it will be returned immediately. Otherwise, upon connection and successful @@ -310,7 +346,9 @@

      Source code for typhos.cache

                   desc = self.describe_cache.get(obj)
                   if desc is not None:
                       # In certain scenarios (such as testing) this might happen
      -                self._new_description(obj, desc)
      + self._new_description(obj, desc)
      +
      + # The default stale cached_path threshold time, in seconds: @@ -399,7 +437,9 @@

      Source code for typhos.cache

                       yield self.path / pattern
       
       
      -
      [docs]class _GlobalDisplayPathCache: +
      +[docs] +class _GlobalDisplayPathCache: """ A cache for all configured display paths. @@ -413,13 +453,18 @@

      Source code for typhos.cache

               for path in utils.DISPLAY_PATHS:
                   self.add_path(path)
       
      -
      [docs] def update(self): +
      +[docs] + def update(self): """Force a reload of all paths in the cache.""" logger.debug('Clearing global path cache.') for path in self.paths: path.cache = None
      -
      [docs] def add_path(self, path): + +
      +[docs] + def add_path(self, path): """ Add a path to be searched during ``glob``. @@ -433,7 +478,9 @@

      Source code for typhos.cache

               path = _CachedPath(
                   path, stale_threshold=TYPHOS_DISPLAY_PATH_CACHE_TIME)
               if path not in self.paths:
      -            self.paths.append(path)
      + self.paths.append(path)
      +
      +
      diff --git a/master/_modules/typhos/display.html b/master/_modules/typhos/display.html index 8626a6c3a..1fe3a5a98 100644 --- a/master/_modules/typhos/display.html +++ b/master/_modules/typhos/display.html @@ -3,19 +3,19 @@ - typhos.display — Typhos 2.4.1 documentation + typhos.display — Typhos 3.0.0 documentation - - - - - - + + + + + + @@ -33,7 +33,7 @@ Typhos
      - 2.4.1 + 3.0.0

    Developer Documentation

      @@ -89,17 +90,20 @@

      Source code for typhos.display

       """Contains the main display widget used for representing an entire device."""
      +from __future__ import annotations
       
      +import copy
       import enum
       import inspect
       import logging
       import os
       import pathlib
       import webbrowser
      -from typing import Optional, Union
      +from typing import Dict, List, Optional, Union
       
       import ophyd
       import pcdsutils
      +import pydm
       import pydm.display
       import pydm.exception
       import pydm.utilities
      @@ -111,6 +115,7 @@ 

      Source code for typhos.display

       from . import panel as typhos_panel
       from . import utils, web, widgets
       from .jira import TyphosJiraIssueWidget
      +from .notes import TyphosNotesEdit
       from .plugins.core import register_signal
       
       logger = logging.getLogger(__name__)
      @@ -123,6 +128,15 @@ 

      Source code for typhos.display

           detailed_screen = 1
           engineering_screen = 2
       
      +    @property
      +    def friendly_name(self) -> str:
      +        """A user-friendly name for the display type."""
      +        return {
      +            self.embedded_screen: "Embedded",
      +            self.detailed_screen: "Detailed",
      +            self.engineering_screen: "Engineering",
      +        }[self]
      +
       
       _DisplayTypes = utils.pyqt_class_from_enum(DisplayTypes)
       DisplayTypes.names = [view.name for view in DisplayTypes]
      @@ -152,7 +166,9 @@ 

      Source code for typhos.display

                                    for f in files]
       
       
      -
      [docs]def normalize_display_type( +
      +[docs] +def normalize_display_type( display_type: Union[DisplayTypes, str, int] ) -> DisplayTypes: """ @@ -184,6 +200,7 @@

      Source code for typhos.display

                   )
      + def normalize_scroll_option( scroll_option: Union[ScrollOptions, str, int] ) -> ScrollOptions: @@ -216,7 +233,9 @@

      Source code for typhos.display

                   )
       
       
      -
      [docs]class TyphosToolButton(QtWidgets.QToolButton): +
      +[docs] +class TyphosToolButton(QtWidgets.QToolButton): """ Base class for tool buttons used in the TyphosDisplaySwitcher. @@ -251,11 +270,16 @@

      Source code for typhos.display

               if menu:
                   menu.exec_(QtGui.QCursor.pos())
       
      -
      [docs] def generate_context_menu(self): +
      +[docs] + def generate_context_menu(self): """Context menu request: override in subclasses.""" return None
      -
      [docs] @classmethod + +
      +[docs] + @classmethod def get_icon(cls, icon=None): """ Get a QIcon, if specified, or fall back to the default. @@ -271,7 +295,10 @@

      Source code for typhos.display

                   return pydm.utilities.IconFont().icon(icon)
               return icon
      -
      [docs] def open_context_menu(self, ev): + +
      +[docs] + def open_context_menu(self, ev): """ Open the instance-specific context menu. @@ -281,10 +308,14 @@

      Source code for typhos.display

               """
               menu = self.generate_context_menu()
               if menu:
      -            menu.exec_(self.mapToGlobal(ev.pos()))
      + menu.exec_(self.mapToGlobal(ev.pos()))
      +
      + -
      [docs]class TyphosDisplayConfigButton(TyphosToolButton): +
      +[docs] +class TyphosDisplayConfigButton(TyphosToolButton): """ The configuration button used in the :class:`TyphosDisplaySwitcher`. @@ -302,11 +333,16 @@

      Source code for typhos.display

               self.templates = None
               self.device_display = None
       
      -
      [docs] def set_device_display(self, device_display): +
      +[docs] + def set_device_display(self, device_display): """Typhos callback: set the :class:`TyphosDeviceDisplay`.""" self.device_display = device_display
      -
      [docs] def create_kind_filter_menu(self, panels, base_menu, *, only): + +
      +[docs] + def create_kind_filter_menu(self, panels, base_menu, *, only): """ Create the "Kind" filter menu. @@ -344,7 +380,10 @@

      Source code for typhos.display

                                             for panel in panels))
                   action.triggered.connect(selected)
      -
      [docs] def create_name_filter_menu(self, panels, base_menu): + +
      +[docs] + def create_name_filter_menu(self, panels, base_menu): """ Create the name-based filtering menu. @@ -381,7 +420,10 @@

      Source code for typhos.display

               action.setDefaultWidget(line_edit)
               base_menu.addAction(action)
      -
      [docs] def hide_empty(self, search=True): + +
      +[docs] + def hide_empty(self, search=True): """ Wrap hide_empty calls for use with search functions and action clicks. @@ -396,7 +438,10 @@

      Source code for typhos.display

                       show_empty(self.device_display)
                   hide_empty(self.device_display, process_widget=False)
      -
      [docs] def create_hide_empty_menu(self, panels, base_menu): + +
      +[docs] + def create_hide_empty_menu(self, panels, base_menu): """ Create the hide empty filtering menu. @@ -426,7 +471,10 @@

      Source code for typhos.display

               action.setChecked(self.device_display.hideEmpty)
               action.triggered.connect(handle_menu)
      -
      [docs] def generate_context_menu(self): + +
      +[docs] + def generate_context_menu(self): """ Generate the custom context menu. @@ -450,35 +498,34 @@

      Source code for typhos.display

               if not display:
                   return base_menu
       
      -        base_menu.addSection('Templates')
               display._generate_template_menu(base_menu)
       
               panels = display.findChildren(typhos_panel.TyphosSignalPanel) or []
      -        if not panels:
      -            return base_menu
      -
      -        base_menu.addSection('Filters')
      -        filter_menu = base_menu.addMenu("&Kind filter")
      -        self.create_kind_filter_menu(panels, filter_menu, only=False)
      -        filter_menu.addSeparator()
      -        self.create_kind_filter_menu(panels, filter_menu, only=True)
      -
      -        self.create_name_filter_menu(panels, base_menu)
      -
      -        base_menu.addSeparator()
      -        self.create_hide_empty_menu(panels, base_menu)
      -
      -        if utils.DEBUG_MODE:
      -            base_menu.addSection('Debug')
      -            action = base_menu.addAction('&Copy to clipboard')
      -            action.triggered.connect(display.copy_to_clipboard)
      +        if panels:
      +            base_menu.addSection('Filters')
      +            filter_menu = base_menu.addMenu("&Kind filter")
      +            self.create_kind_filter_menu(panels, filter_menu, only=False)
      +            filter_menu.addSeparator()
      +            self.create_kind_filter_menu(panels, filter_menu, only=True)
      +            self.create_name_filter_menu(panels, base_menu)
      +            base_menu.addSeparator()
      +            self.create_hide_empty_menu(panels, base_menu)
      +
      +        base_menu.addSection('Tools')
      +        action = base_menu.addAction('&Copy screenshot to clipboard')
      +        action.triggered.connect(display.copy_to_clipboard)
      +
      +        return base_menu
      +
      - return base_menu
      -
      [docs]class TyphosDisplaySwitcherButton(TyphosToolButton): +
      +[docs] +class TyphosDisplaySwitcherButton(TyphosToolButton): """A button which switches the TyphosDeviceDisplay template on click.""" + templates: Optional[List[pathlib.Path]] template_selected = QtCore.Signal(pathlib.Path) icons = {'embedded_screen': 'compress', @@ -490,38 +537,52 @@

      Source code for typhos.display

               super().__init__(icon=self.icons[display_type], parent=parent)
               self.templates = None
       
      -    def _clicked(self):
      +    def _clicked(self) -> None:
               """Clicked callback - set the template."""
               if self.templates is None:
                   logger.warning('set_device_display not called on %s', self)
                   return
       
      -        try:
      -            template = self.templates[0]
      -        except IndexError:
      -            return
      -
      -        self.template_selected.emit(template)
      +        # Show all our options in the context menu:
      +        super()._clicked()
       
      -
      [docs] def generate_context_menu(self): +
      +[docs] + def generate_context_menu(self) -> Optional[QtWidgets.QMenu]: """Context menu request.""" if not self.templates: - return + return None menu = QtWidgets.QMenu(parent=self) + menu.addSection("Switch to screen") + + prefix = os.path.commonprefix(list(str(tpl) for tpl in self.templates)) + if len(prefix) <= 1: + prefix = "" + for template in self.templates: - def selected(*, template=template): + def selected(*, template: pathlib.Path = template): self.template_selected.emit(template) - action = menu.addAction(template.name) + action = menu.addAction(str(template)[len(prefix):]) action.triggered.connect(selected) - return menu
      + return menu
      +
      + -
      [docs]class TyphosDisplaySwitcher(QtWidgets.QFrame, widgets.TyphosDesignerMixin): +
      +[docs] +class TyphosDisplaySwitcher(QtWidgets.QFrame, widgets.TyphosDesignerMixin): """Display switcher set of buttons for use with a TyphosDeviceDisplay.""" + help_toggle_button: TyphosHelpToggleButton + jira_report_button: Optional[TyphosJiraReportButton] + buttons: Dict[str, TyphosToolButton] + config_button: TyphosDisplayConfigButton + _jira_widget: TyphosJiraIssueWidget + template_selected = QtCore.Signal(pathlib.Path) def __init__(self, parent=None, **kwargs): @@ -543,23 +604,32 @@

      Source code for typhos.display

       
               self._create_ui()
       
      +
      +[docs] + def new_jira_widget(self): + """Open a new Jira issue reporting widget.""" + if self.device_display is None: + logger.warning('set_device_display not called on %s', self) + return + devices = self.device_display.devices + device = devices[0] if devices else None + self._jira_widget = TyphosJiraIssueWidget(device=device) + self._jira_widget.show()
      + + def _create_ui(self): layout = self.layout() self.buttons.clear() - self.help_button = None - self.config_button = None self.help_toggle_button = TyphosHelpToggleButton() layout.addWidget(self.help_toggle_button, 0, Qt.AlignRight) - for template_type in DisplayTypes.names: - button = TyphosDisplaySwitcherButton(template_type) - self.buttons[template_type] = button - button.template_selected.connect(self._template_selected) - layout.addWidget(button, 0, Qt.AlignRight) - - friendly_name = template_type.replace('_', ' ') - button.setToolTip(f'Switch to {friendly_name}') + if not utils.JIRA_URL: + self.jira_report_button = None + else: + self.jira_report_button = TyphosJiraReportButton() + self.jira_report_button.clicked.connect(self.new_jira_widget) + layout.addWidget(self.jira_report_button, 0, Qt.AlignRight) self.config_button = TyphosDisplayConfigButton() layout.addWidget(self.config_button, 0, Qt.AlignRight) @@ -571,21 +641,31 @@

      Source code for typhos.display

               if self.device_display is not None:
                   self.device_display.force_template = template
       
      -
      [docs] def set_device_display(self, display): + def _templates_loaded(self, templates: Dict[str, List[pathlib.Path]]) -> None: + ... + +
      +[docs] + def set_device_display(self, display: TyphosDeviceDisplay) -> None: """Typhos hook for setting the associated device display.""" self.device_display = display - - for template_type in self.buttons: - templates = display.templates.get(template_type, []) - self.buttons[template_type].templates = templates + display.templates_loaded.connect(self._templates_loaded) + self._templates_loaded(display.templates) self.config_button.set_device_display(display)
      -
      [docs] def add_device(self, device): + +
      +[docs] + def add_device(self, device): """Typhos hook for setting the associated device.""" - ...
      + ...
      +
      -
      [docs]class TyphosTitleLabel(QtWidgets.QLabel): + +
      +[docs] +class TyphosTitleLabel(QtWidgets.QLabel): """ A label class intended for use as a standardized title. @@ -598,12 +678,29 @@

      Source code for typhos.display

       
           toggle_requested = QtCore.Signal()
       
      -
      [docs] def mousePressEvent(self, event): +
      +[docs] + def mousePressEvent(self, event): """Overridden qt hook for a mouse press.""" if event.button() == Qt.LeftButton: self.toggle_requested.emit() - super().mousePressEvent(event)
      + super().mousePressEvent(event)
      +
      + + + +class TyphosJiraReportButton(TyphosToolButton): + """A standard button for Jira reporting with typhos.""" + + def __init__( + self, + icon: str = "exclamation", + parent: Optional[QtWidgets.QWidget] = None, + ): + super().__init__(icon, parent=parent) + + self.setToolTip("Report an issue about this device with Jira") class TyphosHelpToggleButton(TyphosToolButton): @@ -862,7 +959,9 @@

      Source code for typhos.display

                   self.hide_help()
       
       
      -
      [docs]class TyphosDisplayTitle(QtWidgets.QFrame, widgets.TyphosDesignerMixin): +
      +[docs] +class TyphosDisplayTitle(QtWidgets.QFrame, widgets.TyphosDesignerMixin): """ Standardized Typhos Device Display title. @@ -895,10 +994,13 @@

      Source code for typhos.display

               self.underline.setFrameShadow(self.underline.Plain)
               self.underline.setLineWidth(10)
       
      +        self.notes_edit = TyphosNotesEdit()
      +
               self.grid_layout = QtWidgets.QGridLayout()
               self.grid_layout.addWidget(self.label, 0, 0)
      -        self.grid_layout.addWidget(self.switcher, 0, 1, Qt.AlignRight)
      -        self.grid_layout.addWidget(self.underline, 1, 0, 1, 2)
      +        self.grid_layout.addWidget(self.switcher, 0, 2, Qt.AlignRight)
      +        self.grid_layout.addWidget(self.notes_edit, 0, 1, Qt.AlignLeft)
      +        self.grid_layout.addWidget(self.underline, 1, 0, 1, 3)
       
               self.help = TyphosHelpFrame()
               if utils.HELP_WEB_ENABLED:
      @@ -934,7 +1036,9 @@ 

      Source code for typhos.display

               self.show_switcher = show_switcher
               self.show_underline = show_underline
       
      -
      [docs] def toggle_help(self, show): +
      +[docs] + def toggle_help(self, show): """Toggle the help visibility.""" if self.help is None: return @@ -943,7 +1047,10 @@

      Source code for typhos.display

               if self.help.parent() is None:
                   self.grid_layout.addWidget(self.help, 2, 0, 1, 2)
      -
      [docs] def pop_out_help(self): + +
      +[docs] + def pop_out_help(self): """Pop out the help widget.""" if self.help is None: return @@ -954,6 +1061,7 @@

      Source code for typhos.display

               self.help.show()
               self.help.raise_()
      + @Property(bool) def show_switcher(self): """Get or set whether to show the display switcher.""" @@ -964,14 +1072,20 @@

      Source code for typhos.display

               self._show_switcher = bool(value)
               self.switcher.setVisible(self._show_switcher)
       
      -
      [docs] def add_device(self, device): +
      +[docs] + def add_device(self, device): """Typhos hook for setting the associated device.""" if not self.label.text(): self.label.setText(device.name) + if not self.notes_edit.text(): + self.notes_edit.setup_data(device.name) + if self.help is not None: self.help.add_device(device)
      + @QtCore.Property(bool) def show_underline(self): """Get or set whether to show the underline.""" @@ -982,7 +1096,9 @@

      Source code for typhos.display

               self._show_underline = bool(value)
               self.underline.setVisible(self._show_underline)
       
      -
      [docs] def set_device_display(self, display): +
      +[docs] + def set_device_display(self, display): """Typhos callback: set the :class:`TyphosDeviceDisplay`.""" self.device_display = display @@ -991,6 +1107,7 @@

      Source code for typhos.display

       
               self.label.toggle_requested.connect(toggle)
      + # Make designable properties from the title label available here as well label_alignment = forward_property('label', QtWidgets.QLabel, 'alignment') label_font = forward_property('label', QtWidgets.QLabel, 'font') @@ -1023,7 +1140,10 @@

      Source code for typhos.display

                                                     'midLineWidth')
      -
      [docs]class TyphosDeviceDisplay(utils.TyphosBase, widgets.TyphosDesignerMixin, + +
      +[docs] +class TyphosDeviceDisplay(utils.TyphosBase, widgets.TyphosDesignerMixin, _DisplayTypes): """ Main display for a single ophyd Device. @@ -1045,11 +1165,6 @@

      Source code for typhos.display

               layout.
               If omitted, scroll_option is used instead.
       
      -    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.
       
      @@ -1073,16 +1188,15 @@ 

      Source code for typhos.display

           # Template types and defaults
           Q_ENUMS(_DisplayTypes)
           TemplateEnum = DisplayTypes  # For convenience
      -
      -    device_count_threshold = 0
      -    signal_count_threshold = 30
      +    template_changed = QtCore.Signal(object)
      +    templates_loaded = QtCore.Signal(object)
      +    templates: Dict[str, List[pathlib.Path]]
       
           def __init__(
               self,
               parent: Optional[QtWidgets.QWidget] = None,
               *,
               scrollable: Optional[bool] = None,
      -        composite_heuristics: bool = True,
               embedded_templates: Optional[list[str]] = None,
               detailed_templates: Optional[list[str]] = None,
               engineering_templates: Optional[list[str]] = None,
      @@ -1090,7 +1204,6 @@ 

      Source code for typhos.display

               scroll_option: Union[ScrollOptions, str, int] = 'auto',
               nested: bool = False,
           ):
      -        self._composite_heuristics = composite_heuristics
               self._current_template = None
               self._forced_template = ''
               self._macros = {}
      @@ -1103,6 +1216,12 @@ 

      Source code for typhos.display

               self.templates = {name: [] for name in DisplayTypes.names}
               self._display_type = normalize_display_type(display_type)
       
      +        if nested and self._display_type == DisplayTypes.detailed_screen:
      +            # All nested displays should be embedded by default.
      +            # Based on if they have subdevices, they may become detailed
      +            # during the template loading process
      +            self._display_type = DisplayTypes.embedded_screen
      +
               instance_templates = {
                   'embedded_screen': embedded_templates or [],
                   'detailed_screen': detailed_templates or [],
      @@ -1135,15 +1254,6 @@ 

      Source code for typhos.display

                   else:
                       self.scroll_option = ScrollOptions.no_scroll
       
      -    @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(_ScrollOptions)
           def scroll_option(self) -> ScrollOptions:
               """Place the display in a scrollable area."""
      @@ -1169,53 +1279,118 @@ 

      Source code for typhos.display

               if checked != self._hide_empty:
                   self._hide_empty = checked
       
      +    @property
      +    def _layout_in_scroll_area(self) -> bool:
      +        """Layout the widget in the scroll area or not, based on settings."""
      +        if self.scroll_option == ScrollOptions.auto:
      +            if self.display_type == DisplayTypes.embedded_screen:
      +                return False
      +            return True
      +        elif self.scroll_option == ScrollOptions.scrollbar:
      +            return True
      +        elif self.scroll_option == ScrollOptions.no_scroll:
      +            return False
      +        return True
      +
           def _move_display_to_layout(self, widget):
               if not widget:
                   return
       
               widget.setParent(None)
      -        if self.scroll_option == ScrollOptions.auto:
      -            if self.display_type == DisplayTypes.embedded_screen:
      -                scrollable = False
      -            else:
      -                scrollable = True
      -        elif self.scroll_option == ScrollOptions.scrollbar:
      -            scrollable = True
      -        elif self.scroll_option == ScrollOptions.no_scroll:
      -            scrollable = False
      -        else:
      -            scrollable = True
       
      +        scrollable = self._layout_in_scroll_area
               if scrollable:
                   self._scroll_area.setWidget(widget)
               else:
      -            self.layout().addWidget(widget)
      +            layout: QtWidgets.QVBoxLayout = self.layout()
      +            layout.addWidget(widget, alignment=QtCore.Qt.AlignTop)
       
               self._scroll_area.setVisible(scrollable)
       
      -    def _generate_template_menu(self, base_menu):
      +    def _get_matching_templates_for_class(
      +        self,
      +        cls: type,
      +        display_type: DisplayTypes,
      +    ) -> List[pathlib.Path]:
      +        """Get matching templates for the given class."""
      +        class_name_prefix = f"{cls.__name__}."
      +        return [
      +            filename
      +            for filename in self.templates[display_type.name]
      +            if filename.name.startswith(class_name_prefix)
      +        ]
      +
      +    def _generate_template_menu(self, base_menu: QtWidgets.QMenu) -> None:
               """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())
      +        dev = self.device
      +        if dev is None:
      +            return
       
      -            for filename in filenames:
      -                def switch_template(*, filename=filename):
      -                    self.force_template = filename
      +        actions: List[QtWidgets.QAction] = []
      +
      +        def add_template(filename: pathlib.Path) -> None:
      +            def switch_template(*, filename: pathlib.Path = filename):
      +                self.force_template = filename
      +
      +            action = base_menu.addAction(str(filename))
      +            action.triggered.connect(switch_template)
      +            actions.append(action)
      +
      +            if self.current_template == filename:
      +                base_menu.setDefaultAction(action)
      +
      +        def add_header(label: str, icon: Optional[QtGui.QIcon] = None) -> None:
      +            action = QtWidgets.QWidgetAction(base_menu)
      +            label = QtWidgets.QLabel(label)
      +            label.setObjectName("menu_template_section")
      +            action.setDefaultWidget(label)
      +            if icon is not None:
      +                action.setIcon(icon)
      +            base_menu.addAction(action)
      +
      +        self._refresh_templates()
      +        seen = set()
      +
      +        for template_type in DisplayTypes:
      +            added_header = False
      +            for cls in type(dev).mro():
      +                matching = self._get_matching_templates_for_class(cls, template_type)
      +                templates = set(matching) - seen
      +                if not templates:
      +                    continue
      +
      +                def by_match_order(template: pathlib.Path) -> int:
      +                    return matching.index(template)
      +
      +                if not added_header:
      +                    add_header(
      +                        f"{template_type.friendly_name} screens",
      +                        icon=TyphosToolButton.get_icon(
      +                            TyphosDisplaySwitcherButton.icons[template_type.name]
      +                        ),
      +                    )
      +                    added_header = True
       
      -                action = menu.addAction(os.path.split(filename)[-1])
      -                action.triggered.connect(switch_template)
      +                base_menu.addSection(f"{cls.__name__}")
      +                for filename in sorted(templates, key=by_match_order):
      +                    add_template(filename)
       
      -        refresh_action = base_menu.addAction("Refresh Templates")
      -        refresh_action.triggered.connect(self._refresh_templates)
      +        add_header("Typhos default screens")
      +        for template in DEFAULT_TEMPLATES_FLATTEN:
      +            add_template(template)
      +
      +        prefix = os.path.commonprefix(
      +            [action.text() for action in actions]
      +        )
      +        # Arbitrary threshold: saving on a few characters is not worth it
      +        if len(prefix) > 9:
      +            for action in actions:
      +                action.setText(action.text()[len(prefix):])
       
           def _refresh_templates(self):
      -        """Context menu 'Refresh Templates' clicked."""
      -        # Force an update of the display cache.
      +        """Force an update of the display cache and look for new ui files."""
               cache.get_global_display_path_cache().update()
               self.search_for_templates()
      -        self.load_best_template()
       
           @property
           def current_template(self):
      @@ -1270,7 +1445,9 @@ 

      Source code for typhos.display

               except ValueError:
                   ...
       
      -
      [docs] def get_best_template(self, display_type, macros): +
      +[docs] + def get_best_template(self, display_type, macros): """ Get the best template for the given display type. @@ -1291,6 +1468,7 @@

      Source code for typhos.display

               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 @@ -1302,7 +1480,9 @@

      Source code for typhos.display

       
               self._display_widget = None
       
      -
      [docs] def load_best_template(self): +
      +[docs] + 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 @@ -1321,6 +1501,7 @@

      Source code for typhos.display

       
               if not template:
                   widget = QtWidgets.QWidget()
      +            widget.setObjectName("no_template_standin")
                   template = None
               else:
                   template = pathlib.Path(template)
      @@ -1344,10 +1525,14 @@ 

      Source code for typhos.display

                           pydm.exception.raise_to_operator(ex)
                       else:
                           widget = QtWidgets.QWidget()
      +                    widget.setObjectName("errored_load_standin")
                           template = None
       
               if widget:
      -            widget.setObjectName('display_widget')
      +            if widget.objectName():
      +                widget.setObjectName(f'{widget.objectName()}_display_widget')
      +            else:
      +                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.
      @@ -1369,7 +1554,21 @@ 

      Source code for typhos.display

               self._move_display_to_layout(self._display_widget)
       
               self._update_children()
      -        utils.reload_widget_stylesheet(self)
      + utils.reload_widget_stylesheet(self) + self.updateGeometry() + self.template_changed.emit(template)
      + + +
      +[docs] + def minimumSizeHint(self) -> QtCore.QSize: + if self._layout_in_scroll_area: + return QtCore.QSize( + int(self._scroll_area.viewportSizeHint().width() * 1.05), + super().minimumSizeHint().height(), + ) + return super().minimumSizeHint()
      + @property def display_widget(self): @@ -1404,10 +1603,18 @@

      Source code for typhos.display

               """Load template from file and return the widget."""
               filename = pathlib.Path(filename)
               loader = (pydm.display.load_py_file if filename.suffix == '.py'
      -                  else pydm.display.load_ui_file)
      +                  else utils.load_ui_file)
       
               logger.debug('Load template using %s: %r', loader.__name__, filename)
      -        return loader(str(filename), macros=self._macros)
      +        try:
      +            return loader(str(filename), macros=self._macros)
      +        except Exception as ex:
      +            display: Optional[pydm.Display] = getattr(ex, "pydm_display", None)
      +            if display is not None:
      +                display.setObjectName("_typhos_test_suite_ignore_")
      +                display.close()
      +                display.deleteLater()
      +            raise
       
           def _update_children(self):
               """Notify child widgets of this device display + the device."""
      @@ -1451,7 +1658,9 @@ 

      Source code for typhos.display

               result.update(**(macros or {}))
               return result
       
      -
      [docs] def add_device(self, device, macros=None): +
      +[docs] + def add_device(self, device, macros=None): """ Add a Device and signals to the TyphosDeviceDisplay. @@ -1486,9 +1695,15 @@

      Source code for typhos.display

                   register_signal(component_walk.item)
               self._searched = False
               self.macros = self._build_macros_from_device(device, macros=macros)
      -        self.load_best_template()
      + self.load_best_template() + + if not self.windowTitle(): + self.setWindowTitle(getattr(device, "name", ""))
      -
      [docs] def search_for_templates(self): + +
      +[docs] + def search_for_templates(self): """Search the filesystem for device-specific templates.""" device = self.device if not device: @@ -1516,12 +1731,7 @@

      Source code for typhos.display

                       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
      +            # 2. 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:
      @@ -1529,13 +1739,24 @@ 

      Source code for typhos.display

                           logger.debug('Found new template %s: %s (total=%d)',
                                        display_type, filename, len(template_list))
       
      +            # 3. Ensure that the detailed tree template makes its way in for
      +            #    all top-level screens, if no class-specific screen exists
      +            if DETAILED_TREE_TEMPLATE not in template_list:
      +                if not self._nested or self.suggest_composite_screen(cls):
      +                    template_list.append(DETAILED_TREE_TEMPLATE)
      +
                   # 4. Default templates
                   template_list.extend(
                       [templ for templ in DEFAULT_TEMPLATES[display_type]
                        if templ not in template_list]
      -            )
      + ) + + self.templates_loaded.emit(copy.deepcopy(self.templates))
      -
      [docs] @classmethod + +
      +[docs] + @classmethod def suggest_composite_screen(cls, device_cls): """ Suggest to use the composite screen for the given class. @@ -1545,33 +1766,15 @@

      Source code for typhos.display

               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
      +        for _, component in utils._get_top_level_components(device_cls):
      +            if issubclass(component.cls, ophyd.Device):
      +                return True
      +        return False
      - logger.debug( - '%s screens=%s num_signals=%d num_devices=%d -> composite=%s', - device_cls, specific_screens, num_signals, num_devices, composite - ) - return composite
      -
      [docs] @classmethod +
      +[docs] + @classmethod def from_device(cls, device, template=None, macros=None, **kwargs): """ Create a new TyphosDeviceDisplay from a Device. @@ -1600,7 +1803,10 @@

      Source code for typhos.display

               display.add_device(device, macros=macros)
               return display
      -
      [docs] @classmethod + +
      +[docs] + @classmethod def from_class(cls, klass, *, template=None, macros=None, **kwargs): """ Create a new TyphosDeviceDisplay from a Device class. @@ -1634,6 +1840,7 @@

      Source code for typhos.display

       
               return cls.from_device(obj, template=template, macros=macros)
      + @classmethod def _get_specific_screens(cls, device_cls): """ @@ -1650,7 +1857,9 @@

      Source code for typhos.display

                   if not utils.is_standard_template(template)
               ]
       
      -
      [docs] def to_image(self): +
      +[docs] + def to_image(self): """ Return the entire display as a QtGui.QImage. @@ -1662,7 +1871,10 @@

      Source code for typhos.display

               if self._display_widget is not None:
                   return utils.widget_to_image(self._display_widget)
      -
      [docs] @Slot() + +
      +[docs] + @Slot() def copy_to_clipboard(self): """Copy the display image to the clipboard.""" image = self.to_image() @@ -1670,6 +1882,7 @@

      Source code for typhos.display

                   clipboard = QtGui.QGuiApplication.clipboard()
                   clipboard.setImage(image)
      + @Slot(object) def _tx(self, value): """Receive information from happi channel.""" @@ -1685,7 +1898,10 @@

      Source code for typhos.display

               )
      -
      [docs]def toggle_display(widget, force_state=None): + +
      +[docs] +def toggle_display(widget, force_state=None): """ Toggle the visibility of all :class:`TyphosSignalPanel` in a display. @@ -1710,7 +1926,10 @@

      Source code for typhos.display

               panel.setVisible(state)
      -
      [docs]def show_empty(widget): + +
      +[docs] +def show_empty(widget): """ Recursively shows all panels and widgets, empty or not. @@ -1725,7 +1944,10 @@

      Source code for typhos.display

           toggle_display(widget, force_state=True)
      -
      [docs]def hide_empty(widget, process_widget=True): + +
      +[docs] +def hide_empty(widget, process_widget=True): """ Recursively hide empty panels and widgets. @@ -1766,6 +1988,7 @@

      Source code for typhos.display

               elif isinstance(widget, typhos_panel.TyphosSignalPanel):
                   overall_status = bool(widget._panel_layout.visible_elements)
               widget.setVisible(overall_status)
      +
      diff --git a/master/_modules/typhos/func.html b/master/_modules/typhos/func.html index 37afd51fe..1d5cc99ab 100644 --- a/master/_modules/typhos/func.html +++ b/master/_modules/typhos/func.html @@ -3,19 +3,19 @@ - typhos.func — Typhos 2.4.1 documentation + typhos.func — Typhos 3.0.0 documentation - - - - - - + + + + + + @@ -33,7 +33,7 @@ Typhos
      - 2.4.1 + 3.0.0

    Developer Documentation

      @@ -530,7 +531,9 @@

      Source code for typhos.func

               return QSize(175, 100)
       
       
      -
      [docs]class FunctionPanel(TogglePanel): +
      +[docs] +class FunctionPanel(TogglePanel): """ Function Panel. @@ -562,7 +565,9 @@

      Source code for typhos.func

               for method in methods:
                   self.add_method(method)
       
      -
      [docs] def add_method(self, func, *args, **kwargs): +
      +[docs] + def add_method(self, func, *args, **kwargs): """ Add a :class:`.FunctionDisplay`. @@ -585,10 +590,14 @@

      Source code for typhos.func

               # the first added method that the panel is visible
               self.show_contents(True)
               self.contents.layout().insertWidget(len(self.methods),
      -                                            widget)
      + widget)
      +
      + -
      [docs]class TyphosMethodButton(QPushButton, TyphosDesignerMixin): +
      +[docs] +class TyphosMethodButton(QPushButton, TyphosDesignerMixin): """ QPushButton to access a method of a Device. @@ -607,7 +616,9 @@

      Source code for typhos.func

               self.clicked.connect(self.execute)
               self.devices = list()
       
      -
      [docs] def add_device(self, device): +
      +[docs] + def add_device(self, device): """ Add a new device to the widget. @@ -618,6 +629,7 @@

      Source code for typhos.func

               logger.debug("Adding device %s ...", device.name)
               self.devices.append(device)
      + @Property(str) def method_name(self): """Name of method on provided Device to execute.""" @@ -638,7 +650,9 @@

      Source code for typhos.func

           def use_status(self, value):
               self._use_status = value
       
      -
      [docs] @Slot() +
      +[docs] + @Slot() def execute(self): """Execute the method given by ``method_name``.""" if not self.devices: @@ -691,12 +705,17 @@

      Source code for typhos.func

                   logger.debug("Starting TyphosStatusThread ...")
                   self._status_thread.start()
      -
      [docs] @classmethod + +
      +[docs] + @classmethod def from_device(cls, device, parent=None): """Create a TyphosMethodButton from a device.""" instance = cls(parent=parent) instance.add_device(device) - return instance
      + return instance
      +
      +
      diff --git a/master/_modules/typhos/panel.html b/master/_modules/typhos/panel.html index a0734863a..783b57994 100644 --- a/master/_modules/typhos/panel.html +++ b/master/_modules/typhos/panel.html @@ -3,19 +3,19 @@ - typhos.panel — Typhos 2.4.1 documentation + typhos.panel — Typhos 3.0.0 documentation - - - - - - + + + + + + @@ -33,7 +33,7 @@ Typhos
      - 2.4.1 + 3.0.0

    Developer Documentation

      @@ -100,9 +101,12 @@

      Source code for typhos.panel

           * :class:`TyphosCompositeSignalPanel`
       """
       
      +from __future__ import annotations
      +
       import functools
       import logging
       from functools import partial
      +from typing import Dict, List, Optional
       
       import ophyd
       from ophyd import Kind
      @@ -171,7 +175,9 @@ 

      Source code for typhos.panel

           """
       
       
      -
      [docs]class SignalPanel(QtWidgets.QGridLayout): +
      +[docs] +class SignalPanel(QtWidgets.QGridLayout): """ Basic panel layout for :class:`ophyd.Signal` and other ophyd objects. @@ -331,7 +337,9 @@

      Source code for typhos.panel

                   label.setToolTip(tooltip)
               return label
       
      -
      [docs] def add_signal(self, signal, name=None, *, tooltip=None): +
      +[docs] + def add_signal(self, signal, name=None, *, tooltip=None): """ Add a signal to the panel. @@ -388,6 +396,7 @@

      Source code for typhos.panel

               self._connect_signal(signal)
               return row
      + def _connect_signal(self, signal): """Instantiate widgets for the given signal using the global cache.""" monitor = get_global_widget_type_cache() @@ -433,7 +442,9 @@

      Source code for typhos.panel

       
               return row
       
      -
      [docs] def label_text_from_attribute(self, attr, dotted_name): +
      +[docs] + def label_text_from_attribute(self, attr, dotted_name): """ Get label text for a given attribute. @@ -443,7 +454,10 @@

      Source code for typhos.panel

               """
               return dotted_name
      -
      [docs] def add_row(self, *widgets, **kwargs): + +
      +[docs] + def add_row(self, *widgets, **kwargs): """ Add ``widgets`` to the next row. @@ -468,6 +482,7 @@

      Source code for typhos.panel

       
               return row
      + def _update_row(self, row, widgets, **kwargs): """ Update ``row`` to contain ``widgets``. @@ -497,7 +512,9 @@

      Source code for typhos.panel

                   colspan = self.NUM_COLS - last_column
                   self.addWidget(last_widget, row, last_column, 1, colspan, **kwargs)
       
      -
      [docs] def add_pv(self, read_pv, name, write_pv=None): +
      +[docs] + def add_pv(self, read_pv, name, write_pv=None): """ Add a row, given PV names. @@ -526,6 +543,7 @@

      Source code for typhos.panel

                   sig = EpicsSignalRO(read_pv, name=name)
               return self.add_signal(sig, name)
      + @staticmethod def _apply_name_filter(filter_by, *items): """ @@ -544,7 +562,16 @@

      Source code for typhos.panel

       
               return any(filter_by in item for item in items)
       
      -    def _should_show(self, kind, name, *, kinds, name_filter):
      +    def _should_show(
      +        self,
      +        kind: ophyd.Kind,
      +        name: str,
      +        *,
      +        kinds: list[ophyd.Kind],
      +        name_filter: Optional[str] = None,
      +        show_names: Optional[list[str]] = None,
      +        omit_names: Optional[list[str]] = None,
      +    ):
               """
               Based on the filter settings, indicate if ``signal`` should be shown.
       
      @@ -559,15 +586,29 @@ 

      Source code for typhos.panel

               kinds : list of :class:`ophyd.Kind`
                   Kinds that should be shown.
       
      -        name_filter : str
      -            Name filter text.
      +        name_filter : str, optional
      +            Name filter text - show only signals that match this string. This
      +            is applied after the "omit_names" and "show_names" filters.
      +
      +        show_names : list of str, optinoal
      +            Names to explicitly show.  Applied before the omit filter.
      +
      +        omit_names : list of str, optinoal
      +            Names to explicitly omit.
       
               Returns
               -------
               should_show : bool
               """
      +        kind = Kind(kind)
               if kind not in kinds:
                   return False
      +        for show_name in (show_names or []):
      +            if show_name and show_name in name:
      +                return True
      +        for omit_name in (omit_names or []):
      +            if omit_name and omit_name in name:
      +                return False
               return self._apply_name_filter(name_filter, name)
       
           def _set_visible(self, signal_name, visible):
      @@ -618,7 +659,15 @@ 

      Source code for typhos.panel

                   del self.signal_name_to_info[signal_name]
               self._connect_signal(signal)
       
      -
      [docs] def filter_signals(self, kinds, name_filter=None): +
      +[docs] + def filter_signals( + self, + kinds: list[ophyd.Kind], + name_filter: Optional[str] = None, + show_names: Optional[list[str]] = None, + omit_names: Optional[list[str]] = None, + ): """ Filter signals based on the given kinds. @@ -628,15 +677,29 @@

      Source code for typhos.panel

                   List of kinds to show.
       
               name_filter : str, optional
      -            Additionally filter signals by name.
      +            Name filter text - show only signals that match this string. This
      +            is applied after the "omit_names" and "show_names" filters.
      +
      +        show_names : list of str, optinoal
      +            Names to explicitly show.  Applied before the omit filter.
      +
      +        omit_names : list of str, optinoal
      +            Names to explicitly omit.
               """
               for name, info in list(self.signal_name_to_info.items()):
                   item = info['signal'] or info['component']
      -            visible = self._should_show(item.kind, name,
      -                                        kinds=kinds, name_filter=name_filter)
      +            visible = self._should_show(
      +                item.kind,
      +                name,
      +                kinds=kinds,
      +                name_filter=name_filter,
      +                omit_names=omit_names,
      +                show_names=show_names,
      +            )
                   self._set_visible(name, visible)
       
               self.update()
      + # utils.dump_grid_layout(self) @property @@ -644,7 +707,9 @@

      Source code for typhos.panel

               """Get the current filter settings from the owner widget."""
               return self.parent().filter_settings
       
      -
      [docs] def add_device(self, device): +
      +[docs] + def add_device(self, device): """Typhos hook for adding a new device.""" self.clear() self._devices.append(device) @@ -662,6 +727,7 @@

      Source code for typhos.panel

       
               self.setSizeConstraint(self.SetMinimumSize)
      + def _maybe_add_signal(self, device, attr, dotted_name, component): """ With the filter settings, add either the signal or a component stub. @@ -712,15 +778,21 @@

      Source code for typhos.panel

       
               return self._add_component(device, attr, dotted_name, component)
       
      -
      [docs] def clear(self): +
      +[docs] + def clear(self): """Clear the SignalPanel.""" logger.debug("Clearing layout %r ...", self) utils.clear_layout(self) self._devices.clear() - self.signal_name_to_info.clear()
      + self.signal_name_to_info.clear()
      +
      + -
      [docs]class TyphosSignalPanel(TyphosBase, TyphosDesignerMixin, SignalOrder): +
      +[docs] +class TyphosSignalPanel(TyphosBase, TyphosDesignerMixin, SignalOrder): """ Panel of Signals for a given device, using :class:`SignalPanel`. @@ -735,6 +807,7 @@

      Source code for typhos.panel

       
           Q_ENUMS(SignalOrder)  # Necessary for display in Designer
           SignalOrder = SignalOrder  # For convenience
      +    _kinds: Dict[str, Kind]
           # From top of page to bottom
           kind_order = (Kind.hinted, Kind.normal,
                         Kind.config, Kind.omitted)
      @@ -754,18 +827,25 @@ 

      Source code for typhos.panel

               self._panel_layout = self._panel_class()
               self.setLayout(self._panel_layout)
               self._name_filter = ''
      +        self._show_names = []
      +        self._omit_names = []
               # Add default Kind values
      -        self._kinds = dict.fromkeys([kind.name for kind in Kind], True)
      +        self._kinds = {
      +            "normal": True,
      +            "hinted": True,
      +            "config": True,
      +            "omitted": True,
      +        }
               self._signal_order = SignalOrder.byKind
       
               self.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)
               self.contextMenuEvent = self.open_context_menu
       
      -    def _get_kind(self, kind):
      +    def _get_kind(self, kind: str) -> ophyd.Kind:
               """Property getter for show[kind]."""
               return self._kinds[kind]
       
      -    def _set_kind(self, value, kind):
      +    def _set_kind(self, value: bool, kind: str) -> None:
               """Property setter for show[kind] = value."""
               # If we have a new value store it
               if value != self._kinds[kind]:
      @@ -779,6 +859,8 @@ 

      Source code for typhos.panel

               """Get the filter settings dictionary."""
               return dict(
                   name_filter=self.nameFilter,
      +            omit_names=self.omitNames,
      +            show_names=self.showNames,
                   kinds=self.show_kinds,
               )
       
      @@ -788,9 +870,9 @@ 

      Source code for typhos.panel

               self.updated.emit()
       
           @property
      -    def show_kinds(self):
      +    def show_kinds(self) -> List[Kind]:
               """Get a list of the :class:`ophyd.Kind` that should be shown."""
      -        return [kind for kind in Kind if self._kinds[kind.name]]
      +        return [Kind[kind] for kind, show in self._kinds.items() if show]
       
           # Kind Configuration pyqtProperty
           showHints = Property(bool,
      @@ -811,16 +893,38 @@ 

      Source code for typhos.panel

                                  doc='Show ophyd.Kind.omitted signals')
       
           @Property(str)
      -    def nameFilter(self):
      +    def nameFilter(self) -> str:
               """Get or set the current name filter."""
               return self._name_filter
       
           @nameFilter.setter
      -    def nameFilter(self, name_filter):
      +    def nameFilter(self, name_filter: str):
               if name_filter != self._name_filter:
                   self._name_filter = name_filter.strip()
                   self._update_panel()
       
      +    @Property("QStringList")
      +    def omitNames(self) -> list[str]:
      +        """Get or set the list of names to omit."""
      +        return self._omit_names
      +
      +    @omitNames.setter
      +    def omitNames(self, omit_names: Optional[list[str]]) -> None:
      +        if omit_names != self._omit_names:
      +            self._omit_names = list(omit_names or [])
      +            self._update_panel()
      +
      +    @Property("QStringList")
      +    def showNames(self) -> list[str]:
      +        """Get or set the list of names to omit."""
      +        return self._show_names
      +
      +    @showNames.setter
      +    def showNames(self, show_names: Optional[list[str]]) -> None:
      +        if show_names != self._show_names:
      +            self._show_names = list(show_names or [])
      +            self._update_panel()
      +
           @Property(SignalOrder)
           def sortBy(self):
               """Get or set the order that the signals will be placed in layout."""
      @@ -832,7 +936,9 @@ 

      Source code for typhos.panel

                   self._signal_order = value
                   self._update_panel()
       
      -
      [docs] def add_device(self, device): +
      +[docs] + def add_device(self, device): """Typhos hook for adding a new device.""" self.devices.clear() super().add_device(device) @@ -840,11 +946,17 @@

      Source code for typhos.panel

               self._panel_layout.add_device(device)
               self._update_panel()
      -
      [docs] def set_device_display(self, display): + +
      +[docs] + def set_device_display(self, display): """Typhos hook for when the TyphosDeviceDisplay is associated.""" self.display = display
      -
      [docs] def generate_context_menu(self): + +
      +[docs] + def generate_context_menu(self): """Generate a context menu for this TyphosSignalPanel.""" menu = QtWidgets.QMenu(parent=self) menu.addSection('Kinds') @@ -858,7 +970,10 @@

      Source code for typhos.panel

                   action.triggered.connect(selected)
               return menu
      -
      [docs] def open_context_menu(self, ev): + +
      +[docs] + def open_context_menu(self, ev): """ Open a context menu when the Default Context Menu is requested. @@ -867,10 +982,14 @@

      Source code for typhos.panel

               ev : QEvent
               """
               menu = self.generate_context_menu()
      -        menu.exec_(self.mapToGlobal(ev.pos()))
      + menu.exec_(self.mapToGlobal(ev.pos()))
      +
      + -
      [docs]class CompositeSignalPanel(SignalPanel): +
      +[docs] +class CompositeSignalPanel(SignalPanel): """ Composite panel layout for :class:`ophyd.Signal` and other ophyd objects. @@ -905,12 +1024,17 @@

      Source code for typhos.panel

               super().__init__(signals=None)
               self._containers = {}
       
      -
      [docs] def label_text_from_attribute(self, attr, dotted_name): +
      +[docs] + def label_text_from_attribute(self, attr, dotted_name): """Get label text for a given attribute.""" # For a hierarchical signal panel, use only the attribute name. return attr
      -
      [docs] def add_sub_device(self, device, name): + +
      +[docs] + def add_sub_device(self, device, name): """ Add a sub-device to the next row. @@ -924,14 +1048,18 @@

      Source code for typhos.panel

               """
               logger.debug('%s adding sub-device: %s (%s)', self.__class__.__name__,
                            device.name, device.__class__.__name__)
      -        container = display.TyphosDeviceDisplay(scrollable=False,
      -                                                composite_heuristics=True,
      -                                                nested=True)
      +        container = display.TyphosDeviceDisplay(
      +            scrollable=False,
      +            nested=True,
      +        )
               self._containers[name] = container
               self.add_row(container)
               container.add_device(device)
      -
      [docs] def add_device(self, device): + +
      +[docs] + def add_device(self, device): """Typhos hook for adding a new device.""" # TODO: note that this does not call super # super().add_device(device) @@ -948,6 +1076,7 @@

      Source code for typhos.panel

                   else:
                       self._maybe_add_signal(device, attr, attr, component)
      + @property def visible_elements(self): """Return all visible signals and components.""" @@ -960,7 +1089,10 @@

      Source code for typhos.panel

               return sigs
      -
      [docs]class TyphosCompositeSignalPanel(TyphosSignalPanel): + +
      +[docs] +class TyphosCompositeSignalPanel(TyphosSignalPanel): """ Hierarchical panel for a device, using :class:`CompositeSignalPanel`. @@ -974,6 +1106,7 @@

      Source code for typhos.panel

           """
       
           _panel_class = CompositeSignalPanel
      +
      diff --git a/master/_modules/typhos/plugins/core.html b/master/_modules/typhos/plugins/core.html index 508cf7a27..9f01a1b3d 100644 --- a/master/_modules/typhos/plugins/core.html +++ b/master/_modules/typhos/plugins/core.html @@ -3,19 +3,19 @@ - typhos.plugins.core — Typhos 2.4.1 documentation + typhos.plugins.core — Typhos 3.0.0 documentation - - - - - - + + + + + + @@ -33,7 +33,7 @@ Typhos
      - 2.4.1 + 3.0.0

    Developer Documentation

      @@ -105,7 +106,9 @@

      Source code for typhos.plugins.core

       signal_registry = dict()
       
       
      -
      [docs]def register_signal(signal): +
      +[docs] +def register_signal(signal): """ Add a new Signal to the registry. @@ -147,7 +150,10 @@

      Source code for typhos.plugins.core

               signal_registry[name] = signal
      -
      [docs]class SignalConnection(PyDMConnection): + +
      +[docs] +class SignalConnection(PyDMConnection): """ Connection to monitor an Ophyd Signal. @@ -169,7 +175,9 @@

      Source code for typhos.plugins.core

           def __init__(self, channel, address, protocol=None, parent=None):
               # Create base connection
               super().__init__(channel, address, protocol=protocol, parent=parent)
      +        self._connection_open = True
               self.signal_type = None
      +        self.is_float = False
               # Collect our signal
               self.signal = signal_registry[address]
               # Subscribe to updates from Ophyd
      @@ -184,7 +192,13 @@ 

      Source code for typhos.plugins.core

               # Add listener
               self.add_listener(channel)
       
      -
      [docs] def cast(self, value): + def __dtor__(self) -> None: + self._connection_open = False + self.close() + +
      +[docs] + def cast(self, value): """ Cast a value to the correct Python type based on ``signal_type``. @@ -211,7 +225,10 @@

      Source code for typhos.plugins.core

                   value = self.signal_type(value)
               return value
      -
      [docs] @Slot(int) + +
      +[docs] + @Slot(int) @Slot(float) @Slot(str) @Slot(np.ndarray) @@ -231,10 +248,16 @@

      Source code for typhos.plugins.core

                   logger.exception("Unable to put %r to %s", new_val, self.address)
                   raise_to_operator(exc)
      -
      [docs] def send_new_value(self, value=None, **kwargs): + +
      +[docs] + def send_new_value(self, value=None, **kwargs): """ Update the UI with a new value from the Signal. """ + if not self._connection_open: + return + try: value = self.cast(value) self.new_value_signal[self.signal_type].emit(value) @@ -242,7 +265,10 @@

      Source code for typhos.plugins.core

                   logger.exception("Unable to update %r with value %r.",
                                    self.signal.name, value)
      -
      [docs] def send_new_meta( + +
      +[docs] + def send_new_meta( self, connected=None, write_access=None, @@ -263,12 +289,24 @@

      Source code for typhos.plugins.core

               but for severity we default to NO_ALARM for UI purposes. We don't
               want the UI to assume that anything is in an alarm state.
               """
      +        if not self._connection_open:
      +            return
      +
               # Only emit the non-None values
               if connected is not None:
                   self.connection_state_signal.emit(connected)
               if write_access is not None:
                   self.write_access_signal.emit(write_access)
               if precision is not None:
      +            if precision <= 0:
      +                # Help the user a bit by replacing a clear design error
      +                # with a sensible default
      +                if self.is_float:
      +                    # Float precision at 0 is unhelpful
      +                    precision = 3
      +                else:
      +                    # Integer precision can't be negative
      +                    precision = 0
                   self.prec_signal.emit(precision)
               if units is not None:
                   self.unit_signal.emit(units)
      @@ -280,7 +318,10 @@ 

      Source code for typhos.plugins.core

                   severity = AlarmSeverity.NO_ALARM
               self.new_severity_signal.emit(severity)
      -
      [docs] def add_listener(self, channel): + +
      +[docs] + def add_listener(self, channel): """ Add a listener channel to this connection. @@ -301,6 +342,18 @@

      Source code for typhos.plugins.core

                                    "from signal %r to initialize %r",
                                    self.signal.name, channel)
                   return
      +        if isinstance(signal_val, (float, np.floating)):
      +            # Precision is commonly omitted from non-epics signals
      +            # Pick a sensible default for displaying floats
      +            self.is_float = True
      +            # precision might be missing entirely
      +            signal_meta.setdefault("precision", 3)
      +            # precision might be None, which is code for unset
      +            if signal_meta["precision"] is None:
      +                signal_meta["precision"] = 3
      +        else:
      +            self.is_float = False
      +
               # Report new value
               self.send_new_value(signal_val)
               self.send_new_meta(**signal_meta)
      @@ -315,7 +368,10 @@ 

      Source code for typhos.plugins.core

                           logger.debug("%s has no value_signal for type %s",
                                        channel.address, _typ)
      -
      [docs] def remove_listener(self, channel, destroying=False, **kwargs): + +
      +[docs] + def remove_listener(self, channel, destroying=False, **kwargs): """ Remove a listener channel from this connection. @@ -335,13 +391,20 @@

      Source code for typhos.plugins.core

               super().remove_listener(channel, destroying=destroying, **kwargs)
               logger.debug("Successfully removed %r", channel)
      -
      [docs] def close(self): + +
      +[docs] + def close(self): """Unsubscribe from the Ophyd signal.""" self.signal.unsubscribe(self.value_cid) - self.signal.unsubscribe(self.meta_cid)
      + self.signal.unsubscribe(self.meta_cid)
      +
      -
      [docs]class SignalPlugin(PyDMPlugin): + +
      +[docs] +class SignalPlugin(PyDMPlugin): """Plugin registered with PyDM to handle SignalConnection.""" protocol = 'sig' connection_class = SignalConnection @@ -360,7 +423,19 @@

      Source code for typhos.plugins.core

                                channel)
               except Exception:
                   logger.exception("Unable to create a connection to %r",
      -                             channel)
      + channel) + + def remove_connection(self, channel, destroying=False): + try: + return super().remove_connection(channel, destroying=destroying) + except RuntimeError as ex: + # deleteLater() at teardown can raise; let's silence that + if not str(ex).endswith("has been deleted"): + raise + + with self.lock: + self.connections.pop(self.get_connection_id(channel), None)
      +
      diff --git a/master/_modules/typhos/plugins/happi.html b/master/_modules/typhos/plugins/happi.html index 5245db457..409d8cab8 100644 --- a/master/_modules/typhos/plugins/happi.html +++ b/master/_modules/typhos/plugins/happi.html @@ -3,19 +3,19 @@ - typhos.plugins.happi — Typhos 2.4.1 documentation + typhos.plugins.happi — Typhos 3.0.0 documentation - - - - - - + + + + + + @@ -33,7 +33,7 @@ Typhos
      - 2.4.1 + 3.0.0

    Developer Documentation

      @@ -104,7 +105,9 @@

      Source code for typhos.plugins.happi

       logger = logging.getLogger(__name__)
       
       
      -
      [docs]def register_client(client): +
      +[docs] +def register_client(client): """ Register a Happi Client to be used with the DataPlugin. @@ -114,7 +117,10 @@

      Source code for typhos.plugins.happi

           HappiClientState.client = client
      -
      [docs]class HappiConnection(PyDMConnection): + +
      +[docs] +class HappiConnection(PyDMConnection): """A PyDMConnection to the Happi Database.""" tx = QtCore.Signal(dict) @@ -122,7 +128,9 @@

      Source code for typhos.plugins.happi

               super().__init__(channel, address, protocol=protocol, parent=parent)
               self.add_listener(channel)
       
      -
      [docs] def add_listener(self, channel): +
      +[docs] + def add_listener(self, channel): """Add a new channel to the existing connection.""" super().add_listener(channel) # Connect our channel to the signal @@ -145,14 +153,21 @@

      Source code for typhos.plugins.happi

               # Send the device and metdata to all of our subscribers
               self.tx.emit({'obj': obj, 'md': md})
      -
      [docs] def remove_listener(self, channel, destroying=False, **kwargs): + +
      +[docs] + def remove_listener(self, channel, destroying=False, **kwargs): """Remove a channel from the database connection.""" super().remove_listener(channel, destroying=destroying, **kwargs) if not destroying: - self.tx.disconnect(channel.tx_slot)
      + self.tx.disconnect(channel.tx_slot)
      +
      -
      [docs]class HappiPlugin(PyDMPlugin): + +
      +[docs] +class HappiPlugin(PyDMPlugin): protocol = 'happi' connection_class = HappiConnection @@ -171,6 +186,7 @@

      Source code for typhos.plugins.happi

                                    exc, channel.address)
               except Exception:
                   logger.exception("Unable to load %r from happi", channel.address)
      +
      diff --git a/master/_modules/typhos/positioner.html b/master/_modules/typhos/positioner.html index 99fad5206..845c0a090 100644 --- a/master/_modules/typhos/positioner.html +++ b/master/_modules/typhos/positioner.html @@ -3,19 +3,19 @@ - typhos.positioner — Typhos 2.4.1 documentation + typhos.positioner — Typhos 3.0.0 documentation - - - - - - + + + + + + @@ -33,7 +33,7 @@ Typhos
      - 2.4.1 + 3.0.0

    Developer Documentation

      @@ -88,21 +89,81 @@

      Source code for typhos.positioner

      -import logging
      +from __future__ import annotations
      +
      +import inspect
      +import logging
       import os.path
       import threading
      +import typing
      +from typing import Optional, Union
       
      +import ophyd
       from pydm.widgets.channel import PyDMChannel
       from qtpy import QtCore, QtWidgets, uic
       
      -from . import utils, widgets
      -from .alarm import KindLevel, _KindLevel
      +from typhos.display import TyphosDisplaySwitcher
      +
      +from . import dynamic_font, utils, widgets
      +from .alarm import AlarmLevel, KindLevel, _KindLevel
      +from .panel import SignalOrder, TyphosSignalPanel
       from .status import TyphosStatusThread
       
       logger = logging.getLogger(__name__)
       
      -
      -
      [docs]class TyphosPositionerWidget( +if typing.TYPE_CHECKING: + import pydm.widgets + + from .alarm import TyphosAlarmRectangle + from .notes import TyphosNotesEdit + from .related_display import TyphosRelatedSuiteButton + + +class _TyphosPositionerUI(QtWidgets.QWidget): + """Annotations helper for positioner.ui; not to be instantiated.""" + + alarm_circle: TyphosAlarmRectangle + alarm_label: QtWidgets.QLabel + alarm_layout: QtWidgets.QVBoxLayout + app: QtWidgets.QApplication + clear_error_button: QtWidgets.QPushButton + device_name_label: QtWidgets.QLabel + devices: list + error_label: pydm.widgets.label.PyDMLabel + expand_button: QtWidgets.QPushButton + expert_button: TyphosRelatedSuiteButton + high_limit: pydm.widgets.label.PyDMLabel + high_limit_layout: QtWidgets.QVBoxLayout + high_limit_switch: pydm.widgets.byte.PyDMByteIndicator + horizontalLayout: QtWidgets.QHBoxLayout + low_limit: pydm.widgets.label.PyDMLabel + low_limit_layout: QtWidgets.QVBoxLayout + low_limit_switch: pydm.widgets.byte.PyDMByteIndicator + moving_indicator: pydm.widgets.byte.PyDMByteIndicator + moving_indicator_label: QtWidgets.QLabel + moving_indicator_layout: QtWidgets.QVBoxLayout + row_frame: QtWidgets.QFrame + setpoint_layout: QtWidgets.QVBoxLayout + setpoint_outer_layout: QtWidgets.QVBoxLayout + status_container_widget: QtWidgets.QWidget + status_label: QtWidgets.QLabel + status_text_layout: QtWidgets.QVBoxLayout + stop_button: QtWidgets.QPushButton + tweak_layout: QtWidgets.QHBoxLayout + tweak_negative: QtWidgets.QToolButton + tweak_positive: QtWidgets.QToolButton + tweak_value: QtWidgets.QLineEdit + tweak_widget: QtWidgets.QWidget + user_readback: pydm.widgets.label.PyDMLabel + user_setpoint: pydm.widgets.line_edit.PyDMLineEdit + + # Dynamically added: + set_value: Union[widgets.NoScrollComboBox, QtWidgets.QLineEdit] + + +
      +[docs] +class TyphosPositionerWidget( utils.TyphosBase, widgets.TyphosDesignerMixin, _KindLevel, @@ -171,6 +232,7 @@

      Source code for typhos.positioner

           QtCore.Q_ENUMS(_KindLevel)
           KindLevel = KindLevel
       
      +    ui: _TyphosPositionerUI
           ui_template = os.path.join(utils.ui_dir, 'widgets', 'positioner.ui')
           _readback_attr = 'user_readback'
           _setpoint_attr = 'user_setpoint'
      @@ -184,6 +246,14 @@ 

      Source code for typhos.positioner

           _error_message_attr = 'error_message'
           _min_visible_operation = 0.1
       
      +    alarm_text = {
      +        AlarmLevel.NO_ALARM: 'no alarm',
      +        AlarmLevel.MINOR: 'minor',
      +        AlarmLevel.MAJOR: 'major',
      +        AlarmLevel.DISCONNECTED: 'no conn',
      +        AlarmLevel.INVALID: 'invalid',
      +    }
      +
           def __init__(self, parent=None):
               self._moving = False
               self._last_move = None
      @@ -195,7 +265,7 @@ 

      Source code for typhos.positioner

       
               super().__init__(parent=parent)
       
      -        self.ui = uic.loadUi(self.ui_template, self)
      +        self.ui = typing.cast(_TyphosPositionerUI, uic.loadUi(self.ui_template, self))
               self.ui.tweak_positive.clicked.connect(self.positive_tweak)
               self.ui.tweak_negative.clicked.connect(self.negative_tweak)
               self.ui.stop_button.clicked.connect(self.stop)
      @@ -207,6 +277,8 @@ 

      Source code for typhos.positioner

               self.show_expert_button = False
               self._after_set_moving(False)
       
      +        dynamic_font.patch_widget(self.ui.user_readback, pad_percent=0.01)
      +
           def _clear_status_thread(self):
               """Clear a previous status thread."""
               if self._status_thread is None:
      @@ -227,8 +299,45 @@ 

      Source code for typhos.positioner

               thread.status_finished.connect(self._status_finished)
               thread.start()
       
      -    def _get_timeout(self, set_position, settle_time):
      -        """Use positioner's configuration to select a timeout."""
      +    def _get_timeout(self, set_position: float, settle_time: float, rescale: float = 1) -> float | None:
      +        """
      +        Use positioner's configuration to select a timeout.
      +
      +        This will estimate the amount of time it will take to get to the
      +        set_position. The calculation is simplified and is intended to be
      +        slightly greater than the true expected move duration:
      +
      +        move_time ~= distance/velocity + accel_time + deccel_time
      +        *(note: we assume accel_time = deccel_time)
      +
      +        Which is just the trapezoidal move curve, but a little bit longer.
      +
      +        The timeout will be:
      +        timeout = settle_time + rescale * move_time
      +
      +        A return value of ``None`` will be used if we cannot determine the
      +        velocity, which is interpreted by ophyd as "never times out".
      +        If we can't determine the acceleration time, we will assume it is
      +        zero.
      +
      +        Parameters
      +        ----------
      +        set_position : float
      +            The position we'd like to move to.
      +        settle_time : float
      +            How long to wait on top of the calculated move time.
      +            Note that this does not get ``rescale`` applied on top of it.
      +        rescale : float
      +            A scaling factor, multiplied onto the calculated move time.
      +            This can be used to give some extra margin proportional to
      +            the expected move time, e.g. for long moves.
      +
      +        Returns
      +        -------
      +        timeout : float or None
      +            The timeout to use for this move, or None if a timeout could
      +            not be calculated.
      +        """
               pos_sig = getattr(self.device, self._readback_attr, None)
               vel_sig = getattr(self.device, self._velocity_attr, None)
               acc_sig = getattr(self.device, self._acceleration_attr, None)
      @@ -246,7 +355,7 @@ 

      Source code for typhos.positioner

               else:
                   acc_time = acc_sig.get()
               # This time is always greater than the kinematic calc
      -        return abs(delta/speed) + 2 * abs(acc_time) + abs(settle_time)
      +        return rescale * (abs(delta/speed) + 2 * abs(acc_time)) + abs(settle_time)
       
           def _set(self, value):
               """Inner `set` routine - call device.set() and monitor the status."""
      @@ -258,7 +367,8 @@ 

      Source code for typhos.positioner

                   set_position = float(value)
       
               try:
      -            timeout = self._get_timeout(set_position, 5)
      +            # Always at least 5s, give 20% extra time as margin for long moves
      +            timeout = self._get_timeout(set_position, settle_time=5, rescale=1.2)
               except Exception:
                   # Something went wrong, just run without a timeout.
                   logger.exception('Unable to estimate motor timeout.')
      @@ -279,12 +389,15 @@ 

      Source code for typhos.positioner

           def combo_set(self, index):
               self.set()
       
      -
      [docs] @QtCore.Slot() +
      +[docs] + @QtCore.Slot() def set(self): """Set the device to the value configured by ``ui.set_value``""" if not self.device: return + value = None try: if isinstance(self.ui.set_value, widgets.NoScrollComboBox): value = self.ui.set_value.currentText() @@ -297,7 +410,10 @@

      Source code for typhos.positioner

                   utils.reload_widget_stylesheet(self, cascade=True)
                   utils.raise_to_operator(exc)
      -
      [docs] def tweak(self, offset): + +
      +[docs] + def tweak(self, offset): """Tweak by the given ``offset``.""" try: setpoint = self._get_position() + float(offset) @@ -308,7 +424,10 @@

      Source code for typhos.positioner

               self.ui.set_value.setText(str(setpoint))
               self.set()
      -
      [docs] @QtCore.Slot() + +
      +[docs] + @QtCore.Slot() def positive_tweak(self): """Tweak positive by the amount listed in ``ui.tweak_value``""" try: @@ -316,7 +435,10 @@

      Source code for typhos.positioner

               except Exception:
                   logger.exception('Tweak failed')
      -
      [docs] @QtCore.Slot() + +
      +[docs] + @QtCore.Slot() def negative_tweak(self): """Tweak negative by the amount listed in ``ui.tweak_value``""" try: @@ -324,14 +446,20 @@

      Source code for typhos.positioner

               except Exception:
                   logger.exception('Tweak failed')
      -
      [docs] @QtCore.Slot() + +
      +[docs] + @QtCore.Slot() def stop(self): """Stop device""" for device in self.devices: # success=True means expected stop device.stop(success=True)
      -
      [docs] @QtCore.Slot() + +
      +[docs] + @QtCore.Slot() def clear_error(self): """ Clear the error messages from the device and screen. @@ -352,6 +480,7 @@

      Source code for typhos.positioner

                   self._last_move = None
               utils.reload_widget_stylesheet(self, cascade=True)
      + def _get_position(self): if not self._readback: raise Exception("No Device configured for widget!") @@ -404,7 +533,11 @@

      Source code for typhos.positioner

               except Exception:
                   ...
               else:
      -            if low_limit < high_limit:
      +            if low_limit is None or high_limit is None:
      +                # Some devices may erroneously report `None` limits.
      +                # TyphosPositioner will hide the limit labels in this scenario.
      +                ...
      +            elif low_limit < high_limit:
                       self.ui.low_limit.setText(str(low_limit))
                       self.ui.high_limit.setText(str(high_limit))
                       return
      @@ -442,21 +575,21 @@ 

      Source code for typhos.positioner

               Leverage information at describe to define whether to use a
               PyDMLineEdit or a PyDMEnumCombobox as setpoint widget.
               """
      +        if self.device is None:
      +            return
      +
               try:
                   setpoint_signal = getattr(self.device, self.setpoint_attribute)
                   selection = setpoint_signal.enum_strs is not None
               except Exception:
                   selection = False
      +            setpoint_signal = None
       
               if selection:
                   self.ui.set_value = widgets.NoScrollComboBox()
                   self.ui.set_value.addItems(setpoint_signal.enum_strs)
                   # Activated signal triggers only when the user selects an option
                   self.ui.set_value.activated.connect(self.set)
      -            self.ui.set_value.setSizePolicy(
      -                QtWidgets.QSizePolicy.Expanding,
      -                QtWidgets.QSizePolicy.Fixed,
      -            )
                   self.ui.set_value.setMinimumContentsLength(20)
                   self.ui.tweak_widget.setVisible(False)
               else:
      @@ -464,7 +597,23 @@ 

      Source code for typhos.positioner

                   self.ui.set_value.setAlignment(QtCore.Qt.AlignCenter)
                   self.ui.set_value.returnPressed.connect(self.set)
       
      -        self.ui.setpoint_layout.addWidget(self.ui.set_value)
      +        self.ui.set_value.setSizePolicy(
      +            QtWidgets.QSizePolicy.Fixed,
      +            QtWidgets.QSizePolicy.Fixed,
      +        )
      +        self.ui.set_value.setMinimumWidth(
      +            self.ui.user_setpoint.minimumWidth()
      +        )
      +        self.ui.set_value.setMaximumWidth(
      +            self.ui.user_setpoint.maximumWidth()
      +        )
      +        self.ui.setpoint_layout.addWidget(
      +            self.ui.set_value,
      +            alignment=QtCore.Qt.AlignHCenter,
      +        )
      +        self.ui.set_value.setObjectName('set_value')
      +        # Because set_value is used instead
      +        self.ui.user_setpoint.setVisible(False)
       
           @property
           def device(self):
      @@ -474,7 +623,9 @@ 

      Source code for typhos.positioner

               except Exception:
                   ...
       
      -
      [docs] def add_device(self, device): +
      +[docs] + def add_device(self, device): """Add a device to the widget""" # Add device to cache self.devices.clear() # only one device allowed @@ -510,6 +661,7 @@

      Source code for typhos.positioner

               self.ui.alarm_circle.clear_all_alarm_configs()
               self.ui.alarm_circle.add_device(device)
      + @QtCore.Property(bool, designable=False) def moving(self): """ @@ -681,11 +833,14 @@

      Source code for typhos.positioner

               if kind_level != self.alarmKindLevel:
                   self.ui.alarm_circle.kindLevel = kind_level
       
      -
      [docs] def move_changed(self): +
      +[docs] + def move_changed(self): """Called when a move is begun""" logger.debug("Begin showing move in TyphosPositionerWidget") self.moving = True
      + def _set_status_text(self, text, *, max_length=60): """Set the status text label to ``text``.""" if len(text) >= max_length: @@ -733,22 +888,299 @@

      Source code for typhos.positioner

                       self._initialized = True
                       self.ui.set_value.setText(text)
       
      -
      [docs] def update_alarm_text(self, alarm_level): +
      +[docs] + def update_alarm_text(self, alarm_level): """ Label the alarm circle with a short text bit. """ alarms = self.ui.alarm_circle.AlarmLevel - if alarm_level == alarms.NO_ALARM: - text = 'no alarm' - elif alarm_level == alarms.MINOR: - text = 'minor' - elif alarm_level == alarms.MAJOR: - text = 'major' - elif alarm_level == alarms.DISCONNECTED: - text = 'no conn' + try: + text = self.alarm_text[alarm_level] + except KeyError: + text = self.alarm_text[alarms.INVALID] + self.ui.alarm_label.setText(text)
      + + + @property + def all_linked_attributes(self) -> list[str]: + """All linked attribute names.""" + return [ + attr + for attr in ( + self.acceleration_attribute, + self.error_message_attribute, + self.high_limit_switch_attribute, + self.high_limit_travel_attribute, + self.low_limit_switch_attribute, + self.low_limit_travel_attribute, + self.moving_attribute, + self.readback_attribute, + self.setpoint_attribute, + self.velocity_attribute, + ) + if attr + ] + + @property + def all_linked_signals(self) -> list[ophyd.Signal]: + """All linked signal names.""" + signals = [ + getattr(self.device, attr, None) + for attr in self.all_linked_attributes + ] + return [sig for sig in signals if sig is not None] + +
      +[docs] + def show_ui_type_hints(self): + """Show type hints of widgets included in the UI file.""" + cls_attrs = set() + obj_attrs = set(dir(self.ui)) + annotated = set(self.ui.__annotations__) + for cls in type(self.ui).mro(): + cls_attrs |= set(dir(cls)) + likely_from_ui = obj_attrs - cls_attrs - annotated + for attr in sorted(likely_from_ui): + try: + obj = getattr(self, attr, None) + except Exception: + ... + else: + if obj is not None: + print(f"{attr}: {obj.__class__.__module__}.{obj.__class__.__name__}")
      +
      + + + +class _TyphosPositionerRowUI(_TyphosPositionerUI): + """Annotations helper for positioner_row.ui; not to be instantiated.""" + + notes_edit: TyphosNotesEdit + status_container_widget: QtWidgets.QFrame + extended_signal_panel: Optional[TyphosSignalPanel] + error_prefix: QtWidgets.QLabel + switcher: TyphosDisplaySwitcher + + +class TyphosPositionerRowWidget(TyphosPositionerWidget): + ui: _TyphosPositionerRowUI + ui_template = os.path.join(utils.ui_dir, "widgets", "positioner_row.ui") + + alarm_text = { + AlarmLevel.NO_ALARM: 'ok', + AlarmLevel.MINOR: 'minor', + AlarmLevel.MAJOR: 'major', + AlarmLevel.DISCONNECTED: 'conn', + AlarmLevel.INVALID: 'inv', + } + + def __init__(self, *args, **kwargs): + self._error_message = "" + self._status_text = "" + self._alarm_level = AlarmLevel.DISCONNECTED + + super().__init__(*args, **kwargs) + + for idx in range(self.layout().count()): + item = self.layout().itemAt(idx) + if item is self.ui.status_text_layout: + self.layout().takeAt(idx) + break + + # TODO move these out + self._omit_names = [ + "motor_egu", + "motor_stop", + "motor_done_move", + "direction_of_travel", + "user_readback", + "user_setpoint", + "home_forward", # maybe keep? + "home_reverse", + ] + + self.ui.extended_signal_panel = None + self.ui.expand_button.clicked.connect(self._expand_layout) + self.ui.status_label.setText("") + + # TODO: ${name} / macros don't expand here + + @QtCore.Property("QStringList") + def omitNames(self) -> list[str]: + """Get or set the list of names to omit in the expanded signal panel.""" + return self._omit_names + + @omitNames.setter + def omitNames(self, omit_names: list[str]) -> None: + if omit_names == self._omit_names: + return + + self._omit_names = list(omit_names or []) + if self.ui.extended_signal_panel is not None: + self.ui.extended_signal_panel.omitNames = self._omit_names + + def get_names_to_omit(self) -> list[str]: + """ + Get a list of signal names to omit in the extended panel. + + Returns + ------- + list[str] + """ + device: Optional[ophyd.Device] = self.device + if device is None: + return [] + + omit_signals = self.all_linked_signals + to_keep_signals = [ + getattr(device, attr, None) + for attr in (self.velocity_attribute, self.acceleration_attribute) + ] + for sig in to_keep_signals: + if sig in omit_signals: + omit_signals.remove(sig) + + to_omit = set(sig.name for sig in omit_signals) + + # TODO: move these to a Qt designable property + for name in self.omitNames: + to_omit.add(name) + + if device.name in to_omit: + # Don't let the renamed position signal stop us from showing any + # signals: + to_omit.remove(device.name) + return sorted(to_omit) + + def _create_signal_panel(self) -> Optional[TyphosSignalPanel]: + """Create the 'extended' TyphosSignalPanel for the device.""" + if self.device is None: + return None + + panel = TyphosSignalPanel() + panel.omitNames = self.get_names_to_omit() + panel.sortBy = SignalOrder.byName + panel.add_device(self.device) + + self.ui.layout().addWidget(panel) + return panel + + def _expand_layout(self) -> None: + """Toggle the expansion of the signal panel.""" + if self.ui.extended_signal_panel is None: + self.ui.extended_signal_panel = self._create_signal_panel() + if self.ui.extended_signal_panel is None: + return + + to_show = True + else: + to_show = not self.ui.extended_signal_panel.isVisible() + + self.ui.extended_signal_panel.setVisible(to_show) + + if to_show: + self.ui.expand_button.setText('v') else: - text = 'invalid' - self.ui.alarm_label.setText(text)
      + self.ui.expand_button.setText('>') + + def add_device(self, device: ophyd.Device) -> None: + """Add (or rather set) the ophyd device for this positioner.""" + super().add_device(device) + if device is None: + self.ui.device_name_label.setText("(no device)") + if self.ui.extended_signal_panel is not None: + self.layout().removeWidget(self.ui.extended_signal_panel) + self.ui.extended_signal_panel.destroyLater() + self.ui.extended_signal_panel = None + return + + self.ui.device_name_label.setText(device.name) + self.ui.notes_edit.add_device(device) + self.ui.switcher.help_toggle_button.setToolTip(self._get_tooltip()) + self.ui.switcher.help_toggle_button.setEnabled(False) + + def _get_tooltip(self): + """Update the tooltip based on device information.""" + # Lifted from TyphosHelpFrame + tooltip = [] + # BUG: I'm seeing two devices in `self.devices` for + # $ typhos --fake-device 'ophyd.EpicsMotor[{"prefix":"b"}]' + for device in sorted( + set(self.devices), + key=lambda dev: self.devices.index(dev) + ): + heading = device.name or type(device).__name__ + tooltip.extend([ + heading, + "-" * len(heading), + "" + ]) + + tooltip.append( + inspect.getdoc(device) or + inspect.getdoc(type(device)) or + "No docstring" + ) + tooltip.append("") + return "\n".join(tooltip) + + @utils.linked_attribute('error_message_attribute', 'ui.error_label', True) + def _link_error_message(self, signal, widget): + """Link the IOC error message with the ui element.""" + if signal is None: + widget.hide() + self.ui.error_prefix.hide() + else: + signal.subscribe(self.new_error_message) + + def new_error_message(self, value, *args, **kwargs): + self.update_status_visibility(error_message=value) + + def _set_status_text(self, text, *, max_length=80): + super()._set_status_text(text, max_length=max_length) + self.update_status_visibility(status_text=text) + + def update_alarm_text(self, alarm_level): + super().update_alarm_text(alarm_level=alarm_level) + self.update_status_visibility(alarm_level=alarm_level) + + def update_status_visibility( + self, + error_message: str | None = None, + status_text: str | None = None, + alarm_level: AlarmLevel | None = None, + ) -> None: + """ + Hide/show status and error as appropriate. + + The goal here to make an illusion that there is only one label in + in this space when only one of the labels has text. + + If both are empty, we also want to put "something" there to fill the + void, so we opt for a friendly message or an alarm reminder. + """ + if error_message is not None: + self._error_message = error_message + if status_text is not None: + self._status_text = status_text + if alarm_level is not None: + self._alarm_level = alarm_level + error_message = error_message or self._error_message + status_text = status_text or self._status_text + alarm_level = alarm_level or self._alarm_level + has_status = bool(status_text) + has_error = bool(error_message) + if not has_status and not has_error: + # We want to fill something in, check if we have alarms + if alarm_level == AlarmLevel.NO_ALARM: + self.ui.status_label.setText('Status OK') + else: + self.ui.status_label.setText('Check alarm') + has_status = True + self.ui.status_label.setVisible(has_status) + self.ui.error_label.setVisible(has_error) + self.ui.error_prefix.setVisible(has_error) def clear_error_in_background(device): @@ -762,7 +1194,7 @@

      Source code for typhos.positioner

                   logger.error(msg)
                   logger.debug(msg, exc_info=True)
       
      -    td = threading.Thread(target=inner)
      +    td = threading.Thread(target=inner, daemon=True)
           td.start()
       
      diff --git a/master/_modules/typhos/suite.html b/master/_modules/typhos/suite.html index 7908415a0..a34f1db97 100644 --- a/master/_modules/typhos/suite.html +++ b/master/_modules/typhos/suite.html @@ -3,19 +3,19 @@ - typhos.suite — Typhos 2.4.1 documentation + typhos.suite — Typhos 3.0.0 documentation - - - - - - + + + + + + @@ -33,7 +33,7 @@ Typhos
      - 2.4.1 + 3.0.0

    Developer Documentation

      @@ -95,8 +96,10 @@

      Source code for typhos.suite

       
       import logging
       import os
      +import pathlib
       import textwrap
       from functools import partial
      +from typing import Optional, Union
       
       import ophyd
       import pcdsutils.qt
      @@ -107,8 +110,9 @@ 

      Source code for typhos.suite

       
       from . import utils, widgets
       from .display import DisplayTypes, ScrollOptions, TyphosDeviceDisplay
      -from .tools import TyphosConsole, TyphosLogDisplay, TyphosTimePlot
      -from .utils import TyphosBase, clean_attr, clean_name, flatten_tree, save_suite
      +from .tools import TyphosLogDisplay, TyphosTimePlot
      +from .utils import (TyphosBase, TyphosException, clean_attr, clean_name,
      +                    flatten_tree, save_suite)
       
       logger = logging.getLogger(__name__)
       # Use non-None sentinel value since None means no tools
      @@ -166,6 +170,11 @@ 

      Source code for typhos.suite

               )
       
       
      +class TyphosDisplayNotCreatedError(TyphosException):
      +    """The given subdisplay has not yet been shown."""
      +    ...
      +
      +
       class LazySubdisplay(QtWidgets.QWidget):
           """
           A lazy subdisplay which only is instantiated when shown in the suite.
      @@ -288,7 +297,9 @@ 

      Source code for typhos.suite

               )
       
       
      -
      [docs]class TyphosSuite(TyphosBase): +
      +[docs] +class TyphosSuite(TyphosBase): """ This suite combines tools and devices into a single widget. @@ -328,9 +339,10 @@

      Source code for typhos.suite

           DEFAULT_TITLE = 'Typhos Suite'
           DEFAULT_TITLE_DEVICE = 'Typhos Suite - {device.name}'
       
      -    default_tools = {'Log': TyphosLogDisplay,
      -                     'StripTool': TyphosTimePlot,
      -                     'Console': TyphosConsole}
      +    default_tools = {
      +        "Log": TyphosLogDisplay,
      +        "StripTool": TyphosTimePlot,
      +    }
       
           def __init__(
               self,
      @@ -382,7 +394,9 @@ 

      Source code for typhos.suite

               self.default_display_type = default_display_type
               self.scroll_option = scroll_option
       
      -
      [docs] def add_subdisplay(self, name, display, category): +
      +[docs] + def add_subdisplay(self, name, display, category): """ Add an arbitrary widget to the tree of available widgets and tools. @@ -404,7 +418,10 @@

      Source code for typhos.suite

               parameter = SidebarParameter(value=display, name=name)
               self._add_to_sidebar(parameter, category)
      -
      [docs] def add_lazy_subdisplay( + +
      +[docs] + def add_lazy_subdisplay( self, name: str, display_class: type[QtWidgets.QWidget], category: str ): """ @@ -431,6 +448,7 @@

      Source code for typhos.suite

               )
               self._add_to_sidebar(parameter, category)
      + @property def top_level_groups(self): """ @@ -447,7 +465,9 @@

      Source code for typhos.suite

                       root.child(idx).param
                       for idx in range(root.childCount())}
       
      -
      [docs] def add_tool(self, name: str, tool: type[QtWidgets.QWidget]): +
      +[docs] + def add_tool(self, name: str, tool: type[QtWidgets.QWidget]): """ Add a widget to the toolbar. @@ -467,18 +487,24 @@

      Source code for typhos.suite

               """
               self.add_lazy_subdisplay(name, tool, "Tools")
      -
      [docs] def get_subdisplay(self, display): + +
      +[docs] + def get_subdisplay(self, display: Union[Device, str], instantiate: bool = True): """ Get a subdisplay by name or contained device. Parameters ---------- - display :str or Device + display : str or Device Name of screen or device + instantiate : bool, optional + Instantiate lazy sub-displays if they do not already exist. + Raise otherwise. Returns ------- - widget : QWidget + widget : QWidget or partial Widget that is a member of the :attr:`.ui.subdisplay` Example @@ -507,26 +533,43 @@

      Source code for typhos.suite

       
               subdisplay = display.value()
               if isinstance(subdisplay, partial):
      +            if not instantiate:
      +                raise TyphosDisplayNotCreatedError(
      +                    f"Subdisplay {display} has not been created yet"
      +                )
      +
                   subdisplay = subdisplay()
                   display.setValue(subdisplay)
               return subdisplay
      -
      [docs] @QtCore.Slot(str) + +
      +[docs] + @QtCore.Slot(str) @QtCore.Slot(object) - def show_subdisplay(self, widget): + def show_subdisplay( + self, + widget: Union[QtWidgets.QWidget, SidebarParameter, str], + ) -> QtWidgets.QWidget: """ Open a display in the dock system. Parameters ---------- - widget: QWidget, SidebarParameter or str + 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` + + Returns + ------- + widget : QWidget + The subdisplay that was shown. """ # Grab true widget if not isinstance(widget, QtWidgets.QWidget): widget = self.get_subdisplay(widget) + # Setup the dock dock = widgets.SubDisplay(self) # Set sidebar properly @@ -549,9 +592,26 @@

      Source code for typhos.suite

                   )
                   self._content_frame.layout().setAlignment(
                       dock, QtCore.Qt.AlignTop
      -            )
      + ) + + self._new_template() + if isinstance(widget, TyphosDeviceDisplay): + widget.template_changed.connect(self._new_template) + return widget
      + -
      [docs] @QtCore.Slot(str) + def _new_template(self, template: Optional[pathlib.Path] = None) -> None: + """Hook for when a new template is selected in a sub-display.""" + if self.parent() is not None: + return + + new_width = self.minimumSizeHint().width() + if self.width() < new_width: + self.resize(new_width, self.height()) + +
      +[docs] + @QtCore.Slot(str) @QtCore.Slot(object) def embed_subdisplay(self, widget): """Embed a display in the dock system.""" @@ -574,7 +634,10 @@

      Source code for typhos.suite

               self.embedded_dock.widget().layout().insertWidget(widget_count - 1,
                                                                 widget)
      -
      [docs] @QtCore.Slot() + +
      +[docs] + @QtCore.Slot() @QtCore.Slot(object) def hide_subdisplay(self, widget): """ @@ -588,7 +651,12 @@

      Source code for typhos.suite

                   DockWidget we will close that, otherwise the widget is just hidden.
               """
               if not isinstance(widget, QtWidgets.QWidget):
      -            widget = self.get_subdisplay(widget)
      +            try:
      +                widget = self.get_subdisplay(widget, instantiate=False)
      +            except TyphosDisplayNotCreatedError:
      +                logger.debug("Subdisplay was never shown; nothing to do: %s", widget)
      +                return
      +
               sidebar = self._get_sidebar(widget)
               if sidebar:
                   for item in sidebar.items:
      @@ -614,7 +682,10 @@ 

      Source code for typhos.suite

               else:
                   widget.hide()
      -
      [docs] @QtCore.Slot() + +
      +[docs] + @QtCore.Slot() def hide_subdisplays(self): """Hide all open displays.""" # Grab children from devices @@ -622,6 +693,7 @@

      Source code for typhos.suite

                   for param in flatten_tree(group)[1:]:
                       self.hide_subdisplay(param)
      + @property def tools(self): """Tools loaded into the suite.""" @@ -644,7 +716,9 @@

      Source code for typhos.suite

       
               self.setWindowTitle(title_fmt.format(self=self, device=device))
       
      -
      [docs] def add_device(self, device, children=True, category='Devices'): +
      +[docs] + def add_device(self, device, children=True, category='Devices'): """ Add a device to the suite. @@ -677,7 +751,10 @@

      Source code for typhos.suite

                       logger.exception("Unable to add %s to tool %s",
                                        device.name, type(tool))
      -
      [docs] @classmethod + +
      +[docs] + @classmethod def from_device( cls, device: Device, @@ -741,7 +818,10 @@

      Source code for typhos.suite

                                       show_displays=show_displays,
                                       **kwargs)
      -
      [docs] @classmethod + +
      +[docs] + @classmethod def from_devices( cls, devices: list[Device], @@ -826,7 +906,10 @@

      Source code for typhos.suite

                                        device.name)
               return suite
      -
      [docs] def save(self): + +
      +[docs] + def save(self): """ Save suite settings to a file using :meth:`typhos.utils.save_suite`. @@ -853,9 +936,73 @@

      Source code for typhos.suite

               else:
                   logger.debug("No filename chosen")
      + # Add the template to the docstring save.__doc__ += textwrap.indent('\n' + utils.saved_template, '\t\t') +
      +[docs] + def save_screenshot( + self, + filename: str, + ) -> bool: + """Save a screenshot of this widget to ``filename``.""" + + image = utils.take_widget_screenshot(self) + if image is None: + logger.warning("Failed to take screenshot") + return False + + logger.info( + "Saving screenshot of suite titled '%s' to '%s'", + self.windowTitle(), filename, + ) + image.save(filename) + return True
      + + +
      +[docs] + def save_device_screenshots( + self, + filename_format: str, + ) -> dict[str, str]: + """Save screenshot(s) of devices to ``filename_format``.""" + + filenames = {} + for device in self.devices: + display = self.get_subdisplay(device) + + if hasattr(display, "to_image"): + image = display.to_image() + else: + # This is a fallback for if/when we don't have a TyphosDisplay + image = utils.take_widget_screenshot(display) + + suite_title = self.windowTitle() + widget_title = display.windowTitle() + if image is None: + logger.warning( + "Failed to take screenshot of device: %s in %s", + device.name, suite_title, + ) + continue + + filename = filename_format.format( + suite_title=suite_title, + widget_title=widget_title, + device=device, + name=device.name, + ) + logger.info( + "Saving screenshot of '%s': '%s' to '%s'", + suite_title, widget_title, filename, + ) + image.save(filename) + filenames[device.name] = filename + return filenames
      + + def _get_sidebar(self, widget): items = {} for group in self.top_level_groups.values(): @@ -908,6 +1055,7 @@

      Source code for typhos.suite

                       parameter, parameter.sigEmbed, self.embed_subdisplay, parameter
                   )
               return parameter
      +
      diff --git a/master/_modules/typhos/textedit.html b/master/_modules/typhos/textedit.html index 4f264a7e4..0f1272894 100644 --- a/master/_modules/typhos/textedit.html +++ b/master/_modules/typhos/textedit.html @@ -3,19 +3,19 @@ - typhos.textedit — Typhos 2.4.1 documentation + typhos.textedit — Typhos 3.0.0 documentation - - - - - - + + + + + + @@ -33,7 +33,7 @@ Typhos
      - 2.4.1 + 3.0.0

    Developer Documentation

      @@ -105,7 +106,9 @@

      Source code for typhos.textedit

       logger = logging.getLogger(__name__)
       
       
      -
      [docs]@variety.uses_key_handlers +
      +[docs] +@variety.uses_key_handlers @variety.use_for_variety_write('text-multiline') class TyphosTextEdit(QtWidgets.QWidget, PyDMWritableWidget): """ @@ -163,12 +166,15 @@

      Source code for typhos.textedit

           def _send_clicked(self):
               self.send_value()
       
      -
      [docs] def value_changed(self, value): +
      +[docs] + def value_changed(self, value): """Receive and update the TyphosTextEdit for a new channel value.""" self._raw_value = value super().value_changed(self._from_wire(value)) self.set_display()
      + def _to_wire(self, text=None): """TextEdit text -> numpy array.""" if text is None: @@ -188,7 +194,9 @@

      Source code for typhos.textedit

           def _set_text(self, text):
               return self._text_edit.setText(text)
       
      -
      [docs] def send_value(self): +
      +[docs] + def send_value(self): """Emit a :attr:`send_value_signal` to update channel value.""" send_value = self._to_wire() @@ -203,7 +211,10 @@

      Source code for typhos.textedit

       
               self._text_edit.document().setModified(False)
      -
      [docs] def write_access_changed(self, new_write_access): + +
      +[docs] + def write_access_changed(self, new_write_access): """ Change the TyphosTextEdit to read only if write access is denied """ @@ -212,7 +223,10 @@

      Source code for typhos.textedit

               self._send_button.setVisible(new_write_access)
               self._revert_button.setVisible(new_write_access)
      -
      [docs] def set_display(self): + +
      +[docs] + def set_display(self): """Set the text display of the TyphosTextEdit.""" if self.value is None or self._text_edit.document().isModified(): return @@ -220,6 +234,7 @@

      Source code for typhos.textedit

               self._display_text = str(self.value)
               self._set_text(self._display_text)
      + variety_metadata = variety.create_variety_property() def _reinterpret_text(self): @@ -242,6 +257,7 @@

      Source code for typhos.textedit

               if format_ != 'plain':
                   logger.warning('Non-plain formats not yet implemented.')
               self._reinterpret_text()
      +
      diff --git a/master/_modules/typhos/tools/console.html b/master/_modules/typhos/tools/console.html deleted file mode 100644 index 6bfd268ef..000000000 --- a/master/_modules/typhos/tools/console.html +++ /dev/null @@ -1,321 +0,0 @@ - - - - - - typhos.tools.console — Typhos 2.4.1 documentation - - - - - - - - - - - - - - - - -
      - - -
      - -
      -
      -
      - -
      -
      -
      -
      - -

      Source code for typhos.tools.console

      -import logging
      -import threading
      -
      -from qtconsole.manager import QtKernelManager
      -from qtconsole.rich_jupyter_widget import RichJupyterWidget
      -from qtpy import QtCore, QtWidgets
      -
      -from .. import utils
      -
      -logger = logging.getLogger(__name__)
      -
      -
      -def _make_jupyter_widget_with_kernel(kernel_name):
      -    """
      -    Start a kernel, connect to it, and create a RichJupyterWidget to use it.
      -
      -    Parameters
      -    ----------
      -    kernel_name : str
      -        Kernel name to use.
      -    """
      -    kernel_manager = QtKernelManager(kernel_name=kernel_name)
      -    kernel_manager.start_kernel()
      -
      -    kernel_client = kernel_manager.client()
      -    kernel_client.start_channels()
      -
      -    jupyter_widget = RichJupyterWidget()
      -    jupyter_widget.kernel_manager = kernel_manager
      -    jupyter_widget.kernel_client = kernel_client
      -    return jupyter_widget
      -
      -
      -
      [docs]class TyphosConsole(utils.TyphosBase): - """ - IPython Widget for Typhos Display. - - This widget handles starting a ``JupyterKernel`` and connecting an IPython - console in which the user can type Python commands. It is important to note - that the kernel in which commands are executed is a completely separate - process. This protects the user against locking themselves out of the GUI, - but makes it difficult to pass the Device.. - - To get around this caveat, this widget uses ``happi`` to pass the Device - between the processes. This is not a strict requirement, but if ``happi`` - is not installed, users will need to create a custom ``add_device`` method - if they want their devices loaded in both the GUI and console. - """ - - device_added = QtCore.Signal(object) - kernel_ready = QtCore.Signal() - kernel_shut_down = QtCore.Signal() - -
      [docs] def __init__(self, parent=None): - super().__init__(parent=parent) - self._shutting_down = False - - # Setup widget - self.jupyter_widget = _make_jupyter_widget_with_kernel('python3') - self.jupyter_widget.syntax_style = 'monokai' - self.jupyter_widget.set_default_style(colors='Linux') - self.jupyter_widget.kernel_manager.kernel_restarted.connect( - self._handle_kernel_restart - ) - - # Setup kernel readiness checks - self._ready_lock = threading.Lock() - self._kernel_is_ready = False - self._pending_devices = [] - self._pending_commands = [] - - self._device_history = set() - - self._check_readiness_timer = QtCore.QTimer() - self._check_readiness_timer.setInterval(100) - self._check_readiness_timer.timeout.connect(self._wait_for_readiness) - self._check_readiness_timer.start() - self.kernel_ready.connect(self._add_pending_devices) - - # Set the layout - self.setLayout(QtWidgets.QHBoxLayout()) - self.layout().setContentsMargins(0, 0, 0, 0) - self.layout().addWidget(self.jupyter_widget) - - # Ensure we shutdown the kernel - app = QtWidgets.QApplication.instance() - app.aboutToQuit.connect(lambda: self.shutdown(block=True)) - - self.device_added.connect(self._add_device_history)
      - - @property - def kernel_is_ready(self): - """Is the Jupyter kernel ready?""" - return self.kernel_is_alive and self._kernel_is_read - - @property - def kernel_is_alive(self): - """Is the Jupyter kernel alive and not shutting down?""" - return (self.jupyter_widget.kernel_manager.is_alive() and - not self._shutting_down) - - def _add_pending_devices(self): - """Add devices that were requested prior to the kernel being ready.""" - with self._ready_lock: - self._kernel_is_ready = True - - for command in self._pending_commands: - self.execute(command) - - for device in self._pending_devices: - self._add_device(device) - - self._pending_commands = [] - self._pending_devices = [] - - def _wait_for_readiness(self): - """Wait for the kernel to show the prompt.""" - - def looks_ready(text): - return any(line.startswith('In ') for line in text.splitlines()) - - if looks_ready(self._plain_text): - self.kernel_ready.emit() - self._check_readiness_timer.stop() - - def sizeHint(self): - default = super().sizeHint() - default.setWidth(600) - return default - - def shutdown(self, *, block=False): - """Shutdown the Jupyter Kernel.""" - client = self.jupyter_widget.kernel_client - if self._shutting_down: - logger.debug("Kernel is already shutting down") - return - - self._shutting_down = True - logger.debug("Stopping Jupyter Client") - - def cleanup(): - self.jupyter_widget.kernel_manager.shutdown_kernel() - self.kernel_shut_down.emit() - - client.stop_channels() - if block: - cleanup() - else: - QtCore.QTimer.singleShot(0, cleanup) - - def add_device(self, device): - # Add the device after a short delay to allow the console widget time - # to get initialized - with self._ready_lock: - if not self._kernel_is_ready: - self._pending_devices.append(device) - return - - self._add_device(device) - - @property - def _plain_text(self): - """ - Text in the console. - """ - return self.jupyter_widget._control.toPlainText() - - def execute(self, script, *, echo=True, check_readiness=True): - """ - Execute some code in the console. - """ - if echo: - # Can't seem to get `interactive` or `hidden=False` working: - script = '\n'.join((f"print({repr(script)})", script)) - - if check_readiness: - with self._ready_lock: - if not self._kernel_is_ready: - self._pending_commands.append(script) - return - - self.jupyter_widget.kernel_client.execute(script) - - def _add_device(self, device): - try: - script = utils.code_from_device(device) - self.execute(script) - except Exception: - # Prevent traceback from being displayed - logger.error("Unable to add device %r to TyphosConsole.", - device.name) - else: - self.device_added.emit(device) - - def _handle_kernel_restart(self): - logger.debug('Kernel was restarted.') - for dev in self._device_history: - self.add_device(dev) - - def _add_device_history(self, device): - self._device_history.add(device)
      -
      - -
      -
      -
      - -
      - -
      -

      © Copyright 2023, SLAC National Accelerator Laboratory.

      -
      - - Built with Sphinx using a - theme - provided by Read the Docs. - - -
      -
      -
      -
      -
      - - - - \ No newline at end of file diff --git a/master/_modules/typhos/tools/log.html b/master/_modules/typhos/tools/log.html index 8aef1cf69..000b6b5fc 100644 --- a/master/_modules/typhos/tools/log.html +++ b/master/_modules/typhos/tools/log.html @@ -3,19 +3,19 @@ - typhos.tools.log — Typhos 2.4.1 documentation + typhos.tools.log — Typhos 3.0.0 documentation - - - - - - + + + + + + @@ -33,7 +33,7 @@ Typhos
      - 2.4.1 + 3.0.0

    Developer Documentation

    diff --git a/master/_modules/typhos/tools/plot.html b/master/_modules/typhos/tools/plot.html index f921a1e1a..35f0dbdf4 100644 --- a/master/_modules/typhos/tools/plot.html +++ b/master/_modules/typhos/tools/plot.html @@ -3,19 +3,19 @@ - typhos.tools.plot — Typhos 2.4.1 documentation + typhos.tools.plot — Typhos 3.0.0 documentation - - - - - - + + + + + + @@ -33,7 +33,7 @@ Typhos
    - 2.4.1 + 3.0.0
    @@ -52,6 +52,7 @@
  • Including Python Code
  • Custom Templates
  • Release History
  • +
  • Upcoming Changes
  • Developer Documentation

    Developer Documentation

    Developer Documentation

    Developer Documentation

    Developer Documentation

    -

    Using Happi

    +

    Using Happi

    While happi is not a requirement for using typhos, it is recommended. For more information, visit the GitHub repository. The main purpose of the package is to store information on our @@ -190,7 +191,7 @@

    Using Happi -

    Signals of Devices

    +

    Signals of Devices

    When making a custom screen, you can access signals associated with your device in several ways, in order of suggested use:

      @@ -217,7 +218,7 @@

      Signals of Devices -

      Display Signals

      +

      Display Signals

      The first thing we’ll talk about is showing a group of signals associated with our motor object in a basic form called a TyphosSignalPanel. Simply inspecting the device reveals a few @@ -237,16 +238,16 @@

      Display Signals# Important signals, all hints will be found here as well In [5]: motor.read() Out[5]: -OrderedDict([('motor', {'value': 0, 'timestamp': 1680647484.2487311}), +OrderedDict([('motor', {'value': 0, 'timestamp': 1695856437.8709993}), ('motor_setpoint', - {'value': 0, 'timestamp': 1680647484.2487297})]) + {'value': 0, 'timestamp': 1695856437.8709984})]) # Configuration information In [6]: motor.read_configuration() Out[6]: -OrderedDict([('motor_velocity', {'value': 1, 'timestamp': 1680647484.2491562}), +OrderedDict([('motor_velocity', {'value': 1, 'timestamp': 1695856437.871583}), ('motor_acceleration', - {'value': 1, 'timestamp': 1680647484.2491765})]) + {'value': 1, 'timestamp': 1695856437.871604})])

      The TyphosSignalPanel will render these, allowing us to select a @@ -268,7 +269,7 @@

      Display SignalsSignal represents.

    -

    Filling Templates

    +

    Filling Templates

    Taking this concept further, instead of filling a single panel TyphosDeviceDisplay allows a template to be created with a multitude of widgets and panels. Typhos will find widgets that accept devices, but do @@ -287,7 +288,7 @@

    Filling Templates -

    The TyphosSuite

    +

    The TyphosSuite

    A complete application can be made by loading the TyphosSuite. Below is the complete code from start to finish required to create the suite. Look at the TyphosSuited.default_tools to control which typhos.tools are @@ -309,7 +310,7 @@

    The TyphosSuite -

    Using the StyleSheet

    +

    Using the StyleSheet

    Typhos ships with two stylesheets to improve the look and feel of the widgets. When invoking typhos from the CLI as normal, you can pass the --dark flag to use the dark stylesheet instead of the light mode, @@ -338,7 +339,7 @@

    Using the StyleSheet

    -

    Using the Documentation Widget

    +

    Using the Documentation Widget

    Typhos has a built-in documentation helper, which allows for the in-line linking and display of a user-provided website.

    To inform Typhos how to load documentation specific to your facility, please @@ -364,7 +365,7 @@

    Using the Documentation Widget -

    Using the Jira Bug Reporting Widget

    +

    Using the Jira Bug Reporting Widget

    Typhos has an optional built-in widget to generate Jira user stories/bug reports.

    A prerequisite to this support is, of course, a working Jira installation @@ -383,7 +384,7 @@

    Using the Jira Bug Reporting Widget -

    Launching the Examples

    +

    Launching the Examples

    There are example screens in the typhos.examples submodule. After installing typhos, you can launch them as follows:

    Developer Documentation

    Developer Documentation

    Developer Documentation