gui: add console history
+ add HistorySerializer to merge histories in correct order * move console to own file * promote msgLineEdit to class based on NICOS-HistoryLineEdit Change-Id: I853d49a70640f38275c8762ab345003db5ec5592 Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/30753 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>
This commit is contained in:
parent
41f3a2ecd4
commit
2f730ab444
190
frappy/gui/console.py
Normal file
190
frappy/gui/console.py
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from frappy.gui.qt import QApplication, QFont, QFontMetrics, QKeyEvent, \
|
||||||
|
QLineEdit, QSettings, Qt, QTextCursor, QWidget, pyqtSignal, pyqtSlot, \
|
||||||
|
toHtmlEscaped
|
||||||
|
|
||||||
|
from frappy.errors import SECoPError
|
||||||
|
from frappy.gui.util import loadUi
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleLineEdit(QLineEdit):
|
||||||
|
"""QLineEdit with history. Based on HistoryLineEdit from NICOS gui"""
|
||||||
|
sentText = pyqtSignal(str)
|
||||||
|
scrollingKeys = [Qt.Key.Key_Up, Qt.Key.Key_Down, Qt.Key.Key_PageUp,
|
||||||
|
Qt.Key.Key_PageDown]
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
settings = QSettings()
|
||||||
|
self.history = settings.value('consoleHistory', [])
|
||||||
|
self.scrollWidget = None
|
||||||
|
self._start_text = ''
|
||||||
|
self._current = -1
|
||||||
|
|
||||||
|
def keyPressEvent(self, kev):
|
||||||
|
key_code = kev.key()
|
||||||
|
|
||||||
|
# if it's a shifted scroll key...
|
||||||
|
if kev.modifiers() & Qt.KeyboardModifier.ShiftModifier and \
|
||||||
|
self.scrollWidget and \
|
||||||
|
key_code in self.scrollingKeys:
|
||||||
|
# create a new, unshifted key event and send it to the
|
||||||
|
# scrolling widget
|
||||||
|
nev = QKeyEvent(kev.type(), kev.key(),
|
||||||
|
Qt.KeyboardModifier.NoModifier)
|
||||||
|
QApplication.sendEvent(self.scrollWidget, nev)
|
||||||
|
return
|
||||||
|
|
||||||
|
if key_code == Qt.Key.Key_Escape:
|
||||||
|
# abort history search
|
||||||
|
self.setText(self._start_text)
|
||||||
|
self._current = -1
|
||||||
|
QLineEdit.keyPressEvent(self, kev)
|
||||||
|
|
||||||
|
elif key_code == Qt.Key.Key_Up:
|
||||||
|
# go earlier
|
||||||
|
if self._current == -1:
|
||||||
|
self._start_text = self.text()
|
||||||
|
self._current = len(self.history)
|
||||||
|
self.stepHistory(-1)
|
||||||
|
elif key_code == Qt.Key.Key_Down:
|
||||||
|
# go later
|
||||||
|
if self._current == -1:
|
||||||
|
return
|
||||||
|
self.stepHistory(1)
|
||||||
|
|
||||||
|
elif key_code == Qt.Key.Key_PageUp:
|
||||||
|
# go earlier with prefix
|
||||||
|
if self._current == -1:
|
||||||
|
self._current = len(self.history)
|
||||||
|
self._start_text = self.text()
|
||||||
|
prefix = self.text()[:self.cursorPosition()]
|
||||||
|
self.stepHistoryUntil(prefix, 'up')
|
||||||
|
|
||||||
|
elif key_code == Qt.Key.Key_PageDown:
|
||||||
|
# go later with prefix
|
||||||
|
if self._current == -1:
|
||||||
|
return
|
||||||
|
prefix = self.text()[:self.cursorPosition()]
|
||||||
|
self.stepHistoryUntil(prefix, 'down')
|
||||||
|
|
||||||
|
elif key_code in (Qt.Key.Key_Return, key_code == Qt.Key.Key_Enter):
|
||||||
|
# accept - add to history and do normal processing
|
||||||
|
self._current = -1
|
||||||
|
text = self.text()
|
||||||
|
if text and (not self.history or self.history[-1] != text):
|
||||||
|
# append to history, but only if it isn't equal to the last
|
||||||
|
self.history.append(text)
|
||||||
|
self.sentText.emit(text)
|
||||||
|
QLineEdit.keyPressEvent(self, kev)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# process normally
|
||||||
|
QLineEdit.keyPressEvent(self, kev)
|
||||||
|
|
||||||
|
def stepHistory(self, num):
|
||||||
|
self._current += num
|
||||||
|
if self._current <= -1:
|
||||||
|
# no further
|
||||||
|
self._current = 0
|
||||||
|
return
|
||||||
|
if self._current >= len(self.history):
|
||||||
|
# back to start
|
||||||
|
self._current = -1
|
||||||
|
self.setText(self._start_text)
|
||||||
|
return
|
||||||
|
self.setText(self.history[self._current])
|
||||||
|
|
||||||
|
def stepHistoryUntil(self, prefix, direction):
|
||||||
|
if direction == 'up':
|
||||||
|
lookrange = range(self._current - 1, -1, -1)
|
||||||
|
else:
|
||||||
|
lookrange = range(self._current + 1, len(self.history))
|
||||||
|
for i in lookrange:
|
||||||
|
if self.history[i].startswith(prefix):
|
||||||
|
self._current = i
|
||||||
|
self.setText(self.history[i])
|
||||||
|
self.setCursorPosition(len(prefix))
|
||||||
|
return
|
||||||
|
if direction == 'down':
|
||||||
|
# nothing found: go back to start
|
||||||
|
self._current = -1
|
||||||
|
self.setText(self._start_text)
|
||||||
|
self.setCursorPosition(len(prefix))
|
||||||
|
|
||||||
|
|
||||||
|
class Console(QWidget):
|
||||||
|
def __init__(self, node, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
loadUi(self, 'console.ui')
|
||||||
|
self._node = node
|
||||||
|
self._clearLog()
|
||||||
|
self.msgLineEdit.scrollWidget = self.logTextBrowser
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def on_sendPushButton_clicked(self):
|
||||||
|
msg = self.msgLineEdit.text().strip()
|
||||||
|
|
||||||
|
if not msg:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._addLogEntry(
|
||||||
|
'<span style="font-weight:bold">Request:</span> '
|
||||||
|
'<tt>%s</tt>' % toHtmlEscaped(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.rstrip('\n'))
|
||||||
|
else:
|
||||||
|
self._addLogEntry(reply.rstrip('\n'))
|
||||||
|
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), error=True)
|
||||||
|
except Exception as e:
|
||||||
|
self._addLogEntry('error when sending %r: %r' % (msg, e),
|
||||||
|
error=True)
|
||||||
|
|
||||||
|
self.msgLineEdit.clear()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def on_clearPushButton_clicked(self):
|
||||||
|
self._clearLog()
|
||||||
|
|
||||||
|
def _clearLog(self):
|
||||||
|
self.logTextBrowser.clear()
|
||||||
|
|
||||||
|
self._addLogEntry('<div style="font-weight: bold">'
|
||||||
|
'SECoP Communication Shell<br/>'
|
||||||
|
'=========================<br/></div>',
|
||||||
|
raw=True)
|
||||||
|
|
||||||
|
def _addLogEntry(self, msg, raw=False, error=False):
|
||||||
|
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
|
||||||
|
|
||||||
|
self.logTextBrowser.setHtml(content)
|
||||||
|
self.logTextBrowser.moveCursor(QTextCursor.MoveOperation.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
|
@ -22,8 +22,8 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
|
|
||||||
from frappy.gui.qt import QAction, QInputDialog, QKeySequence, QMainWindow, \
|
from frappy.gui.qt import QAction, QInputDialog, QKeySequence, QMainWindow, \
|
||||||
QMessageBox, QPixmap, QSettings, QShortcut, Qt, QWidget, pyqtSignal, \
|
QMessageBox, QObject, QPixmap, QSettings, QShortcut, Qt, QWidget, \
|
||||||
pyqtSlot
|
pyqtSignal, pyqtSlot
|
||||||
|
|
||||||
import frappy.version
|
import frappy.version
|
||||||
from frappy.gui.connection import QSECNode
|
from frappy.gui.connection import QSECNode
|
||||||
@ -75,6 +75,19 @@ class Greeter(QWidget):
|
|||||||
self.addnodes.emit([item.text()])
|
self.addnodes.emit([item.text()])
|
||||||
|
|
||||||
|
|
||||||
|
class HistorySerializer(QObject):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
settings = QSettings()
|
||||||
|
self.history = settings.value('consoleHistory', [])
|
||||||
|
|
||||||
|
def append(self, text):
|
||||||
|
self.history.append(text)
|
||||||
|
|
||||||
|
def saveHistory(self):
|
||||||
|
settings = QSettings()
|
||||||
|
settings.setValue('consoleHistory', self.history)
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
recentNodesChanged = pyqtSignal()
|
recentNodesChanged = pyqtSignal()
|
||||||
|
|
||||||
@ -84,6 +97,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.log = logger
|
self.log = logger
|
||||||
self.logwin = LogWindow(logger, self)
|
self.logwin = LogWindow(logger, self)
|
||||||
self.logwin.hide()
|
self.logwin.hide()
|
||||||
|
self.historySerializer = HistorySerializer()
|
||||||
|
|
||||||
loadUi(self, 'mainwin.ui')
|
loadUi(self, 'mainwin.ui')
|
||||||
Colors._setPalette(self.palette())
|
Colors._setPalette(self.palette())
|
||||||
@ -194,6 +208,7 @@ class MainWindow(QMainWindow):
|
|||||||
node = QSECNode(host, self.log, parent=self)
|
node = QSECNode(host, self.log, parent=self)
|
||||||
nodeWidget = NodeWidget(node)
|
nodeWidget = NodeWidget(node)
|
||||||
nodeWidget.setParent(self)
|
nodeWidget.setParent(self)
|
||||||
|
nodeWidget.consoleTextSent.connect(self.historySerializer.append)
|
||||||
nodeWidget._rebuildAdvanced(self.actionDetailed_View.isChecked())
|
nodeWidget._rebuildAdvanced(self.actionDetailed_View.isChecked())
|
||||||
|
|
||||||
# Node and NodeWidget created without error
|
# Node and NodeWidget created without error
|
||||||
@ -258,4 +273,5 @@ class MainWindow(QMainWindow):
|
|||||||
for widget in self._nodeWidgets.values():
|
for widget in self._nodeWidgets.values():
|
||||||
# this is only qt signals deconnecting!
|
# this is only qt signals deconnecting!
|
||||||
widget.getSecNode().terminate_connection()
|
widget.getSecNode().terminate_connection()
|
||||||
|
self.historySerializer.saveHistory()
|
||||||
self.logwin.onClose()
|
self.logwin.onClose()
|
||||||
|
@ -21,14 +21,12 @@
|
|||||||
#
|
#
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
|
|
||||||
import json
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from frappy.gui.qt import QCursor, QFont, QFontMetrics, QIcon, QInputDialog, \
|
from frappy.gui.qt import QCursor, QIcon, QInputDialog, QMenu, QSettings, \
|
||||||
QMenu, QSettings, QTextCursor, QVBoxLayout, QWidget, pyqtSignal, \
|
QVBoxLayout, QWidget, pyqtSignal
|
||||||
pyqtSlot, toHtmlEscaped
|
|
||||||
|
|
||||||
from frappy.errors import SECoPError
|
from frappy.gui.console import Console
|
||||||
from frappy.gui.moduleoverview import ModuleOverview
|
from frappy.gui.moduleoverview import ModuleOverview
|
||||||
from frappy.gui.modulewidget import ModuleWidget
|
from frappy.gui.modulewidget import ModuleWidget
|
||||||
from frappy.gui.paramview import ParameterView
|
from frappy.gui.paramview import ParameterView
|
||||||
@ -36,82 +34,9 @@ from frappy.gui.plotting import getPlotWidget
|
|||||||
from frappy.gui.util import Colors, loadUi
|
from frappy.gui.util import Colors, loadUi
|
||||||
|
|
||||||
|
|
||||||
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> '
|
|
||||||
'<tt>%s</tt>' % toHtmlEscaped(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.rstrip('\n'))
|
|
||||||
else:
|
|
||||||
self._addLogEntry(reply.rstrip('\n'))
|
|
||||||
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), error=True)
|
|
||||||
except Exception as e:
|
|
||||||
self._addLogEntry('error when sending %r: %r' % (msg, e),
|
|
||||||
error=True)
|
|
||||||
|
|
||||||
self.msgLineEdit.selectAll()
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def on_clearPushButton_clicked(self):
|
|
||||||
self._clearLog()
|
|
||||||
|
|
||||||
def _clearLog(self):
|
|
||||||
self.logTextBrowser.clear()
|
|
||||||
|
|
||||||
self._addLogEntry('<div style="font-weight: bold">'
|
|
||||||
'SECoP Communication Shell<br/>'
|
|
||||||
'=========================<br/></div>',
|
|
||||||
raw=True)
|
|
||||||
|
|
||||||
def _addLogEntry(self, msg, raw=False, error=False):
|
|
||||||
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
|
|
||||||
|
|
||||||
self.logTextBrowser.setHtml(content)
|
|
||||||
self.logTextBrowser.moveCursor(QTextCursor.MoveOperation.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):
|
class NodeWidget(QWidget):
|
||||||
noPlots = pyqtSignal(bool)
|
noPlots = pyqtSignal(bool)
|
||||||
|
consoleTextSent = pyqtSignal(str)
|
||||||
|
|
||||||
def __init__(self, node, parent=None):
|
def __init__(self, node, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@ -139,6 +64,7 @@ class NodeWidget(QWidget):
|
|||||||
|
|
||||||
self.consoleWidget.setTitle('Console')
|
self.consoleWidget.setTitle('Console')
|
||||||
cmd = Console(node, self.consoleWidget)
|
cmd = Console(node, self.consoleWidget)
|
||||||
|
cmd.msgLineEdit.sentText.connect(self.consoleTextSent.emit)
|
||||||
self.consoleWidget.replaceWidget(cmd)
|
self.consoleWidget.replaceWidget(cmd)
|
||||||
|
|
||||||
viewLayout = self.viewContent.layout()
|
viewLayout = self.viewContent.layout()
|
||||||
|
@ -36,8 +36,8 @@ try:
|
|||||||
QPointF, QPropertyAnimation, QRectF, QSettings, QSize, Qt, \
|
QPointF, QPropertyAnimation, QRectF, QSettings, QSize, Qt, \
|
||||||
pyqtProperty, pyqtSignal, pyqtSlot
|
pyqtProperty, pyqtSignal, pyqtSlot
|
||||||
from PyQt6.QtGui import QAction, QBrush, QColor, QCursor, QDrag, QFont, \
|
from PyQt6.QtGui import QAction, QBrush, QColor, QCursor, QDrag, QFont, \
|
||||||
QFontMetrics, QIcon, QKeySequence, QMouseEvent, QPainter, QPalette, \
|
QFontMetrics, QIcon, QKeyEvent, QKeySequence, QMouseEvent, QPainter, \
|
||||||
QPen, QPixmap, QPolygonF, QShortcut, QStandardItem, \
|
QPalette, QPen, QPixmap, QPolygonF, QShortcut, QStandardItem, \
|
||||||
QStandardItemModel, QTextCursor
|
QStandardItemModel, QTextCursor
|
||||||
from PyQt6.QtWidgets import QApplication, QCheckBox, QComboBox, QDialog, \
|
from PyQt6.QtWidgets import QApplication, QCheckBox, QComboBox, QDialog, \
|
||||||
QDialogButtonBox, QDoubleSpinBox, QFileDialog, QFrame, QGridLayout, \
|
QDialogButtonBox, QDoubleSpinBox, QFileDialog, QFrame, QGridLayout, \
|
||||||
@ -56,9 +56,9 @@ except ImportError as e:
|
|||||||
QPointF, QPropertyAnimation, QRectF, QSettings, QSize, Qt, \
|
QPointF, QPropertyAnimation, QRectF, QSettings, QSize, Qt, \
|
||||||
pyqtProperty, pyqtSignal, pyqtSlot
|
pyqtProperty, pyqtSignal, pyqtSlot
|
||||||
from PyQt5.QtGui import QBrush, QColor, QCursor, QDrag, QFont, \
|
from PyQt5.QtGui import QBrush, QColor, QCursor, QDrag, QFont, \
|
||||||
QFontMetrics, QIcon, QKeySequence, QMouseEvent, QPainter, QPalette, \
|
QFontMetrics, QIcon, QKeyEvent, QKeySequence, QMouseEvent, QPainter, \
|
||||||
QPen, QPixmap, QPolygonF, QStandardItem, QStandardItemModel, \
|
QPalette, QPen, QPixmap, QPolygonF, QStandardItem, \
|
||||||
QTextCursor
|
QStandardItemModel, QTextCursor
|
||||||
from PyQt5.QtWidgets import QAction, QApplication, QCheckBox, QComboBox, \
|
from PyQt5.QtWidgets import QAction, QApplication, QCheckBox, QComboBox, \
|
||||||
QDialog, QDialogButtonBox, QDoubleSpinBox, QFileDialog, QFrame, \
|
QDialog, QDialogButtonBox, QDoubleSpinBox, QFileDialog, QFrame, \
|
||||||
QGridLayout, QGroupBox, QHBoxLayout, QInputDialog, QLabel, QLineEdit, \
|
QGridLayout, QGroupBox, QHBoxLayout, QInputDialog, QLabel, QLineEdit, \
|
||||||
|
@ -65,7 +65,7 @@ p, li { white-space: pre-wrap; }
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLineEdit" name="msgLineEdit"/>
|
<widget class="ConsoleLineEdit" name="msgLineEdit"/>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="clearPushButton">
|
<widget class="QPushButton" name="clearPushButton">
|
||||||
@ -91,6 +91,13 @@ p, li { white-space: pre-wrap; }
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>ConsoleLineEdit</class>
|
||||||
|
<extends>QLineEdit</extends>
|
||||||
|
<header>frappy.gui.console.h</header>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
<resources/>
|
<resources/>
|
||||||
<connections>
|
<connections>
|
||||||
<connection>
|
<connection>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user