Rework GUI.

Work-in-progress state of the new gui.

Change-Id: Ib5e9ad2178b372fbd2914077096a9c73f025ecb7
This commit is contained in:
Alexander Zaft
2023-01-24 11:13:26 +01:00
parent 12f21996e4
commit a36d875fae
37 changed files with 4967 additions and 1403 deletions

308
frappy/gui/nodewidget.py Normal file
View File

@@ -0,0 +1,308 @@
from collections import OrderedDict
import json
import pprint
import frappy.gui.resources # pylint: disable=unused-import
from frappy.gui.qt import QFont, QFontMetrics, QLabel, QTextCursor, QWidget, \
pyqtSlot, toHtmlEscaped, QVBoxLayout, QGridLayout, QPlainTextEdit, \
QMenu, QCursor, QIcon, QInputDialog, pyqtSignal
from frappy.gui.util import Colors, loadUi
from frappy.gui.plotting import getPlotWidget
from frappy.gui.modulectrl import ModuleCtrl
from frappy.gui.paramview import ParameterView
from frappy.gui.modulewidget import ModuleWidget
from frappy.gui.moduleoverview import ModuleOverview
from frappy.errors import SECoPError
class Console(QWidget):
def __init__(self, node, parent=None):
super().__init__(parent)
loadUi(self, 'console.ui')
self._node = node
self._clearLog()
@pyqtSlot()
def on_sendPushButton_clicked(self):
msg = self.msgLineEdit.text().strip()
if not msg:
return
self._addLogEntry(
'<span style="font-weight:bold">Request:</span> '
'%s:' % msg,
raw=True)
# msg = msg.split(' ', 2)
try:
reply = self._node.syncCommunicate(*self._node.decode_message(msg))
if msg == 'describe':
_, eid, stuff = self._node.decode_message(reply)
reply = '%s %s %s' % (_, eid, json.dumps(
stuff, indent=2, separators=(',', ':'), sort_keys=True))
self._addLogEntry(reply, newline=True, pretty=False)
else:
self._addLogEntry(reply, newline=True, pretty=False)
except SECoPError as e:
einfo = e.args[0] if len(e.args) == 1 else json.dumps(e.args)
self._addLogEntry(
'%s: %s' % (e.name, einfo),
newline=True,
pretty=False,
error=True)
except Exception as e:
self._addLogEntry(
'error when sending %r: %r' % (msg, e),
newline=True,
pretty=False,
error=True)
@pyqtSlot()
def on_clearPushButton_clicked(self):
self._clearLog()
def _clearLog(self):
self.logTextBrowser.clear()
self._addLogEntry('SECoP Communication Shell')
self._addLogEntry('=========================')
self._addLogEntry('', newline=True)
def _addLogEntry(self,
msg,
newline=False,
pretty=False,
raw=False,
error=False):
if pretty:
msg = pprint.pformat(msg, width=self._getLogWidth())
msg = msg[1:-1]
if not raw:
if error:
msg = '<div style="color:#FF0000"><b><pre>%s</pre></b></div>' % toHtmlEscaped(
str(msg)).replace('\n', '<br />')
else:
msg = '<pre>%s</pre>' % toHtmlEscaped(
str(msg)).replace('\n', '<br />')
content = ''
if self.logTextBrowser.toPlainText():
content = self.logTextBrowser.toHtml()
content += msg
if newline:
content += '<br />'
self.logTextBrowser.setHtml(content)
self.logTextBrowser.moveCursor(QTextCursor.End)
def _getLogWidth(self):
fontMetrics = QFontMetrics(QFont('Monospace'))
# calculate max avail characters by using an m (which is possible
# due to monospace)
result = self.logTextBrowser.width() / fontMetrics.width('m')
return result
class NodeWidget(QWidget):
noPlots = pyqtSignal(bool)
def __init__(self, node, parent=None):
super().__init__(parent)
loadUi(self, 'nodewidget.ui')
self._node = node
self._node.stateChange.connect(self._set_node_state)
self._modules = OrderedDict()
self._detailedModules = {}
self._detailedParams = {}
self._activePlots = {}
self.top_splitter.setStretchFactor(0, 2)
self.top_splitter.setStretchFactor(1, 10)
self.top_splitter.setSizes([180, 500])
self.middle_splitter.setCollapsible(self.middle_splitter.indexOf(self.view), False)
self.middle_splitter.setStretchFactor(0, 20)
self.middle_splitter.setStretchFactor(1, 1)
self.infobox_splitter.setStretchFactor(0,3)
self.infobox_splitter.setStretchFactor(1,2)
self.consoleWidget.setTitle('Console')
cmd = Console(node, self.consoleWidget)
self.consoleWidget.replaceWidget(cmd)
viewLayout = self.viewContent.layout()
for module in node.modules:
widget = ModuleWidget(node, module, self.view)
widget.plot.connect(lambda param, module=module:
self.plotParam(module, param))
widget.plotAdd.connect(lambda param, module=module:
self._plotPopUp(module, param))
self.noPlots.connect(widget.plotsPresent)
self._modules[module] = widget
details = ModuleCtrl(node, module)
self._detailedModules[module] = details
viewLayout.addWidget(details)
details.setHidden(True)
self._detailedParams[module] = {}
for param in node.getParameters(module):
view = ParameterView(node, module, param)
self._detailedParams[module][param] = view
view.setHidden(True)
viewLayout.addWidget(view)
viewLayout.addWidget(widget)
self._initNodeInfo()
def _initNodeInfo(self):
self.tree = ModuleOverview(self._node)
infolayout = QVBoxLayout()
infolayout.addWidget(self.tree)
self.infotree.setLayout(infolayout)
# disabled until i find a way to deselect and go back to overview
self.tree.itemChanged.connect(self.changeViewContent)
self.tree.customContextMenuRequested.connect(self._treeContextMenu)
self._description = QPlainTextEdit(self._node.properties.get('description','no description available'))
self._description_label = QLabel('Description:')
self._description.setReadOnly(True)
self._host = QLabel(self._node.conn.uri)
self._host.hide()
self._host_label = QLabel('Host:')
self._host_label.hide()
self._protocoll = QLabel(self._node.conn.secop_version)
self._protocoll.setWordWrap(True)
self._protocoll.hide()
self._protocoll_label = QLabel('Protocoll Version:')
self._protocoll_label.hide()
layout = QGridLayout()
layout.addWidget(self._host_label, 0, 0)
layout.addWidget(self._host, 0, 1)
layout.addWidget(self._protocoll_label, 1, 0)
layout.addWidget(self._protocoll, 1, 1)
layout.addWidget(self._description_label, 2, 0)
layout.addWidget(self._description, 3, 0, -1, -1)
self.nodeinfo.setLayout(layout)
def _set_node_state(self, nodename, online, state):
p = self.palette()
if online:
p.setColor(self.backgroundRole(),Colors.palette.window().color())
self.tree.setToReconnected()
else:
p.setColor(self.backgroundRole(), Colors.colors['orange'])
# TODO: reset this for non-status modules!
self.tree.setToDisconnected()
self.setPalette(p)
def _rebuildAdvanced(self, advanced):
self._host.setHidden(not advanced)
self._host_label.setHidden(not advanced)
self._protocoll.setHidden(not advanced)
self._protocoll_label.setHidden(not advanced)
self.tree._rebuildAdvanced(advanced)
for m in self._modules.values():
m.rebuildAdvanced(advanced)
def getSecNode(self):
return self._node
def changeViewContent(self, module, param, prevmod, prevparam):
if prevmod == '':
for mod in self._modules.values():
mod.setHidden(True)
elif prevparam == '':
self._detailedModules[prevmod].setHidden(True)
else:
self._detailedParams[prevmod][prevparam].setHidden(True)
if module == '':
# no module -> reset to overview
for mod in self._modules.values():
mod.setHidden(False)
elif param == '':
# set to single module view
self._detailedModules[module].setHidden(False)
else:
# set to single param view
self._detailedParams[module][param].setHidden(False)
def _treeContextMenu(self, pos):
index = self.tree.indexAt(pos)
if not index.isValid():
return
item = self.tree.itemFromIndex(index)
menu = QMenu()
opt_plot = menu.addAction('Plot')
opt_plot.setIcon(QIcon(':/icons/plot'))
menu_plot_ext = menu.addMenu("Plot with...")
menu_plot_ext.setIcon(QIcon(':/icons/plot'))
if not self._activePlots:
menu_plot_ext.setEnabled(False)
else:
for (m,p),plot in self._activePlots.items():
opt_ext = menu_plot_ext.addAction("%s:%s" % (m,p))
opt_ext.triggered.connect(
lambda plot=plot: self._requestPlot(item, plot))
menu.addSeparator()
opt_clear = menu.addAction('Clear Selection')
opt_plot.triggered.connect(lambda: self._requestPlot(item))
opt_clear.triggered.connect(self.tree.clearTreeSelection)
#menu.exec_(self.mapToGlobal(pos))
menu.exec_(QCursor.pos())
def _requestPlot(self, item, plot=None):
module = item.module
param = item.param or 'value'
self.plotParam(module, param, plot)
def _plotPopUp(self, module, param):
plots = {'%s -> %s' % (m,p): (m,p) for (m,p) in self._activePlots}
dialog = QInputDialog()
#dialog.setInputMode()
dialog.setOption(QInputDialog.UseListViewForComboBoxItems)
dialog.setComboBoxItems(plots.keys())
dialog.setTextValue(list(plots)[0])
dialog.setWindowTitle('Plot %s with...' % param)
dialog.setLabelText('')
if dialog.exec_() == QInputDialog.Accepted:
item = dialog.textValue()
self.plotParam(module, param, self._activePlots[plots[item]])
def plotParam(self, module, param, plot=None):
# - liveness?
# - better plot window management?
# only allow one open plot per parameter TODO: change?
if (module, param) in self._activePlots:
return
if plot:
plot.addCurve(self._node, module, param)
plot.setCurveColor(module, param, Colors.colors[len(plot.curves) % 6])
else:
plot = getPlotWidget()
plot.addCurve(self._node, module, param)
plot.setCurveColor(module, param, Colors.colors[1])
self._activePlots[(module, param)] = plot
plot.closed.connect(lambda: self._removePlot(module, param))
plot.show()
self.noPlots.emit(len(self._activePlots) == 0)
# initial datapoint
cache = self._node.queryCache(module)
if param in cache:
plot.update(module, param, cache[param])
def _removePlot(self, module, param):
self._activePlots.pop((module,param))
self.noPlots.emit(len(self._activePlots) == 0)