diff --git a/assets/qml/Windows/Console.qml b/assets/qml/Windows/Console.qml index 79dbbb46..3e8d024d 100644 --- a/assets/qml/Windows/Console.qml +++ b/assets/qml/Windows/Console.qml @@ -164,6 +164,7 @@ Control { focus: true readOnly: true font.pixelSize: 12 + vt100emulation: true centerOnScroll: false undoRedoEnabled: false Layout.fillWidth: true @@ -299,6 +300,17 @@ Control { } } + CheckBox { + id: filterVt100 + text: qsTr("VT-100") + Layout.alignment: Qt.AlignVCenter + checked: textEdit.vt100emulation + onCheckedChanged: { + if (textEdit.vt100emulation != checked) + textEdit.vt100emulation = checked + } + } + Item { Layout.fillWidth: true } diff --git a/src/UI/QmlPlainTextEdit.cpp b/src/UI/QmlPlainTextEdit.cpp index 536ca4f4..8351aab5 100644 --- a/src/UI/QmlPlainTextEdit.cpp +++ b/src/UI/QmlPlainTextEdit.cpp @@ -46,8 +46,11 @@ using namespace UI; */ QmlPlainTextEdit::QmlPlainTextEdit(QQuickItem *parent) : QQuickPaintedItem(parent) + , m_autoscroll(true) + , m_emulateVt100(false) , m_copyAvailable(false) , m_textEdit(new QPlainTextEdit) + , m_terminalState(VT100_Text) { // Set item flags setFlag(ItemHasContents, true); @@ -255,6 +258,15 @@ bool QmlPlainTextEdit::centerOnScroll() const return textEdit()->centerOnScroll(); } +/** + * Returns true if the control shall parse basic VT-100 escape secuences. This can be + * useful if you need to interface with a shell/CLI from Serial Studio. + */ +bool QmlPlainTextEdit::vt100emulation() const +{ + return m_emulateVt100; +} + /** * This property holds whether undo and redo are enabled. * Users are only able to undo or redo actions if this property is true, and if there is @@ -485,21 +497,7 @@ void QmlPlainTextEdit::setAutoscroll(const bool enabled) */ void QmlPlainTextEdit::insertText(const QString &text) { - // Add text at the end of the text document - QTextCursor cursor(textEdit()->document()); - cursor.beginEditBlock(); - cursor.movePosition(QTextCursor::End); - cursor.insertText(text); - cursor.endEditBlock(); - - // Autoscroll to bottom (if needed) - updateScrollbarVisibility(); - if (autoscroll()) - scrollToBottom(); - - // Redraw the control - update(); - emit textChanged(); + addText(text, vt100emulation()); } /** @@ -530,6 +528,17 @@ void QmlPlainTextEdit::setCenterOnScroll(const bool enabled) emit centerOnScrollChanged(); } +/** + * Enables/disables interpretation of VT-100 escape secuences. This can be useful when + * interfacing through network ports or interfacing with a MCU that implements some + * kind of shell. + */ +void QmlPlainTextEdit::setVt100Emulation(const bool enabled) +{ + m_emulateVt100 = enabled; + emit vt100EmulationChanged(); +} + /** * Enables/disables undo/redo history support. */ @@ -638,6 +647,33 @@ void QmlPlainTextEdit::setCopyAvailable(const bool yes) emit copyAvailableChanged(); } +/** + * Inserts the given @a text directly, no additional line breaks added. + */ +void QmlPlainTextEdit::addText(const QString &text, const bool enableVt100) +{ + // Get text to insert + QString textToInsert = text; + if (enableVt100) + textToInsert = vt100Processing(text); + + // Add text at the end of the text document + QTextCursor cursor(textEdit()->document()); + cursor.beginEditBlock(); + cursor.movePosition(QTextCursor::End); + cursor.insertText(textToInsert); + cursor.endEditBlock(); + + // Autoscroll to bottom (if needed) + updateScrollbarVisibility(); + if (autoscroll()) + scrollToBottom(); + + // Redraw the control + update(); + emit textChanged(); +} + /** * Hack: call the appropiate protected mouse event handler function of the QPlainTextEdit * item depending on event type @@ -720,3 +756,114 @@ void QmlPlainTextEdit::processWheelEvents(QWheelEvent *event) } } } + +/** + * Processes the given @a data to remove the escape sequences from the text. Colors and + * text format is not processed. + * + * Implementation based on https://github.com/sebcaux/QVTerminal + * List of commands: https://espterm.github.io/docs/VT100%20escape%20codes.html + * + * I did the necessary stuff to be able to watch ASCII Star Wars from Serial Studio. + * If you want/need to do more stuff, please make a PR. + */ +QString QmlPlainTextEdit::vt100Processing(const QString &data) +{ + QString text; + QString command; + bool hasNumbers = false; + bool hasCommand = false; + + for (int i = 0; i < data.length(); ++i) + { + QChar c = data.at(i); + switch (m_terminalState) + { + case VT100_Text: + if (c == 0x1B) + { + addText(text, false); + text.clear(); + m_terminalState = VT100_Escape; + } + + else if (c == "\n") + { + addText(text + "\n", false); + text.clear(); + } + + else + text.append(c); + + break; + case VT100_Escape: + command.clear(); + if (c == "[") + m_terminalState = VT100_Command; + else if (c == "(") + m_terminalState = VT100_ResetFont; + break; + case VT100_Command: + // Go to escape sequence + if (c == 0x1B) + { + m_terminalState = VT100_Escape; + break; + } + + // Escape from command mode + hasNumbers = (c >= '0' && c <= '9'); + hasCommand = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); + if (!hasNumbers && !hasCommand) + { + m_terminalState = VT100_Text; + break; + } + + // Construct command + command.append(c); + + // Clear screen + if (command == "2J") + { + textEdit()->clear(); + m_terminalState = VT100_Text; + } + + // Move cursor to upper left corner (ugly implementation) + else if (command == "H") + { + textEdit()->clear(); + m_terminalState = VT100_Text; + } + + // Clear line + else if (command == "2K") + { + textEdit()->setFocus(); + auto storedCursor = textEdit()->textCursor(); + textEdit()->moveCursor(QTextCursor::End, QTextCursor::MoveAnchor); + textEdit()->moveCursor(QTextCursor::StartOfLine, + QTextCursor::MoveAnchor); + textEdit()->moveCursor(QTextCursor::End, QTextCursor::KeepAnchor); + textEdit()->textCursor().removeSelectedText(); + textEdit()->textCursor().deletePreviousChar(); + textEdit()->setTextCursor(storedCursor); + m_terminalState = VT100_Text; + } + + // Escape to normal text if command length >= 3 chars + else if (command.length() >= 3) + m_terminalState = VT100_Text; + + break; + case VT100_ResetFont: + m_terminalState = VT100_Text; + break; + } + } + + // Return VT-100 processed text + return text; +} diff --git a/src/UI/QmlPlainTextEdit.h b/src/UI/QmlPlainTextEdit.h index ddf58140..d49b9a9f 100644 --- a/src/UI/QmlPlainTextEdit.h +++ b/src/UI/QmlPlainTextEdit.h @@ -92,6 +92,10 @@ class QmlPlainTextEdit : public QQuickPaintedItem READ scrollbarWidth WRITE setScrollbarWidth NOTIFY scrollbarWidthChanged) + Q_PROPERTY(bool vt100emulation + READ vt100emulation + WRITE setVt100Emulation + NOTIFY vt100EmulationChanged) // clang-format on signals: @@ -106,11 +110,21 @@ signals: void widgetEnabledChanged(); void scrollbarWidthChanged(); void centerOnScrollChanged(); + void vt100EmulationChanged(); void placeholderTextChanged(); void undoRedoEnabledChanged(); void maximumBlockCountChanged(); public: + enum VT100_State + { + VT100_Text, + VT100_Escape, + VT100_Command, + VT100_ResetFont + }; + Q_ENUM(VT100_State) + QmlPlainTextEdit(QQuickItem *parent = 0); ~QmlPlainTextEdit(); @@ -131,6 +145,7 @@ public: bool copyAvailable() const; bool widgetEnabled() const; bool centerOnScroll() const; + bool vt100emulation() const; bool undoRedoEnabled() const; int maximumBlockCount() const; QString placeholderText() const; @@ -155,6 +170,7 @@ public slots: void setPalette(const QPalette &palette); void setWidgetEnabled(const bool enabled); void setCenterOnScroll(const bool enabled); + void setVt100Emulation(const bool enabled); void setUndoRedoEnabled(const bool enabled); void setPlaceholderText(const QString &text); void scrollToBottom(const bool repaint = false); @@ -164,16 +180,22 @@ private slots: void updateWidgetSize(); void updateScrollbarVisibility(); void setCopyAvailable(const bool yes); + void addText(const QString &text, const bool enableVt100); protected: void processMouseEvents(QMouseEvent *event); void processWheelEvents(QWheelEvent *event); +private: + QString vt100Processing(const QString &data); + private: QColor m_color; bool m_autoscroll; + bool m_emulateVt100; bool m_copyAvailable; QPlainTextEdit *m_textEdit; + VT100_State m_terminalState; }; }