-
Notifications
You must be signed in to change notification settings - Fork 26
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
REF: button bar rework - template switch menu + Jira reporting button #572
Changes from all commits
cd31851
6e2fe4d
084d70d
d1bb30f
2da4d4b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe unrelated to this PR and more as a consequence of #563: there's some edge cases now where you can accidentally pick a template that doesn't itself contain a button title bar, and there's no way to go back. Not sure how to handle this case. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good one... maybe the suite needs to be smarter and insert a title bar if it doesn't exist in the newly-created display? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, either a smart title bar or some alternate way to switch templates- right clicking? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,14 @@ | ||
"""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 | ||
|
@@ -34,6 +36,15 @@ class DisplayTypes(enum.IntEnum): | |
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] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's wild to me that this works, the code reads pretty confusingly if you forget that this is an I don't think many other classes would be able to look themselves up in a dictionary of their own attributes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've used this pattern before with enums and kinda liked it - but if you think it's confusing it's worth redoing honestly as "clever code" is not good code There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't necessarily think it's worth changes- the only reason I was confused is because:
|
||
|
||
|
||
_DisplayTypes = utils.pyqt_class_from_enum(DisplayTypes) | ||
DisplayTypes.names = [view.name for view in DisplayTypes] | ||
|
@@ -361,35 +372,30 @@ def generate_context_menu(self): | |
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 | ||
|
||
|
||
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', | ||
|
@@ -401,30 +407,32 @@ def __init__(self, display_type, *, parent=None): | |
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 | ||
# Show all our options in the context menu: | ||
super()._clicked() | ||
|
||
self.template_selected.emit(template) | ||
|
||
def generate_context_menu(self): | ||
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 | ||
|
@@ -433,6 +441,12 @@ def selected(*, template=template): | |
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): | ||
|
@@ -454,23 +468,29 @@ def __init__(self, parent=None, **kwargs): | |
|
||
self._create_ui() | ||
|
||
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) | ||
|
@@ -482,13 +502,14 @@ def _template_selected(self, template): | |
if self.device_display is not None: | ||
self.device_display.force_template = template | ||
|
||
def set_device_display(self, display): | ||
def _templates_loaded(self, templates: Dict[str, List[pathlib.Path]]) -> None: | ||
... | ||
|
||
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) | ||
|
||
def add_device(self, device): | ||
|
@@ -517,6 +538,19 @@ def mousePressEvent(self, 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): | ||
""" | ||
A standard button used to toggle help information display. | ||
|
@@ -986,6 +1020,8 @@ class TyphosDeviceDisplay(utils.TyphosBase, widgets.TyphosDesignerMixin, | |
Q_ENUMS(_DisplayTypes) | ||
TemplateEnum = DisplayTypes # For convenience | ||
template_changed = QtCore.Signal(object) | ||
templates_loaded = QtCore.Signal(object) | ||
templates: Dict[str, List[pathlib.Path]] | ||
|
||
def __init__( | ||
self, | ||
|
@@ -1102,29 +1138,90 @@ def _move_display_to_layout(self, widget): | |
|
||
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 | ||
|
||
action = menu.addAction(os.path.split(filename)[-1]) | ||
action.triggered.connect(switch_template) | ||
def by_match_order(template: pathlib.Path) -> int: | ||
return matching.index(template) | ||
|
||
refresh_action = base_menu.addAction("Refresh Templates") | ||
refresh_action.triggered.connect(self._refresh_templates) | ||
if not added_header: | ||
add_header( | ||
f"{template_type.friendly_name} screens", | ||
icon=TyphosToolButton.get_icon( | ||
TyphosDisplaySwitcherButton.icons[template_type.name] | ||
), | ||
) | ||
added_header = True | ||
|
||
base_menu.addSection(f"{cls.__name__}") | ||
for filename in sorted(templates, key=by_match_order): | ||
add_template(filename) | ||
|
||
add_header("Typhos default screens") | ||
for template in DEFAULT_TEMPLATES_FLATTEN: | ||
add_template(template) | ||
|
||
prefix = os.path.commonprefix( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this widely useful utility function hiding in the os.path module? Python weird There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I couldn't agree more... |
||
[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): | ||
|
@@ -1285,7 +1382,7 @@ def size_hint(*args, **kwargs): | |
def minimumSizeHint(self) -> QtCore.QSize: | ||
if self._layout_in_scroll_area: | ||
return QtCore.QSize( | ||
self._scroll_area.viewportSizeHint().width(), | ||
int(self._scroll_area.viewportSizeHint().width() * 1.05), | ||
super().minimumSizeHint().height(), | ||
) | ||
return super().minimumSizeHint() | ||
|
@@ -1458,6 +1555,8 @@ def search_for_templates(self): | |
if templ not in template_list] | ||
) | ||
|
||
self.templates_loaded.emit(copy.deepcopy(self.templates)) | ||
|
||
@classmethod | ||
def suggest_composite_screen(cls, device_cls): | ||
""" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thoughts about this ordering of template types + instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's nice! It helps make the template organization/hierarchy a little more obvious.
Leads me to wonder if people would ever want the base templates. I'm sure there's a world where we do, but for now people probably don't care huh.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, there probably is little reason to display those - though maybe better to display more options than less just in case? The menu should look less cluttered for a non-development checkout version, but not much better admittedly.
Another thought is that this menu could also have nested submenus. I'm fond of seeing all the choices in a big list personally. That said, base/"advanced" ones still could be in an "Other screens" menu, maybe...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quick note: the most up-to-date version of this looks even better with the shared prefixes cleaned up
I'm curious how this will end up looking for dev_conda