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

REF: button bar rework - template switch menu + Jira reporting button #572

Merged
merged 5 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
223 changes: 161 additions & 62 deletions typhos/display.py
Copy link
Contributor Author

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?
image

Copy link
Contributor

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.

Copy link
Contributor Author

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...

Copy link
Member

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

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?

Copy link
Member

Choose a reason for hiding this comment

The 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?
Or, prevent us from swapping to the "dead end" templates I guess (somehow)

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
Expand Down Expand Up @@ -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]
Copy link
Member

Choose a reason for hiding this comment

The 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 IntEnum (as I did, on initial review).

I don't think many other classes would be able to look themselves up in a dictionary of their own attributes.

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'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

Copy link
Member

Choose a reason for hiding this comment

The 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:

  • The diff cut off at a point where I didn't see "IntEnum"
  • I briefly forgot that IntEnum can be used as an int



_DisplayTypes = utils.pyqt_class_from_enum(DisplayTypes)
DisplayTypes.names = [view.name for view in DisplayTypes]
Expand Down Expand Up @@ -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',
Expand All @@ -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
Expand All @@ -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):
Expand All @@ -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)
Expand All @@ -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):
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Copy link
Member

Choose a reason for hiding this comment

The 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

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 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):
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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):
"""
Expand Down
12 changes: 12 additions & 0 deletions typhos/ui/style.qss
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ TyphosLoading {
color: red;
}

TyphosDisplayConfigButton > QMenu::item:default {
color: darkgreen;
}

TyphosDisplayConfigButton > QMenu::separator {
}

QLabel#menu_template_section {
font: bold;
padding: 4 4 4 4px;
}

TyphosNotesEdit {
background: transparent;
color: black;
Expand Down
Loading