Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: layout changes for compact complex screens + row positioner widget #563

Merged
merged 41 commits into from
Sep 1, 2023
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
9533338
WIP/POC: positioner which uses horizontal space
klauer Aug 4, 2023
7549719
CLN: remove prototype collapsible widget
klauer Aug 5, 2023
47ea46a
MNT: rename 'one line positioner' -> 'positioner row'
klauer Aug 8, 2023
76c8c74
REF: always suggest tree for top-level screens
klauer Aug 8, 2023
3ca4ad5
MNT: move device name label to top
klauer Aug 8, 2023
ac43191
MNT: add back title in positioner
klauer Aug 8, 2023
4f102df
TMP: patch over issue 565 temporarily
klauer Aug 8, 2023
a00bba4
FIX: patch over issue 565
klauer Aug 8, 2023
a75ac10
WIP: tweaks for positioner row after discussion
klauer Aug 15, 2023
06c3966
ENH: 'expand panel' option for positioner row
klauer Aug 15, 2023
5a42a84
MNT: add type hints + fix expand button
klauer Aug 15, 2023
ccb8f40
FIX: clear error daemon thread
klauer Aug 15, 2023
c0709d4
MNT: more alignment fixes
klauer Aug 15, 2023
b82f263
MNT: clear out status label text
klauer Aug 15, 2023
b0ac7cf
MNT: placeholder for size policy fixing
klauer Aug 15, 2023
df39abf
Merge branch 'master' into enh_oneline_positioner
klauer Aug 15, 2023
751c08a
MNT: basic layout nitpicking
ZLLentz Aug 15, 2023
2c7dc7f
WIP: try bigger readback and moving up the name
ZLLentz Aug 15, 2023
9b9aaf4
ENH: layout adjustments for resizing and error handling
ZLLentz Aug 16, 2023
e919aae
ENH: increase bounding boxes and fill out the placeholder templates
ZLLentz Aug 17, 2023
284375b
Merge branch 'enh_oneline_positioner' into mnt_layout_nitpick
ZLLentz Aug 17, 2023
d5dcd3d
REF: make TyphosNotesEdit designable
klauer Aug 28, 2023
27d5c0c
MNT: fix up typhos imports to be relative
klauer Aug 28, 2023
9138889
ENH: add notes to row positioner widget
klauer Aug 28, 2023
6e1afa5
FIX: call notes_edit add_device
klauer Aug 28, 2023
5d6f402
Merge remote-tracking branch 'origin/master' into wip_oneline_notes
klauer Aug 28, 2023
923e021
FIX: designability + stylesheets for notes line edit
klauer Aug 28, 2023
b4524d9
REF/API: clean up loading priority + document
klauer Aug 28, 2023
1ec1846
DOC: add upcoming changes to docs
klauer Aug 28, 2023
d207920
MNT: do not require {} for keyword arg-less devices
klauer Aug 28, 2023
e62c574
TST: do not assume the loaded template name
klauer Aug 28, 2023
402b747
TST: ensure old detailed screen is loaded
klauer Aug 28, 2023
f0cd1ab
TST: fix test suite after switch to relative imports
klauer Aug 28, 2023
3190483
MNT: hide user_setpoint in row widget
klauer Aug 29, 2023
c7322a2
FIX: size hint depending on mode
klauer Aug 29, 2023
45c6e5c
FIX: more reasonable minimum width for row widget
ZLLentz Aug 29, 2023
4b616c5
MNT: remove development device templates
klauer Aug 31, 2023
aaf937d
MNT: remove unintentional change
klauer Aug 31, 2023
8da1096
Merge branch 'master' into enh_oneline_positioner
klauer Aug 31, 2023
f7ba0db
FIX: widgets may be gc'd before screenshot; ignore them
klauer Aug 30, 2023
60763fc
FIX: cherry-pick fixup
klauer Sep 1, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Related Projects
python_methods.rst
templates.rst
release_notes.rst
upcoming_changes.rst

.. toctree::
:maxdepth: 3
Expand Down
17 changes: 17 additions & 0 deletions docs/source/templates.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,20 @@ custom templates:
point is expecting to find a ``str``, ``pathlib.Path``, or ``list`` of
such objects at your entry point. One such example of how to do this can
be found `here <https://github.com/pcdshub/pcdsdevices/blob/cab3fe158ebc0d032fe07f03ec52ca79cda90c6e/setup.py#L21>`_


For top-level devices (e.g., ``at2l0``), the template load priority is as
follows:

- Happi-defined values (``"detailed_screen"``, ``embedded_screen"``,
``"engineering_screen"``)
- Device-specific screens, if available (named as ``ClassNameHere.detailed.ui``)
- The detailed tree, if the device has sub-devices
- The default templates

For nested displays in a device tree, sub-device (e.g., ``at2l0.blade_01``)
template load priority is as follows:

