diff --git a/meshroom/ui/qml/Controls/TextFileViewer.qml b/meshroom/ui/qml/Controls/TextFileViewer.qml new file mode 100644 index 00000000..19f07c6d --- /dev/null +++ b/meshroom/ui/qml/Controls/TextFileViewer.qml @@ -0,0 +1,331 @@ +import QtQuick 2.11 +import QtQml.Models 2.11 +import QtQuick.Controls 2.5 +import QtQuick.Layouts 1.11 +import MaterialIcons 2.2 + +import Utils 1.0 + +/** + * Text file viewer with auto-reload feature. + * Uses a ListView with one delegate by line instead of a TextArea for performance reasons. + */ +Item { + id: root + + /// Source text file to load + property url source + /// Whether to periodically reload the source file + property bool autoReload: false + /// Interval (in ms) at which source file should be reloaded if autoReload is enabled + property int autoReloadInterval: 2000 + /// Whether the source is currently being loaded + property bool loading: false + + onSourceChanged: loadSource() + onVisibleChanged: if(visible) loadSource() + + RowLayout { + anchors.fill: parent + spacing: 0 + + // Toolbar + Pane { + Layout.alignment: Qt.AlignTop + Layout.fillHeight: true + padding: 0 + background: Rectangle { color: Qt.darker(Colors.sysPalette.window, 1.2) } + Column { + height: parent.height + ToolButton { + text: MaterialIcons.refresh + ToolTip.text: "Reload" + ToolTip.visible: hovered + font.family: MaterialIcons.fontFamily + onClicked: loadSource(false) + } + ToolButton { + text: MaterialIcons.vertical_align_top + ToolTip.text: "Scroll to Top" + ToolTip.visible: hovered + font.family: MaterialIcons.fontFamily + onClicked: textView.positionViewAtBeginning() + } + ToolButton { + text: MaterialIcons.vertical_align_bottom + ToolTip.text: "Scroll to Bottom" + ToolTip.visible: hovered + font.family: MaterialIcons.fontFamily + onClicked: textView.positionViewAtEnd() + checkable: false + checked: textView.atYEnd + } + ToolButton { + text: MaterialIcons.assignment + ToolTip.text: "Copy" + ToolTip.visible: hovered + font.family: MaterialIcons.fontFamily + onClicked: copySubMenu.open() + Menu { + id: copySubMenu + x: parent.width + + MenuItem { + text: "Copy Visible Text" + onTriggered: { + var t = ""; + for(var i = textView.firstVisibleIndex(); i < textView.lastVisibleIndex(); ++i) + t += textView.model[i] + "\n"; + Clipboard.setText(t); + } + } + MenuItem { + text: "Copy All" + onTriggered: { + Clipboard.setText(textView.text); + } + } + } + } + ToolButton { + text: MaterialIcons.open_in_new + ToolTip.text: "Open Externally" + ToolTip.visible: hovered + font.family: MaterialIcons.fontFamily + enabled: root.source !== "" + onClicked: Qt.openUrlExternally(root.source) + } + } + } + + MouseArea { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.margins: 4 + + ListView { + id: textView + + property string text + + // model consists in text split by line + model: textView.text.split("\n") + visible: text != "" + + anchors.fill: parent + clip: true + focus: true + + // custom key navigation handling + keyNavigationEnabled: false + highlightFollowsCurrentItem: true + highlightMoveDuration: 0 + Keys.onPressed: { + switch(event.key) + { + case Qt.Key_Home: + textView.positionViewAtBeginning(); + break; + case Qt.Key_End: + textView.positionViewAtEnd(); + break; + case Qt.Key_Up: + currentIndex = firstVisibleIndex(); + decrementCurrentIndex(); + break; + case Qt.Key_Down: + currentIndex = lastVisibleIndex(); + incrementCurrentIndex(); + break; + case Qt.Key_PageUp: + textView.positionViewAtIndex(firstVisibleIndex(), ListView.End); + break; + case Qt.Key_PageDown: + textView.positionViewAtIndex(lastVisibleIndex(), ListView.Beginning); + break; + } + } + + function setText(value, keepPosition) { + // store cursor position and content position + var topIndex = firstVisibleIndex(); + var scrollToBottom = atYEnd; + // replace text + text = value; + + if(scrollToBottom) + positionViewAtEnd(); + else if(topIndex !== firstVisibleIndex() && keepPosition) + positionViewAtIndex(topIndex, ListView.Beginning); + } + + function firstVisibleIndex() { + return indexAt(contentX, contentY); + } + + function lastVisibleIndex() { + return indexAt(contentX, contentY + height - 2); + } + + ScrollBar.vertical: ScrollBar { + id: vScrollBar + minimumSize: 0.05 + } + + ScrollBar.horizontal: ScrollBar { minimumSize: 0.1 } + + // TextMetrics for line numbers column + TextMetrics { + id: lineMetrics + font.family: "Monospace, Consolas, Monaco" + text: textView.count * 10 + } + + // TextMetrics for textual progress bar + TextMetrics { + id: progressMetrics + // total number of character in textual progress bar + property int count: 51 + property string character: '*' + text: character.repeat(count) + } + + delegate: RowLayout { + width: textView.width + spacing: 6 + + // Line number + Label { + text: index + 1 + Layout.minimumWidth: lineMetrics.width + rightPadding: 2 + enabled: false + Layout.fillHeight: true + horizontalAlignment: Text.AlignRight + } + + Loader { + id: delegateLoader + Layout.fillWidth: true + // default line delegate + sourceComponent: line_component + + // line delegate selector based on content + StateGroup { + states: [ + State { + name: "progressBar" + // detect textual progressbar (non empty line with only progressbar character) + when: modelData.trim().length + && modelData.split(progressMetrics.character).length - 1 === modelData.trim().length + PropertyChanges { + target: delegateLoader + sourceComponent: progressBar_component + } + } + ] + } + + // ProgressBar delegate + Component { + id: progressBar_component + Item { + Layout.fillWidth: true + implicitHeight: progressMetrics.height + ProgressBar { + width: progressMetrics.width + height: parent.height - 2 + anchors.verticalCenter: parent.verticalCenter + from: 0 + to: progressMetrics.count + value: modelData.length + } + } + } + + // Default line delegate + Component { + id: line_component + TextInput { + wrapMode: Text.WrapAnywhere + text: modelData + font.family: "Monospace, Consolas, Monaco" + padding: 0 + selectByMouse: true + readOnly: true + selectionColor: Colors.sysPalette.highlight + persistentSelection: false + Keys.forwardTo: [textView] + + color: { + // color line according to log level + if(text.indexOf("[warning]") >= 0) + return Colors.orange; + else if(text.indexOf("[error]") >= 0) + return Colors.red; + return palette.text; + } + } + } + } + } + } + + RowLayout { + anchors.fill: parent + anchors.rightMargin: vScrollBar.width + z: -1 + + Item { + Layout.preferredWidth: lineMetrics.width + Layout.fillHeight: true + } + + // IBeamCursor shape overlay + MouseArea { + Layout.fillWidth: true + Layout.fillHeight: true + cursorShape: Qt.IBeamCursor + } + } + + // File loading indicator + BusyIndicator { + Component.onCompleted: running = Qt.binding(function() { return root.loading }) + padding: 0 + anchors.right: parent.right + anchors.bottom: parent.bottom + implicitWidth: 16 + implicitHeight: 16 + } + } + } + + // Auto-reload current file timer + Timer { + running: root.autoReload + interval: root.autoReloadInterval + repeat: true + // reload file on start and stop + onRunningChanged: loadSource(true) + onTriggered: loadSource(true) + } + + // Load current source file and update ListView's model + function loadSource(keepPosition = false) + { + if(!visible) + return; + loading = true; + var xhr = new XMLHttpRequest; + xhr.open("GET", root.source); + xhr.onload = function() { + // - can't rely on 'Last-Modified' header response to verify + // that file has changed on disk (not always up-to-date) + // - instead, let QML engine evaluate whether 'text' property value has changed + textView.setText(xhr.status === 200 ? xhr.responseText : "", keepPosition); + loading = false; + }; + xhr.send(); + } +} diff --git a/meshroom/ui/qml/Controls/qmldir b/meshroom/ui/qml/Controls/qmldir index 2efd461a..295947a2 100644 --- a/meshroom/ui/qml/Controls/qmldir +++ b/meshroom/ui/qml/Controls/qmldir @@ -5,3 +5,4 @@ Group 1.0 Group.qml MessageDialog 1.0 MessageDialog.qml Panel 1.0 Panel.qml SearchBar 1.0 SearchBar.qml +TextFileViewer 1.0 TextFileViewer.qml diff --git a/meshroom/ui/qml/GraphEditor/NodeEditor.qml b/meshroom/ui/qml/GraphEditor/NodeEditor.qml index 5cb63f86..3ce454ff 100644 --- a/meshroom/ui/qml/GraphEditor/NodeEditor.qml +++ b/meshroom/ui/qml/GraphEditor/NodeEditor.qml @@ -96,7 +96,7 @@ Panel { } Label { color: Qt.lighter(palette.mid, 1.2) - text: "Select a Node to edit its Attributes" + text: "Select a Node to access its Details" } } } @@ -128,28 +128,30 @@ Panel { node: root.node } } - TabBar { - id: tabBar - - Layout.fillWidth: true - width: childrenRect.width - position: TabBar.Footer - TabButton { - text: "Attributes" - width: implicitWidth - padding: 4 - leftPadding: 8 - rightPadding: leftPadding - } - TabButton { - text: "Log" - width: implicitWidth - leftPadding: 8 - rightPadding: leftPadding - } - } } } } + + TabBar { + id: tabBar + + Layout.fillWidth: true + width: childrenRect.width + position: TabBar.Footer + currentIndex: 1 + TabButton { + text: "Attributes" + width: implicitWidth + padding: 4 + leftPadding: 8 + rightPadding: leftPadding + } + TabButton { + text: "Log" + width: implicitWidth + leftPadding: 8 + rightPadding: leftPadding + } + } } } diff --git a/meshroom/ui/qml/GraphEditor/NodeLog.qml b/meshroom/ui/qml/GraphEditor/NodeLog.qml index fb2fb84e..455b2b0c 100644 --- a/meshroom/ui/qml/GraphEditor/NodeLog.qml +++ b/meshroom/ui/qml/GraphEditor/NodeLog.qml @@ -1,9 +1,9 @@ -import QtQuick 2.9 +import QtQuick 2.12 import QtQuick.Controls 2.3 import QtQuick.Controls 1.4 as Controls1 // SplitView import QtQuick.Layouts 1.3 import MaterialIcons 2.2 -import Utils 1.0 +import Controls 1.0 import "common.js" as Common @@ -85,17 +85,13 @@ FocusScope { id: fileSelector Layout.fillWidth: true property string currentFile: chunksLV.currentChunk ? chunksLV.currentChunk[currentItem.fileProperty] : "" - property string lastLoadedFile - property date lastModTime - onCurrentFileChanged: if(visible) loadCurrentFile() - onVisibleChanged: if(visible) loadCurrentFile() - TabButton { property string fileProperty: "logFile" - text: "Log" + text: "Output" padding: 4 } + TabButton { property string fileProperty: "statisticsFile" text: "Statistics" @@ -108,152 +104,12 @@ FocusScope { } } - RowLayout { - Layout.fillHeight: true + TextFileViewer { Layout.fillWidth: true - spacing: 0 - Pane { - id: tb - Layout.alignment: Qt.AlignTop - Layout.fillHeight: true - padding: 0 - background: Rectangle { color: Qt.darker(activePalette.window, 1.2) } - Column { - height: parent.height - ToolButton { - text: MaterialIcons.refresh - ToolTip.text: "Refresh" - ToolTip.visible: hovered - font.family: MaterialIcons.fontFamily - onClicked: loadCurrentFile(false) - } - ToolButton { - id: autoRefresh - text: MaterialIcons.timer - ToolTip.text: "Auto-Refresh when Running" - ToolTip.visible: hovered - font.family: MaterialIcons.fontFamily - checked: true - checkable: true - } - ToolButton { - text: MaterialIcons.vertical_align_top - ToolTip.text: "Scroll to Top" - ToolTip.visible: hovered - font.family: MaterialIcons.fontFamily - onClicked: logArea.cursorPosition = 0 - } - ToolButton { - text: MaterialIcons.vertical_align_bottom - ToolTip.text: "Scroll to Bottom" - ToolTip.visible: hovered - font.family: MaterialIcons.fontFamily - onClicked: logArea.cursorPosition = logArea.length - } - ToolButton { - id: autoScroll - text: MaterialIcons.system_update_alt - ToolTip.text: "Auto-Scroll to Bottom" - ToolTip.visible: hovered - font.family: MaterialIcons.fontFamily - checkable: true - checked: true - } - ToolButton { - text: MaterialIcons.open_in_new - ToolTip.text: "Open Externally" - ToolTip.visible: hovered - font.family: MaterialIcons.fontFamily - enabled: fileSelector.currentFile != "" - onClicked: Qt.openUrlExternally(Filepath.stringToUrl(fileSelector.currentFile)) - } - } - } - // Log display - ScrollView { - id: logScrollView - Layout.fillWidth: true - Layout.fillHeight: true - TextArea { - id: logArea - selectByMouse: true - selectByKeyboard: true - persistentSelection: true - font.family: "Monospace, Consolas, Monaco" - } - } + Layout.fillHeight: true + autoReload: chunksLV.currentChunk !== undefined && chunksLV.currentChunk.statusName === "RUNNING" + source: Filepath.stringToUrl(fileSelector.currentFile) } } } - - // Auto-reload current file if NodeChunk is being computed - Timer { - running: autoRefresh.checked && chunksLV.currentChunk != undefined && chunksLV.currentChunk.statusName === "RUNNING" - interval: 2000 - repeat: true - // reload file on start and stop - onRunningChanged: loadCurrentFile(true) - onTriggered: loadCurrentFile(true) - } - - function loadCurrentFile(keepCursorPosition) - { - if(keepCursorPosition == undefined) - keepCursorPosition = false - var xhr = new XMLHttpRequest; - xhr.open("GET", Filepath.stringToUrl(fileSelector.currentFile)); - xhr.onreadystatechange = function() { - if(xhr.readyState == XMLHttpRequest.HEADERS_RECEIVED) - { - // if the file is already open - // check last modification date - var lastMod = new Date(xhr.getResponseHeader("Last-Modified")); - if(fileSelector.lastLoadedFile == fileSelector.currentFile - && lastMod.getTime() == fileSelector.lastModTime.getTime() ) - { - // file has not changed, don't reload it - xhr.doLoad = false; - return - } - // file is different or last modification time has changed - fileSelector.lastModTime = lastMod - xhr.doLoad = true - } - if (xhr.readyState == XMLHttpRequest.DONE) { - // store lastLoadedFile url - fileSelector.lastLoadedFile = fileSelector.currentFile - // if responseText should not be loaded - if(!xhr.doLoad) - { - // file could not be opened, reset text and lastModTime - if(xhr.status == 0) - { - fileSelector.lastModTime = new Date() - logArea.text = '' - } - return; - } - // store cursor position and content position - var cursorPosition = logArea.cursorPosition; - var contentY = logScrollView.ScrollBar.vertical.position; - - // replace text - logArea.text = xhr.responseText; - - if(autoScroll.checked) - { - // Reset cursor position to trigger scroll to bottom - logArea.cursorPosition = 0; - logArea.cursorPosition = logArea.length; - } - else if(keepCursorPosition) - { - if(cursorPosition) - logArea.cursorPosition = cursorPosition; - logScrollView.ScrollBar.vertical.position = contentY - } - } - }; - xhr.send(); - } }