
Change-Id: Ie0525e4ef9a94085da811e7eaa2e0b7430bade95 Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/33388 Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de> Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de> Reviewed-by: Alexander Zaft <a.zaft@fz-juelich.de>
459 lines
16 KiB
Python
459 lines
16 KiB
Python
# *****************************************************************************
|
|
# Copyright (c) 2015-2023 by the authors, see LICENSE
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify it under
|
|
# the terms of the GNU General Public License as published by the Free Software
|
|
# Foundation; either version 2 of the License, or (at your option) any later
|
|
# version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along with
|
|
# this program; if not, write to the Free Software Foundation, Inc.,
|
|
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
#
|
|
# Module authors:
|
|
# Alexander Zaft <a.zaft@fz-juelich.de>
|
|
#
|
|
# *****************************************************************************
|
|
|
|
from frappy.gui.qt import QColor, QDialog, QHBoxLayout, QIcon, QLabel, \
|
|
QLineEdit, QMessageBox, QPropertyAnimation, QPushButton, Qt, QToolButton, \
|
|
QWidget, pyqtProperty, pyqtSignal
|
|
|
|
from frappy.gui.inputwidgets import get_input_widget
|
|
from frappy.gui.util import Colors, loadUi
|
|
from frappy.gui.valuewidgets import get_widget
|
|
|
|
|
|
class CommandDialog(QDialog):
|
|
def __init__(self, cmdname, argument, parent=None):
|
|
super().__init__(parent)
|
|
loadUi(self, 'cmddialog.ui')
|
|
|
|
self.setWindowTitle(f'Arguments for {cmdname}')
|
|
# row = 0
|
|
|
|
self._labels = []
|
|
self.widgets = []
|
|
# improve! recursive?
|
|
dtype = argument
|
|
label = QLabel(repr(dtype))
|
|
label.setWordWrap(True)
|
|
widget = get_widget(dtype, readonly=False)
|
|
self.gridLayout.addWidget(label, 0, 0)
|
|
self.gridLayout.addWidget(widget, 0, 1)
|
|
self._labels.append(label)
|
|
self.widgets.append(widget)
|
|
|
|
self.gridLayout.setRowStretch(1, 1)
|
|
self.setModal(True)
|
|
self.resize(self.sizeHint())
|
|
|
|
def get_value(self):
|
|
try:
|
|
return self.widgets[0].get_value()
|
|
except Exception as e:
|
|
QMessageBox.warning(self.parent(), 'Operation failed', str(e))
|
|
return None
|
|
|
|
def exec(self):
|
|
if super().exec():
|
|
return self.get_value()
|
|
return None
|
|
|
|
|
|
def showCommandResultDialog(command, args, result, extras=''):
|
|
m = QMessageBox()
|
|
args = '' if args is None else repr(args)
|
|
m.setText(f'calling: {command}({args})\nyielded: {result!r}\nqualifiers: {extras}')
|
|
m.exec()
|
|
|
|
|
|
def showErrorDialog(command, args, error):
|
|
m = QMessageBox()
|
|
args = '' if args is None else repr(args)
|
|
m.setText(f'calling: {command}({args})\nraised {error!r}')
|
|
m.exec()
|
|
|
|
|
|
class CommandButton(QPushButton):
|
|
def __init__(self, cmdname, cmdinfo, cb, parent=None):
|
|
super().__init__(parent)
|
|
|
|
self._cmdname = cmdname
|
|
self._argintype = cmdinfo['datatype'].argument # single datatype
|
|
self.result = cmdinfo['datatype'].result
|
|
self._cb = cb # callback function for exection
|
|
|
|
self.setText(cmdname)
|
|
if cmdinfo['description']:
|
|
self.setToolTip(cmdinfo['description'])
|
|
self.pressed.connect(self.on_pushButton_pressed)
|
|
|
|
def on_pushButton_pressed(self):
|
|
#self.setEnabled(False)
|
|
if self._argintype:
|
|
dlg = CommandDialog(self._cmdname, self._argintype)
|
|
args = dlg.exec()
|
|
if args is not None:
|
|
# no errors when converting value and 'Cancel' wasn't clicked
|
|
self._cb(self._cmdname, args)
|
|
else:
|
|
# no need for arguments
|
|
self._cb(self._cmdname, None)
|
|
#self.setEnabled(True)
|
|
|
|
|
|
class AnimatedLabel(QLabel):
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setAutoFillBackground(True)
|
|
self.backgroundColor = self.palette().color(self.backgroundRole())
|
|
self.animation = QPropertyAnimation(self, b"bgColor", self)
|
|
self.animation.setDuration(350)
|
|
self.animation.setStartValue(Colors.colors['yellow'])
|
|
self.animation.setEndValue(self.backgroundColor)
|
|
|
|
@pyqtProperty(QColor)
|
|
def bgColor(self):
|
|
return self.palette().color(self.backgroundRole())
|
|
|
|
@bgColor.setter
|
|
def bgColor(self, color):
|
|
p = self.palette()
|
|
p.setColor(self.backgroundRole(), color)
|
|
self.setPalette(p)
|
|
|
|
def triggerAnimation(self):
|
|
self.animation.start()
|
|
|
|
|
|
class AnimatedLabelHandthrough(QWidget):
|
|
"""This class is a crutch for the failings of the current grouping
|
|
implementation. TODO: It has to be removed in the grouping rework """
|
|
def __init__(self, label, btn, parent=None):
|
|
super().__init__(parent)
|
|
self.label = label
|
|
box = QHBoxLayout()
|
|
box.addWidget(btn)
|
|
box.addWidget(label)
|
|
box.setContentsMargins(0,0,0,0)
|
|
self.setLayout(box)
|
|
|
|
def triggerAnimation(self):
|
|
self.label.triggerAnimation()
|
|
|
|
|
|
class ModuleWidget(QWidget):
|
|
plot = pyqtSignal(str)
|
|
plotAdd = pyqtSignal(str)
|
|
paramDetails = pyqtSignal(str, str)
|
|
def __init__(self, node, name, parent=None):
|
|
super().__init__(parent)
|
|
loadUi(self, 'modulewidget.ui')
|
|
self._node = node
|
|
self._name = name
|
|
self._paramDisplays = {}
|
|
self._paramInputs = {}
|
|
self._addbtns = []
|
|
self._paramWidgets = {}
|
|
self._groups = {}
|
|
self._groupStatus = {}
|
|
self.independentParams = []
|
|
|
|
self._initModuleInfo()
|
|
self.infoGrid.hide()
|
|
|
|
row = 0
|
|
params = dict(self._node.getParameters(self._name))
|
|
if 'status' in params:
|
|
params.pop('status')
|
|
self._addRParam('status', row)
|
|
row += 1
|
|
if 'value' in params:
|
|
params.pop('value')
|
|
self._addRParam('value', row)
|
|
row += 1
|
|
if 'target' in params:
|
|
params.pop('target')
|
|
self._addRWParam('target', row)
|
|
row += 1
|
|
|
|
allGroups = set()
|
|
for param in params:
|
|
props = self._node.getProperties(self._name, param)
|
|
group = props.get('group', '')
|
|
if group:
|
|
allGroups.add(group)
|
|
self._groups.setdefault(group, []).append(param)
|
|
else:
|
|
self.independentParams.append(param)
|
|
for key in sorted(allGroups.union(set(self.independentParams))):
|
|
if key in allGroups:
|
|
if key in self._groups[key]:
|
|
# Param with same name as group
|
|
self._addParam(key, row)
|
|
name = AnimatedLabel(key)
|
|
button = QToolButton()
|
|
button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextOnly)
|
|
button.setText('+')
|
|
button.setObjectName('collapseButton')
|
|
button.pressed.connect(
|
|
lambda group=key: self._toggleGroupCollapse(group))
|
|
groupLabel = AnimatedLabelHandthrough(name, button)
|
|
|
|
l = self.moduleDisplay.layout()
|
|
label = l.itemAtPosition(row, 0).widget()
|
|
l.replaceWidget(label, groupLabel)
|
|
row += 1
|
|
old = self._paramWidgets[key].pop(0)
|
|
old.setParent(None)
|
|
self._paramWidgets[key].insert(0, groupLabel)
|
|
self._setParamHidden(key, True)
|
|
else:
|
|
name = AnimatedLabel(key)
|
|
button = QToolButton()
|
|
button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextOnly)
|
|
button.setText('+')
|
|
button.setObjectName('collapseButton')
|
|
button.pressed.connect(
|
|
lambda group=key: self._toggleGroupCollapse(group))
|
|
box = QHBoxLayout()
|
|
box.addWidget(button)
|
|
box.addWidget(name)
|
|
box.setContentsMargins(0,0,0,0)
|
|
groupLabel = QWidget()
|
|
groupLabel.setLayout(box)
|
|
|
|
l = self.moduleDisplay.layout()
|
|
l.addWidget(groupLabel, row, 0)
|
|
row += 1
|
|
self._paramWidgets[key] = [groupLabel]
|
|
self._groups[key].append(key)
|
|
self._setParamHidden(key, True)
|
|
for p in self._groups[key]:
|
|
if p == key:
|
|
continue
|
|
self._addParam(p, row)
|
|
row += 1
|
|
self._setParamHidden(p, True)
|
|
else:
|
|
self._addParam(key, row)
|
|
row += 1
|
|
self._setParamHidden(key, True)
|
|
|
|
self._addCommands(row)
|
|
|
|
cache = self._node.queryCache(self._name)
|
|
for param, val in cache.items():
|
|
self._updateValue(self._name, param, val)
|
|
|
|
node.newData.connect(self._updateValue)
|
|
|
|
def _initModuleInfo(self):
|
|
props = dict(self._node.getModuleProperties(self._name))
|
|
self.moduleName.setText(self._name)
|
|
self._moduleDescription = props.pop('description',
|
|
'no description provided')
|
|
text = self._moduleDescription.split('\n', 1)[0]
|
|
self.moduleDescription.setText(text)
|
|
|
|
self.groupInfo.setText(props.pop('group', '-'))
|
|
feats = ','.join(props.pop('features', [])) or '-'
|
|
self.featuresInfo.setText(feats)
|
|
self.implementationInfo.setText(props.pop('implementation', 'MISSING'))
|
|
ifaces = ','.join(props.pop('interface_classes', [])) or '-'
|
|
self.interfaceClassesInfo.setText(ifaces)
|
|
|
|
# any additional properties are added after the standard ones
|
|
row = 2
|
|
count = 0
|
|
for prop, value in props.items():
|
|
l = QHBoxLayout()
|
|
l.setContentsMargins(0,0,0,0)
|
|
name = QLabel(f'<b>{prop.capitalize()}:</b>')
|
|
val = QLabel(str(value))
|
|
val.setWordWrap(True)
|
|
l.addWidget(name)
|
|
l.addWidget(val)
|
|
additional = QWidget()
|
|
additional.setLayout(l)
|
|
self.infoGrid.layout().addWidget(
|
|
additional, row + count // 2, count % 2)
|
|
count += 1
|
|
|
|
def on_showDetailsBtn_toggled(self, checked):
|
|
self.showDetails(checked)
|
|
|
|
def _updateValue(self, mod, param, val):
|
|
if mod != self._name:
|
|
return
|
|
if param in self._paramDisplays:
|
|
self._paramDisplays[param].setText(val.formatted())
|
|
|
|
def _addParam(self, param, row):
|
|
paramProps = self._node.getProperties(self._name, param)
|
|
if paramProps['readonly']:
|
|
self._addRParam(param, row)
|
|
else:
|
|
self._addRWParam(param, row)
|
|
|
|
def _addRParam(self, param, row):
|
|
nameLabel = AnimatedLabel(param)
|
|
display = QLineEdit()
|
|
|
|
p = display.palette()
|
|
p.setColor(display.backgroundRole(), Colors.palette.window().color())
|
|
display.setPalette(p)
|
|
self._paramDisplays[param] = display
|
|
self._paramWidgets[param] = [nameLabel, display]
|
|
|
|
l = self.moduleDisplay.layout()
|
|
l.addWidget(nameLabel, row,0,1,1)
|
|
l.addWidget(display, row,1,1,5)
|
|
l.addWidget(QLabel(''), row,6,1,1)
|
|
self._addButtons(param, row)
|
|
|
|
def _addRWParam(self, param, row):
|
|
nameLabel = AnimatedLabel(param)
|
|
display = QLineEdit()
|
|
props = self._node.getProperties(self._name, param)
|
|
inputEdit = get_input_widget(props.get('datatype'))
|
|
submitButton = QPushButton('set')
|
|
submitButton.setIcon(QIcon(':/icons/submit'))
|
|
|
|
p = display.palette()
|
|
p.setColor(display.backgroundRole(), Colors.palette.window().color())
|
|
display.setPalette(p)
|
|
submitButton.pressed.connect(lambda: self._button_pressed(param))
|
|
inputEdit.submitted.connect(lambda param=param: self._button_pressed(param))
|
|
self._paramDisplays[param] = display
|
|
self._paramInputs[param] = inputEdit
|
|
self._paramWidgets[param] = [nameLabel, display, inputEdit, submitButton]
|
|
|
|
l = self.moduleDisplay.layout()
|
|
l.addWidget(nameLabel, row,0,1,1)
|
|
l.addWidget(display, row,1,1,2)
|
|
l.addWidget(inputEdit, row,4,1,2)
|
|
l.addWidget(submitButton, row, 6)
|
|
self._addButtons(param, row)
|
|
|
|
def _addButtons(self, param, row):
|
|
if param == 'status':
|
|
return
|
|
plotButton = QToolButton()
|
|
plotButton.setIcon(QIcon(':/icons/plot'))
|
|
plotButton.setToolTip(f'Plot {param}')
|
|
plotAddButton = QToolButton()
|
|
plotAddButton.setIcon(QIcon(':/icons/plot-add'))
|
|
plotAddButton.setToolTip('Plot With...')
|
|
|
|
detailsButton= QToolButton()
|
|
detailsButton.setIcon(QIcon(':/icons/plot-add'))
|
|
detailsButton.setToolTip('show parameter details')
|
|
|
|
plotButton.clicked.connect(lambda: self.plot.emit(param))
|
|
plotAddButton.clicked.connect(lambda: self.plotAdd.emit(param))
|
|
detailsButton.clicked.connect(lambda: self.showParamDetails(param))
|
|
|
|
self._addbtns.append(plotAddButton)
|
|
plotAddButton.setDisabled(True)
|
|
self._paramWidgets[param].append(plotButton)
|
|
self._paramWidgets[param].append(plotAddButton)
|
|
self._paramWidgets[param].append(detailsButton)
|
|
|
|
l = self.moduleDisplay.layout()
|
|
l.addWidget(plotButton, row, 7)
|
|
l.addWidget(plotAddButton, row, 8)
|
|
l.addWidget(detailsButton, row, 9)
|
|
|
|
def _addCommands(self, startrow):
|
|
cmdicons = {
|
|
'stop': QIcon(':/icons/stop'),
|
|
}
|
|
cmds = self._node.getCommands(self._name)
|
|
if not cmds:
|
|
return
|
|
|
|
l = self.moduleDisplay.layout()
|
|
# max cols in GridLayout, find out programmatically?
|
|
maxcols = 7
|
|
l.addWidget(QLabel('Commands:'))
|
|
for (i, cmd) in enumerate(cmds):
|
|
cmdb = CommandButton(cmd, cmds[cmd], self._execCommand)
|
|
if cmd in cmdicons:
|
|
cmdb.setIcon(cmdicons[cmd])
|
|
row = startrow + i // maxcols
|
|
col = (i % maxcols) + 1
|
|
l.addWidget(cmdb, row, col)
|
|
|
|
|
|
def _execCommand(self, command, args=None):
|
|
try:
|
|
result, qualifiers = self._node.execCommand(
|
|
self._name, command, args)
|
|
except Exception as e:
|
|
showErrorDialog(command, args, e)
|
|
return
|
|
if result is not None:
|
|
showCommandResultDialog(command, args, result, qualifiers)
|
|
|
|
def _setParamHidden(self, param, hidden):
|
|
for w in self._paramWidgets[param]:
|
|
w.setHidden(hidden)
|
|
|
|
def _toggleGroupCollapse(self, group):
|
|
collapsed = not self._groupStatus.get(group, True)
|
|
self._groupStatus[group] = collapsed
|
|
for param in self._groups[group]:
|
|
if param == group: # dont hide the top level
|
|
btn = self._paramWidgets[param][0].findChild(QToolButton,
|
|
'collapseButton')
|
|
if collapsed:
|
|
btn.setText('+')
|
|
else:
|
|
btn.setText('-')
|
|
continue
|
|
self._setParamHidden(param, collapsed)
|
|
|
|
def _setGroupHidden(self, group, show):
|
|
for param in self._groups[group]:
|
|
if show and param == group: # dont hide the top level
|
|
self._setParamHidden(param, False)
|
|
elif show and self._groupStatus.get(group, False):
|
|
self._setParamHidden(param, False)
|
|
else:
|
|
self._setParamHidden(param, True)
|
|
|
|
def showDetails(self, show):
|
|
if show:
|
|
self.moduleDescription.setText(self._moduleDescription)
|
|
else:
|
|
text = self._moduleDescription.split('\n', 1)[0]
|
|
self.moduleDescription.setText(text)
|
|
self.infoGrid.setHidden(not show)
|
|
for param in self.independentParams:
|
|
if param in ['value', 'status', 'target']:
|
|
continue
|
|
self._setParamHidden(param, not show)
|
|
for group in self._groups:
|
|
self._setGroupHidden(group, show)
|
|
|
|
def showParamDetails(self, param):
|
|
self.paramDetails.emit(self._name, param)
|
|
|
|
def _button_pressed(self, param):
|
|
try:
|
|
target = self._paramInputs[param].get_input()
|
|
self._node.setParameter(self._name, param, target)
|
|
except Exception as e:
|
|
QMessageBox.warning(self.parent(), 'Operation failed', str(e))
|
|
|
|
def plotsPresent(self, present):
|
|
for btn in self._addbtns:
|
|
btn.setDisabled(present)
|