- Device-specific screens, if available (named as ``ClassNameHere.embedded.ui``)
- The detailed tree, if the device has sub-devices
- The default templates
50 changes: 50 additions & 0 deletions docs/source/upcoming_release_notes/563-row_positioner.rst
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the biggest pre-rel I've ever seen

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
563 row-positioner
##################

API Changes
-----------
- ``TyphosNoteEdit`` now supports ``.add_device()`` like other typhos widgets.
This is alongside its original ``setup_data`` API.
- ``TyphosDeviceDisplay`` composite heuristics have been removed in favor of
simpler methods, described in the features section.

Features
--------
- ``TyphosNoteEdit`` is now a ``TyphosBase`` object and is accessible in the Qt
designer.
- Added new designable widget ``TyphosPositionerRowWidget``. This compact
positioner widget makes dense motor-heavy screens much more space efficient.
- The layout method for ``TyphosDeviceDisplay`` has changed. For large device trees,
it now favors showing the compact "embedded" screens over detailed screens. The order
of priority is now as follows:
- For top-level devices (e.g., ``at2l0``), the template load priority is as follows:

* Happi-defined values (``"detailed_screen"``, ``embedded_screen"``, ``"engineering_screen"``)
* Device-specific screens, if available (named as ``ClassNameHere.detailed.ui``)
* The detailed tree, if the device has sub-devices
* The default templates

- For nested displays in a device tree, sub-device (e.g., ``at2l0.blade_01``)
template load priority is as follows:

* Device-specific screens, if available (named as ``ClassNameHere.embedded.ui``)
* The detailed tree, if the device has sub-devices
* The default templates (``embedded_screen.ui``)

Bugfixes
--------
- For devices which do not require keyword arguments to instantiate, the typhos
CLI will no longer require an empty dictionary. That is, ``$ typhos
ophyd.sim.SynAxis[]`` is equivalent to ``$ typhos ophyd.sim.SynAxis[{}]``.
As before, ophyd's required "name" keyword argument is filled in by typhos by
default.


Maintenance
-----------
- N/A

Contributors
------------
- klauer
- ZLLentz
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ TyphosDisplaySwitcherPlugin = "typhos.display:TyphosDisplaySwitcher"
TyphosDisplayTitlePlugin = "typhos.display:TyphosDisplayTitle"
TyphosHelpFramePlugin = "typhos.display:TyphosHelpFrame"
TyphosMethodButtonPlugin = "typhos.func:TyphosMethodButton"
TyphosNotesEditPlugin = "typhos.notes:TyphosNotesEdit"
TyphosPositionerWidgetPlugin = "typhos.positioner:TyphosPositionerWidget"
TyphosPositionerRowWidgetPlugin = "typhos.positioner:TyphosPositionerRowWidget"
TyphosRelatedSuiteButtonPlugin = "typhos.related_display:TyphosRelatedSuiteButton"
TyphosSignalPanelPlugin = "typhos.panel:TyphosSignalPanel"

Expand Down
1 change: 0 additions & 1 deletion typhos/alarm.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,6 @@ def update_current_alarm(self):
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):
Expand Down
31 changes: 16 additions & 15 deletions typhos/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@
from pydm.widgets.template_repeater import FlowLayout
from qtpy import QtCore, QtWidgets

import typhos
from typhos.app import get_qapp, launch_suite
from typhos.benchmark.cases import run_benchmarks
from typhos.benchmark.profile import profiler_context
from typhos.display import DisplayTypes, ScrollOptions
from typhos.suite import TyphosSuite
from typhos.utils import (apply_standard_stylesheets, compose_stylesheets,
nullcontext)
from . import __version__ as typhos_version
from . import utils
from .app import get_qapp, launch_suite
from .benchmark.cases import run_benchmarks
from .benchmark.profile import profiler_context
from .display import DisplayTypes, ScrollOptions
from .suite import TyphosSuite
from .utils import apply_standard_stylesheets, compose_stylesheets, nullcontext

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -265,11 +265,11 @@ def _create_happi_client(cfg):
"""Create a happi client based on configuration ``cfg``."""
import happi

import typhos.plugins.happi
from .plugins import happi as happi_plugin

if typhos.plugins.happi.HappiClientState.client:
if happi_plugin.HappiClientState.client:
logger.debug("Using happi Client already registered with Typhos")
return typhos.plugins.happi.HappiClientState.client
return happi_plugin.HappiClientState.client

