From 372f36a0ee46e241120972a131b53495402450ed Mon Sep 17 00:00:00 2001 From: Alex Spataru Date: Fri, 5 Feb 2021 23:09:46 -0500 Subject: [PATCH] Add line counter in console & fix several issues --- assets/assets.qrc | 1 + assets/qml/Widgets/LogView.qml | 192 +++++++++++++++++++++++++++++++++ assets/qml/Windows/Console.qml | 160 +++++++++++++-------------- assets/qml/main.qml | 2 +- src/IO/Console.cpp | 41 +++++-- src/IO/Console.h | 12 ++- 6 files changed, 312 insertions(+), 96 deletions(-) create mode 100644 assets/qml/Widgets/LogView.qml diff --git a/assets/assets.qrc b/assets/assets.qrc index 08a002be..47533395 100644 --- a/assets/assets.qrc +++ b/assets/assets.qrc @@ -62,6 +62,7 @@ qml/Widgets/GroupDelegate.qml qml/Widgets/GyroDelegate.qml qml/Widgets/LED.qml + qml/Widgets/LogView.qml qml/Widgets/MapDelegate.qml qml/Widgets/Window.qml qml/Windows/About.qml diff --git a/assets/qml/Widgets/LogView.qml b/assets/qml/Widgets/LogView.qml new file mode 100644 index 00000000..35414c50 --- /dev/null +++ b/assets/qml/Widgets/LogView.qml @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2020-2021 Alex Spataru + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 + +Rectangle { + id: root + clip: true + smooth: false + color: backgroundColor + + // + // Custom properties + // + property int lineOffset: 0 + property string selectedText + property Menu contextMenu: null + property alias font: label.font + property bool autoscroll: false + property string placeholderText: "" + property alias model: listView.model + readonly property int digits: listView.count.toString().length + + // + // Set colors + // + property color textColor: "#72d084" + property color caretLineColor: "#222228" + property color backgroundColor: "#060601" + property color lineCountTextColor: "#545454" + property color lineCountBackgroundColor: "#121212" + + // + // Gets the line number for the given @a index + // + function getLineNumber(index) { + var lIndex = listView.indexAt(0, 0) + if (lIndex < 0) + lIndex = 0 + + return lIndex + index + 1 + root.lineOffset + } + + // + // Placeholder text & font source for rest of widget + // + Text { + id: label + opacity: 0.5 + anchors.top: parent.top + anchors.left: parent.left + text: root.placeholderText + visible: listView.count == 0 + anchors.margins: app.spacing + anchors.leftMargin: lineCountRect.width + } + + // + // Line count rectangle + // + Rectangle { + id: lineCountRect + + anchors.top: parent.top + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.margins: root.border.width + color: root.lineCountBackgroundColor + width: root.font.pixelSize * (root.digits + 1) + } + + // + // Text view + // + ListView { + id: listView + cacheBuffer: 0 + currentIndex: 0 + model: root.model + anchors.fill: parent + anchors.leftMargin: 0 + highlightMoveDuration: 0 + anchors.margins: app.spacing + + // + // Scrollbar + // + ScrollBar.vertical: ScrollBar { + id: scrollbar + } + + // + // Hacks to implement auto-scrolling & position preservation when new data is + // added to the data model + // + property int currentContentY + property int previousCurrentIndex + onMovementEnded: { + currentContentY = contentY + } + + onCountChanged: { + if (root.autoscroll) { + listView.positionViewAtEnd() + listView.currentIndex = listView.count - 2 + currentContentY = contentY + previousCurrentIndex = currentIndex + root.selectedText = root.model[currentIndex] + } + + else { + contentY = currentContentY + currentIndex = previousCurrentIndex + } + } + + // + // Line delegate + // + delegate: Text { + font: root.font + text: modelData + color: root.textColor + height: font.pixelSize + width: listView.width - x + x: app.spacing + lineCountRect.width + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + } + + // + // Highlight item + // + highlight: Rectangle { + z: 0 + color: root.caretLineColor + + Text { + font: root.font + width: lineCountRect.width + color: root.lineCountTextColor + horizontalAlignment: Qt.AlignHCenter + anchors.verticalCenter: parent.verticalCenter + text: listView.currentIndex + root.lineOffset + 1 + } + } + } + + // + // Simple implementation of a mouse cursor + // + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.RightButton + + onClicked: contextMenu.popup() + onMouseYChanged: { + if (containsMouse && (!autoscroll || !Cpp_IO_Manager.connected)) { + var index = listView.indexAt(lineCountRect.width + 2 * app.spacing, + mouseY + listView.contentY) + + if (index >= 0) { + listView.currentIndex = index + listView.previousCurrentIndex = index + root.selectedText = root.model[index] + } + } + } + } +} diff --git a/assets/qml/Windows/Console.qml b/assets/qml/Windows/Console.qml index 64c5c2d1..6c24d630 100644 --- a/assets/qml/Windows/Console.qml +++ b/assets/qml/Windows/Console.qml @@ -31,30 +31,10 @@ import "../Widgets" as Widgets Control { id: root - //Component.onCompleted: Cpp_IO_Console.setTextDocument(textArea.textDocument) background: Rectangle { color: app.windowBackgroundColor } - // - // Enable/disable text rendering when visibility changes - // - onVisibleChanged: Cpp_IO_Console.enableRender = visible - - // - // Console text color - // - property int fontSize: 12 - readonly property color consoleColor: "#8ecd9d" - - // - // Hacks to allow context menu to work - // - property int curPos - property int selectEnd - property int selectStart - property TextEdit textArea: null - // // Function to send through serial port data // @@ -75,6 +55,49 @@ Control { property alias displayMode: displayModeCombo.currentIndex } + // + // Copy shortcut + // + Shortcut { + sequence: StandardKey.Copy + onActivated: Cpp_IO_Console.copy(logView.selectedText) + } + + // + // Copy shortcut + // + Shortcut { + sequence: StandardKey.Save + onActivated: Cpp_IO_Console.save() + } + + // + // Right-click context menu + // + Menu { + id: menu + + MenuItem { + text: qsTr("Copy") + enabled: logView.selectedText.length > 0 + onClicked: Cpp_IO_Console.copy(logView.selectedText) + } + + MenuItem { + text: qsTr("Clear") + opacity: enabled ? 1 : 0.5 + onTriggered: Cpp_IO_Console.clear() + enabled: Cpp_IO_Console.saveAvailable + } + + MenuItem { + opacity: enabled ? 1 : 0.5 + text: qsTr("Save as") + "..." + onTriggered: Cpp_IO_Console.save() + enabled: Cpp_IO_Console.saveAvailable + } + } + // // Controls // @@ -86,69 +109,19 @@ Control { // // Console display // - Rectangle { + Widgets.LogView { + id: logView border.width: 1 - color: "#121218" + contextMenu: menu + font.pixelSize: 12 Layout.fillWidth: true + border.color: caretLineColor Layout.fillHeight: true - border.color: palette.midlight - - Text { - opacity: 0.5 - anchors.top: parent.top - anchors.left: parent.left - anchors.margins: app.spacing - - color: root.consoleColor - font.family: app.monoFont - font.pixelSize: root.fontSize - visible: Cpp_IO_Console.lineCount == 0 - text: qsTr("No data received so far...") - } - - ListView { - id: model - clip: true - anchors.fill: parent - anchors.margins: app.spacing - model: Cpp_IO_Console.lineCount - - ScrollBar.vertical: ScrollBar { - id: scrollbar - } - - property int currentContentY - - onMovementEnded: { - currentContentY = contentY - } - - onCountChanged: { - if (Cpp_IO_Console.autoscroll) - model.positionViewAtEnd() - else - contentY = currentContentY - } - - delegate: Text { - id: line - width: model.width - color: root.consoleColor - font.family: app.monoFont - font.pixelSize: root.fontSize - text: Cpp_IO_Console.getLine(index) - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - - Connections { - target: Cpp_IO_Console - enabled: Cpp_IO_Console.lineCount == (index + 1) - - function onDataReceived() { - line.text = Cpp_IO_Console.getLine(index) - } - } - } - } + font.family: app.monoFont + model: Cpp_IO_Console.dataModel + lineOffset: Cpp_IO_Console.lineOffset + autoscroll: Cpp_IO_Console.autoscroll + placeholderText: qsTr("No data received so far...") } // @@ -160,32 +133,47 @@ Control { TextField { id: send height: 24 - font.pixelSize: 12 + font: logView.font Layout.fillWidth: true - palette.base: "#121218" - color: root.consoleColor - font.family: app.monoFont + color: logView.textColor opacity: enabled ? 1 : 0.5 enabled: Cpp_IO_Manager.readWrite + palette.base: logView.backgroundColor placeholderText: qsTr("Send data to device") + "..." + background: Rectangle { + border.width: 1 + color: logView.backgroundColor + border.color: logView.border.color + } + + // + // Validate hex strings + // validator: RegExpValidator { regExp: hexCheckbox.checked ? /^[a-fA-F0-9]+$/ : /[\s\S]*/ } + // + // Send data on + // Keys.onReturnPressed: root.sendData() + // + // Navigate command history upwards with + // Keys.onUpPressed: { Cpp_IO_Console.historyUp() send.text = Cpp_IO_Console.currentHistoryString } + // + // Navigate command history downwards with + // Keys.onDownPressed: { Cpp_IO_Console.historyDown() send.text = Cpp_IO_Console.currentHistoryString } - - Behavior on opacity {NumberAnimation{}} } CheckBox { diff --git a/assets/qml/main.qml b/assets/qml/main.qml index 3414ec33..ae4617d4 100644 --- a/assets/qml/main.qml +++ b/assets/qml/main.qml @@ -125,7 +125,7 @@ ApplicationWindow { // function showWelcomeGuide() { Cpp_IO_Console.clear() - Cpp_IO_Console.append(Cpp_Misc_Translator.welcomeConsoleText() + "\n") + Cpp_IO_Console.append(Cpp_Misc_Translator.welcomeConsoleText()) } // diff --git a/src/IO/Console.cpp b/src/IO/Console.cpp index fefa0a3b..8bc2fbff 100644 --- a/src/IO/Console.cpp +++ b/src/IO/Console.cpp @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -34,9 +35,9 @@ using namespace IO; static Console *INSTANCE = nullptr; /** - * Set maximum scrollback to 10000 lines + * Set maximum scrollback to 5000 lines */ -static const int SCROLLBACK = 10000; +static const int SCROLLBACK = 5000; /** * Constructor function @@ -46,6 +47,7 @@ Console::Console() , m_lineEnding(LineEnding::NoLineEnding) , m_displayMode(DisplayMode::DisplayPlainText) , m_historyItem(0) + , m_lineOffset(0) , m_echo(false) , m_autoscroll(true) , m_showTimestamp(true) @@ -163,14 +165,20 @@ int Console::lineCount() const } /** - * Returns the string at the given @a line of the log file + * Returns the starting line number of the data model */ -QString Console::getLine(const int line) const +quint32 Console::lineOffset() const { - if (line < lineCount()) - return m_data.at(line); + return m_lineOffset; +} - return ""; +/** + * Returns a pointer to the string list model, which is used by the QML interface to + * display the console data. + */ +QStringList Console::dataModel() const +{ + return m_data; } /** @@ -234,7 +242,7 @@ void Console::save() QByteArray data; for (int i = 0; i < lineCount(); ++i) { - data.append(getLine(i).toUtf8()); + data.append(m_data.at(i).toUtf8()); data.append("\r"); data.append("\n"); } @@ -256,8 +264,11 @@ void Console::save() void Console::clear() { m_data.clear(); + m_lineOffset = 0; m_data.reserve(SCROLLBACK); + emit dataReceived(); + emit lineOffsetChanged(); } /** @@ -292,6 +303,15 @@ void Console::historyDown() } } +/** + * Adds the given data to the system clipboard + */ +void Console::copy(const QString &data) +{ + if (!data.isEmpty()) + qApp->clipboard()->setText(data); +} + /** * Sends the given @a data to the currently connected device using the options specified * by the user with the rest of the functions of this class. @@ -463,7 +483,12 @@ void Console::append(const QString &string, const bool addTimestamp) while (lineCount() > SCROLLBACK) { for (int i = 0; i < SCROLLBACK * 0.1; ++i) + { + ++m_lineOffset; m_data.takeFirst(); + } + + emit lineOffsetChanged(); } // Update UI diff --git a/src/IO/Console.h b/src/IO/Console.h index 77c187ed..bf02d7cd 100644 --- a/src/IO/Console.h +++ b/src/IO/Console.h @@ -65,12 +65,19 @@ class Console : public QObject Q_PROPERTY(int lineCount READ lineCount NOTIFY dataReceived) + Q_PROPERTY(QStringList dataModel + READ dataModel + NOTIFY dataReceived) + Q_PROPERTY(quint32 lineOffset + READ lineOffset + NOTIFY lineOffsetChanged) // clang-format on signals: void echoChanged(); void dataReceived(); void dataModeChanged(); + void lineOffsetChanged(); void autoscrollChanged(); void lineEndingChanged(); void displayModeChanged(); @@ -115,7 +122,8 @@ public: QString currentHistoryString() const; int lineCount() const; - Q_INVOKABLE QString getLine(const int line) const; + quint32 lineOffset() const; + QStringList dataModel() const; Q_INVOKABLE QStringList dataModes() const; Q_INVOKABLE QStringList lineEndings() const; @@ -126,6 +134,7 @@ public slots: void clear(); void historyUp(); void historyDown(); + void copy(const QString &data); void send(const QString &data); void setEcho(const bool enabled); void setDataMode(const DataMode mode); @@ -152,6 +161,7 @@ private: DisplayMode m_displayMode; int m_historyItem; + quint32 m_lineOffset; bool m_echo; bool m_autoscroll;