-
Notifications
You must be signed in to change notification settings - Fork 7
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: Add device descriptions #124
Changes from all commits
77e69ce
3946f93
e0dc0bb
a61e41d
50fa367
a83e1a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,13 +4,17 @@ | |
import os | ||
import weakref | ||
from functools import partial | ||
from pathlib import Path | ||
from typing import Any, Optional | ||
|
||
import yaml | ||
from pydm.widgets import PyDMRelatedDisplayButton, PyDMShellCommand | ||
from qtpy import QtCore, QtGui, QtWidgets | ||
from qtpy.QtCore import Property, QEvent, QSize, Qt | ||
from qtpy.QtGui import QHoverEvent | ||
from qtpy.QtWidgets import QGridLayout, QMenu, QPushButton, QWidget | ||
from qtpy.QtWidgets import (QGridLayout, QHBoxLayout, QLineEdit, QMenu, | ||
QPushButton, QWidget, QWidgetAction) | ||
from ruamel.yaml import YAML | ||
from typhos.utils import reload_widget_stylesheet | ||
|
||
import lucid | ||
|
@@ -21,6 +25,52 @@ | |
logger = logging.getLogger(__name__) | ||
|
||
|
||
class ButtonDescriptionWidget(QWidget): | ||
"""Widget with a button an description line edit""" | ||
def __init__( | ||
self, | ||
*args, | ||
button_text: str = '', | ||
button_cb: Optional[callable] = None, | ||
desc_text: str = '', | ||
desc_cb: Optional[callable] = None, | ||
**kwargs | ||
) -> None: | ||
super().__init__(*args, **kwargs) | ||
self.button = QPushButton(button_text) | ||
if button_cb: | ||
self.button.clicked.connect(button_cb) | ||
self.desc_edit = QLineEdit(desc_text) | ||
self.desc_edit.setPlaceholderText('no description') | ||
if desc_cb: | ||
self.desc_edit.editingFinished.connect( | ||
lambda: desc_cb(self.desc_edit.text()) | ||
) | ||
self.hlayout = QHBoxLayout() | ||
self.hlayout.addWidget(self.button) | ||
self.hlayout.addWidget(self.desc_edit) | ||
self.setLayout(self.hlayout) | ||
|
||
|
||
def get_device_description(device_name: str, config_file: Optional[Any]) -> str: | ||
""" Get the device description from the yaml file""" | ||
if not config_file: | ||
return "" | ||
|
||
if isinstance(config_file, (str, bytes, os.PathLike)): | ||
fpath = config_file | ||
with open(config_file, 'r') as tf: | ||
config = yaml.full_load(tf) | ||
else: # some TextIOWrapper | ||
fpath = os.path.abspath(config_file.name) | ||
with open(fpath, 'r') as tf: | ||
config = yaml.full_load(tf) | ||
|
||
if not config.get('DEVICE_HINTS'): | ||
return "" | ||
return config['DEVICE_HINTS'].get(device_name, "") | ||
|
||
|
||
class BaseDeviceButton(QPushButton): | ||
"""Base class for QPushButton to show devices""" | ||
_OPEN_ALL = "Open All" | ||
|
@@ -31,6 +81,7 @@ def __init__(self, title, *args, **kwargs): | |
# References for created screens | ||
self._device_displays = {} | ||
self._suite = None | ||
self.config_file = None | ||
# Setup Menu | ||
self.setContextMenuPolicy(Qt.PreventContextMenu) | ||
self.device_menu = QMenu() | ||
|
@@ -44,9 +95,9 @@ def show_device(self, device): | |
return self._device_displays[device.name] | ||
|
||
def show_all(self): | ||
"""Create a widget for contained devices""" | ||
if len(self.devices) == 0: | ||
return None | ||
"""Create a widget for contained devices""" | ||
if not self._suite: | ||
self._suite = suite_for_devices(self.devices, parent=self, pin=True) | ||
else: | ||
|
@@ -63,8 +114,14 @@ def _devices_shown(self, shown): | |
|
||
def _menu_shown(self): | ||
# Current menu options | ||
menu_devices = [action.text() | ||
for action in self.device_menu.actions()] | ||
# gather current actions | ||
menu_devices = [] | ||
for action in self.device_menu.actions(): | ||
if isinstance(action, QWidgetAction): | ||
menu_devices.append(action.defaultWidget().button.text()) | ||
else: | ||
menu_devices.append(action.text()) | ||
|
||
if self._OPEN_ALL not in menu_devices: | ||
show_all_devices = self._show_all_wrapper() | ||
self.device_menu.addAction(self._OPEN_ALL, show_all_devices) | ||
|
@@ -74,7 +131,14 @@ def _menu_shown(self): | |
if device.name not in menu_devices: | ||
# Add to device menu | ||
show_device = self._show_device_wrapper(device) | ||
self.device_menu.addAction(device.name, show_device) | ||
action_widget = ButtonDescriptionWidget( | ||
button_text=device.name, button_cb=show_device, | ||
desc_text=get_device_description(device.name, self.config_file), | ||
desc_cb=partial(self._update_desc, device.name) | ||
) | ||
action = QWidgetAction(self.device_menu) | ||
action.setDefaultWidget(action_widget) | ||
self.device_menu.addAction(action) | ||
|
||
def _show_all_wrapper(self): | ||
return lucid.LucidMainWindow.in_dock( | ||
|
@@ -88,6 +152,31 @@ def _show_device_wrapper(self, device): | |
partial(self.show_device, device), | ||
title=device.name) | ||
|
||
def _update_desc(self, device_name: str, device_desc: str) -> None: | ||
if self.config_file: | ||
logger.debug(f'updating device description: {device_name} = {device_desc}') | ||
# open yaml file | ||
ryaml = YAML() | ||
ryaml.default_flow_style = False | ||
if isinstance(self.config_file, (str, bytes, os.PathLike)): | ||
fpath = self.config_file | ||
with open(self.config_file, 'r') as tf: | ||
config = ryaml.load(tf) | ||
else: | ||
fpath = os.path.abspath(self.config_file.name) | ||
with open(fpath, 'r') as tf: | ||
config = ryaml.load(tf) | ||
|
||
if not config.get('DEVICE_HINTS'): | ||
config['DEVICE_HINTS'] = {} | ||
if device_desc == config['DEVICE_HINTS'].get(device_name): | ||
return | ||
if device_desc == '': | ||
config['DEVICE_HINTS'].pop(device_name, None) | ||
else: | ||
config['DEVICE_HINTS'][device_name] = device_desc | ||
ryaml.dump(config, Path(fpath)) | ||
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 think this is the first time in lucid where the user config file is written back to by the program. 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. Putting it all in the same file is probably the easy way out. I suppose the case where multiple lucid's are open at the same time will hit upon this rather quickly. This reminds me of the issue we had in happi where a crash interrupts writing and wipes the file. I should do the write-new-file -> copy over sequence we implemented there. 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. To me, this feels qualitatively different to 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 commented on the typhos PR before making it back to this one... Separating the files makes sense. You mention happi, @ZLLentz, did you guys discuss about putting these notes directly into the database? It seems sensible to have shared notes in a database to me. Or is it like different hutches should have different notes about a shared beamline component or something? 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 think that happi could become a source for notes, but at a lower priority (shadowed by local or env var notes). This is precisely for the reason you mentioned; different hutches might be looking at the same beamline component and have different notes, and I wouldn't want them to clobber each other. The other wrinkle is that happi makes sense for top-level device information, but storing component level notes in happi might get a bit messy (though not completely unreasonable). 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 feel like we've talked about storing extra data in happi as a go-to per-device data source a bunch of times in many contexts, which makes me think that it's worth doing. It might even be worth storing subdevice notes along with the device notes, I'm not sure? It's not immediately clear to me and warrants some discussion and mock-ups. 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 lot of sources of notes and documentation... Off the top of my head, correct me if I'm wrong:
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. To some extent the Typhos PR could supercede this one, or at least LUCID/Typhos could use the same files. (so maybe that simplifies things slightly) Adding yet another source of documentation that can fall out of date leaves a slightly bitter after taste, but I could argue it fills a slightly different niche |
||
|
||
def eventFilter(self, obj, event): | ||
""" | ||
QWidget.eventFilter to be installed on child indicators | ||
|
@@ -101,10 +190,7 @@ def eventFilter(self, obj, event): | |
self._show_all_wrapper()() | ||
return True | ||
elif event.button() == Qt.LeftButton: | ||
if len(self.devices) == 1: | ||
self._show_device_wrapper(self.devices[0])() | ||
else: | ||
self.device_menu.exec_(self.mapToGlobal(event.pos())) | ||
self.device_menu.exec_(self.mapToGlobal(event.pos())) | ||
return True | ||
return False | ||
|
||
|
@@ -116,7 +202,7 @@ class IndicatorCell(BaseDeviceButton): | |
spacing = 1 | ||
margin = 5 | ||
|
||
def __init__(self, *args, **kwargs): | ||
def __init__(self, *args, toolbar=None, **kwargs): | ||
super().__init__(*args, **kwargs) | ||
# Disable borders on the widget unless a hover occurs | ||
self.setStyleSheet('QPushButton:!hover {border: None}') | ||
|
@@ -126,6 +212,7 @@ def __init__(self, *args, **kwargs): | |
self._selecting_widgets = list() | ||
self.installEventFilter(self) | ||
self.devices = list() | ||
self.config_file = toolbar | ||
|
||
@property | ||
def matchable_names(self): | ||
|
@@ -171,12 +258,13 @@ def _devices_shown(self, shown, selector=None): | |
class IndicatorGroup(BaseDeviceButton): | ||
"""QPushButton to select an entire row or column of devices""" | ||
|
||
def __init__(self, *args, orientation, **kwargs): | ||
def __init__(self, *args, orientation, toolbar=None, **kwargs): | ||
super().__init__(*args, **kwargs) | ||
self.setText(str(self.title)) | ||
self.cells = [] | ||
self.installEventFilter(self) | ||
self.orientation = orientation | ||
self.config_file = toolbar | ||
|
||
def add_cell(self, cell): | ||
self.cells.append(cell) | ||
|
@@ -229,7 +317,7 @@ def groups(self): | |
|
||
def add_devices(self, devices, system=None, stand=None): | ||
# Create cell | ||
cell = IndicatorCell(title=f'{stand} {system}') | ||
cell = IndicatorCell(title=f'{stand} {system}', toolbar=self.toolbar_file or None) | ||
for device in devices: | ||
cell.add_device(device) | ||
# Add to proper location in grid | ||
|
@@ -252,7 +340,8 @@ def add_devices(self, devices, system=None, stand=None): | |
def _add_group(self, group, as_row): | ||
# Add to layout | ||
group = IndicatorGroup(title=group, | ||
orientation='row' if as_row else 'column') | ||
orientation='row' if as_row else 'column', | ||
toolbar=self.toolbar_file or None) | ||
self._groups[group.title] = group | ||
# Find the correct position | ||
if as_row: | ||
|
@@ -359,6 +448,7 @@ def __init__(self, parent=None, toolbar_file=None): | |
self.frame = QtWidgets.QFrame(parent) | ||
self.frame.setLayout(QtWidgets.QVBoxLayout()) | ||
self.frame.layout().addWidget(self) | ||
self.toolbar_file = toolbar_file | ||
|
||
if toolbar_file is not None: | ||
vertical_spacer = QtWidgets.QSpacerItem( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,4 +7,5 @@ pyqt5>=5 | |
pyqtads | ||
pyyaml | ||
qtpy | ||
ruamel.yaml | ||
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. unrelated: why are package names allowed to contain periods? 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 like it either, but it's a pretty widely used package so it seems |
||
typhos |
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.
Should we consolidate all yaml parsing to use ruamel in a future PR? It's kind of strange to use two different libraries for the same thing.
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.
ruamel is only really necessary for writing the file, if we want to preserve comments / spacing etc. For reading pyyaml is sufficient. It's probably good to be consistent but I thought it wasn't too important