diff --git a/frappy/gui/console.py b/frappy/gui/console.py
new file mode 100644
index 0000000..9369aea
--- /dev/null
+++ b/frappy/gui/console.py
@@ -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(
+ 'Request: '
+ '%s' % 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('
'
+ 'SECoP Communication Shell
'
+ '=========================
',
+ raw=True)
+
+ def _addLogEntry(self, msg, raw=False, error=False):
+ if not raw:
+ if error:
+ msg = (''
+ % toHtmlEscaped(str(msg)).replace('\n', '
'))
+ else:
+ msg = ('%s
'
+ % toHtmlEscaped(str(msg)).replace('\n', '
'))
+
+ 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
diff --git a/frappy/gui/mainwindow.py b/frappy/gui/mainwindow.py
index d5eb762..afe4450 100644
--- a/frappy/gui/mainwindow.py
+++ b/frappy/gui/mainwindow.py
@@ -22,8 +22,8 @@
# *****************************************************************************
from frappy.gui.qt import QAction, QInputDialog, QKeySequence, QMainWindow, \
- QMessageBox, QPixmap, QSettings, QShortcut, Qt, QWidget, pyqtSignal, \
- pyqtSlot
+ QMessageBox, QObject, QPixmap, QSettings, QShortcut, Qt, QWidget, \
+ pyqtSignal, pyqtSlot
import frappy.version
from frappy.gui.connection import QSECNode
@@ -75,6 +75,19 @@ class Greeter(QWidget):
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):
recentNodesChanged = pyqtSignal()
@@ -84,6 +97,7 @@ class MainWindow(QMainWindow):
self.log = logger
self.logwin = LogWindow(logger, self)
self.logwin.hide()
+ self.historySerializer = HistorySerializer()
loadUi(self, 'mainwin.ui')
Colors._setPalette(self.palette())
@@ -194,6 +208,7 @@ class MainWindow(QMainWindow):
node = QSECNode(host, self.log, parent=self)
nodeWidget = NodeWidget(node)
nodeWidget.setParent(self)
+ nodeWidget.consoleTextSent.connect(self.historySerializer.append)
nodeWidget._rebuildAdvanced(self.actionDetailed_View.isChecked())
# Node and NodeWidget created without error
@@ -258,4 +273,5 @@ class MainWindow(QMainWindow):
for widget in self._nodeWidgets.values():
# this is only qt signals deconnecting!
widget.getSecNode().terminate_connection()
+ self.historySerializer.saveHistory()
self.logwin.onClose()
diff --git a/frappy/gui/nodewidget.py b/frappy/gui/nodewidget.py
index 3b7c78c..f71f3a7 100644
--- a/frappy/gui/nodewidget.py
+++ b/frappy/gui/nodewidget.py
@@ -21,14 +21,12 @@
#
# *****************************************************************************
-import json
from collections import OrderedDict
-from frappy.gui.qt import QCursor, QFont, QFontMetrics, QIcon, QInputDialog, \
- QMenu, QSettings, QTextCursor, QVBoxLayout, QWidget, pyqtSignal, \
- pyqtSlot, toHtmlEscaped
+from frappy.gui.qt import QCursor, QIcon, QInputDialog, QMenu, QSettings, \
+ QVBoxLayout, QWidget, pyqtSignal
-from frappy.errors import SECoPError
+from frappy.gui.console import Console
from frappy.gui.moduleoverview import ModuleOverview
from frappy.gui.modulewidget import ModuleWidget
from frappy.gui.paramview import ParameterView
@@ -36,82 +34,9 @@ from frappy.gui.plotting import getPlotWidget
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(
- 'Request: '
- '%s' % 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(''
- 'SECoP Communication Shell
'
- '=========================
',
- raw=True)
-
- def _addLogEntry(self, msg, raw=False, error=False):
- if not raw:
- if error:
- msg = (''
- % toHtmlEscaped(str(msg)).replace('\n', '
'))
- else:
- msg = ('%s
'
- % toHtmlEscaped(str(msg)).replace('\n', '
'))
-
- 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):
noPlots = pyqtSignal(bool)
+ consoleTextSent = pyqtSignal(str)
def __init__(self, node, parent=None):
super().__init__(parent)
@@ -139,6 +64,7 @@ class NodeWidget(QWidget):
self.consoleWidget.setTitle('Console')
cmd = Console(node, self.consoleWidget)
+ cmd.msgLineEdit.sentText.connect(self.consoleTextSent.emit)
self.consoleWidget.replaceWidget(cmd)
viewLayout = self.viewContent.layout()
diff --git a/frappy/gui/qt.py b/frappy/gui/qt.py
index c4c110e..7c671e3 100644
--- a/frappy/gui/qt.py
+++ b/frappy/gui/qt.py
@@ -36,8 +36,8 @@ try:
QPointF, QPropertyAnimation, QRectF, QSettings, QSize, Qt, \
pyqtProperty, pyqtSignal, pyqtSlot
from PyQt6.QtGui import QAction, QBrush, QColor, QCursor, QDrag, QFont, \
- QFontMetrics, QIcon, QKeySequence, QMouseEvent, QPainter, QPalette, \
- QPen, QPixmap, QPolygonF, QShortcut, QStandardItem, \
+ QFontMetrics, QIcon, QKeyEvent, QKeySequence, QMouseEvent, QPainter, \
+ QPalette, QPen, QPixmap, QPolygonF, QShortcut, QStandardItem, \
QStandardItemModel, QTextCursor
from PyQt6.QtWidgets import QApplication, QCheckBox, QComboBox, QDialog, \
QDialogButtonBox, QDoubleSpinBox, QFileDialog, QFrame, QGridLayout, \
@@ -56,9 +56,9 @@ except ImportError as e:
QPointF, QPropertyAnimation, QRectF, QSettings, QSize, Qt, \
pyqtProperty, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QBrush, QColor, QCursor, QDrag, QFont, \
- QFontMetrics, QIcon, QKeySequence, QMouseEvent, QPainter, QPalette, \
- QPen, QPixmap, QPolygonF, QStandardItem, QStandardItemModel, \
- QTextCursor
+ QFontMetrics, QIcon, QKeyEvent, QKeySequence, QMouseEvent, QPainter, \
+ QPalette, QPen, QPixmap, QPolygonF, QStandardItem, \
+ QStandardItemModel, QTextCursor
from PyQt5.QtWidgets import QAction, QApplication, QCheckBox, QComboBox, \
QDialog, QDialogButtonBox, QDoubleSpinBox, QFileDialog, QFrame, \
QGridLayout, QGroupBox, QHBoxLayout, QInputDialog, QLabel, QLineEdit, \
diff --git a/frappy/gui/ui/console.ui b/frappy/gui/ui/console.ui
index bf7722d..66cc251 100644
--- a/frappy/gui/ui/console.ui
+++ b/frappy/gui/ui/console.ui
@@ -65,7 +65,7 @@ p, li { white-space: pre-wrap; }
-
-
+
-
@@ -91,6 +91,13 @@ p, li { white-space: pre-wrap; }
+
+
+ ConsoleLineEdit
+ QLineEdit
+
+
+