logger.debug("Creating new happi Client from configuration")
return happi.Client.from_config(cfg=cfg)
Expand Down Expand Up @@ -330,7 +330,7 @@ def create_suite(
layout_obj = get_layout_from_cli(layout, cols)
display_type_enum = get_display_type_from_cli(display_type)
scroll_enum = get_scrollable_from_cli(scroll_option)
return typhos.TyphosSuite.from_devices(
return TyphosSuite.from_devices(
devices,
content_layout=layout_obj,
default_display_type=display_type_enum,
Expand Down Expand Up @@ -453,7 +453,7 @@ def create_devices(device_names, cfg=None, fake_devices=False):
devices = list()

klass_regex = re.compile(
r'([a-zA-Z][a-zA-Z0-9\.\_]*)\[(\{.+})*[\,]*\]' # noqa
r'([a-zA-Z][a-zA-Z0-9\.\_]*)\[(\{.*})*[\,]*\]' # noqa
)

for device_name in device_names:
Expand Down Expand Up @@ -567,7 +567,7 @@ def typhos_run(
The window created. This is returned after the application
is done running. Primarily used in unit tests.
"""
with typhos.utils.no_device_lazy_load():
with utils.no_device_lazy_load():
suite = create_suite(
device_names,
cfg=cfg,
Expand Down Expand Up @@ -616,7 +616,8 @@ def typhos_cli(args):
args = parser.parse_args(args, TyphosArguments())

if args.version:
print(f'Typhos: Version {typhos.__version__} from {typhos.__file__}')
typhos_file = sys.modules["typhos"].__file__
print(f'Typhos: Version {typhos_version} from {typhos_file}')
return

if any(
Expand Down
113 changes: 45 additions & 68 deletions typhos/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,12 @@ def generate_context_menu(self):
if not display:
return base_menu

for cls in type(display.device).mro():
if cls.__name__ in ("BlueskyInterface", "Device"):
break

base_menu.addSection(cls.__name__)

klauer marked this conversation as resolved.
Show resolved Hide resolved
base_menu.addSection('Templates')
display._generate_template_menu(base_menu)

Expand Down Expand Up @@ -962,11 +968,6 @@ class TyphosDeviceDisplay(utils.TyphosBase, widgets.TyphosDesignerMixin,
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.

Expand All @@ -990,25 +991,20 @@ class TyphosDeviceDisplay(utils.TyphosBase, widgets.TyphosDesignerMixin,
# Template types and defaults
Q_ENUMS(_DisplayTypes)
TemplateEnum = DisplayTypes # For convenience

device_count_threshold = 0
signal_count_threshold = 30
template_changed = QtCore.Signal(object)

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,
display_type: Union[DisplayTypes, str, int] = 'detailed_screen',
scroll_option: Union[ScrollOptions, str, int] = 'auto',
nested: bool = False,
):
self._composite_heuristics = composite_heuristics
self._current_template = None
self._forced_template = ''
self._macros = {}
Expand All @@ -1021,6 +1017,12 @@ def __init__(
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 [],
Expand Down Expand Up @@ -1053,15 +1055,6 @@ def __init__(
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."""
Expand All @@ -1087,27 +1080,31 @@ def hideEmpty(self, checked):
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)

Expand Down Expand Up @@ -1292,12 +1289,12 @@ def size_hint(*args, **kwargs):
self.template_changed.emit(template)

def minimumSizeHint(self) -> QtCore.QSize:
if self._scroll_area is None:
return super().minimumSizeHint()
return QtCore.QSize(
self._scroll_area.viewportSizeHint().width(),
super().minimumSizeHint().height(),
)
if self._layout_in_scroll_area:
return QtCore.QSize(
self._scroll_area.viewportSizeHint().width(),
super().minimumSizeHint().height(),
)
return super().minimumSizeHint()

@property
def display_widget(self):
Expand Down Expand Up @@ -1447,19 +1444,20 @@ def search_for_templates(self):
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:
template_list.append(filename)
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting choice. It's useful but has added to my confusion about template names. Why do we do this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it's like:

  • "AT2L0 is a complex device. If it has a custom screen when loaded, use it"
  • In a scenario where AT2L0 didn't have a custom screen, we'd want it to use the detailed tree as the "next best thing"

It's all confusing, for sure. Does that touch on your confusion on this aspect or is it something else?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's more that detailed_tree.ui shows up in each of the embedded/detailed/engineering dropdowns.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that may have been unintentional

Copy link
Contributor

@tangkong tangkong Aug 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aaah ok the comment made it seem intentional, but that's not really a proper reading of the comment. Gotcha

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]
Expand All @@ -1476,31 +1474,10 @@ def suggest_composite_screen(cls, device_cls):
composite : bool
If True, favor the composite screen.
"""
num_devices = 0
num_signals = 0
for attr, component in utils._get_top_level_components(device_cls):
num_devices += issubclass(component.cls, ophyd.Device)
num_signals += issubclass(component.cls, ophyd.Signal)

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

logger.debug(
'%s screens=%s num_signals=%d num_devices=%d -> composite=%s',
device_cls, specific_screens, num_signals, num_devices, composite
)
return composite
for _, component in utils._get_top_level_components(device_cls):
if issubclass(component.cls, ophyd.Device):
return True
return False

@classmethod
def from_device(cls, device, template=None, macros=None, **kwargs):
Expand Down
Loading
Loading