From 13d0763e30baa087fd20ee48684b5ee49382c08d Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Sat, 14 Sep 2019 23:07:41 +0200 Subject: [PATCH] [ui] GraphEditor: single tab group + status table - Use a single tab group for attributes, log, statistics, status - Use a ListView with key/value to display the node status fields (instead of a file viewer) --- .../ui/qml/GraphEditor/ChunksListView.qml | 66 +++++++ meshroom/ui/qml/GraphEditor/NodeEditor.qml | 39 +++- meshroom/ui/qml/GraphEditor/NodeLog.qml | 132 +++----------- .../ui/qml/GraphEditor/NodeStatistics.qml | 65 +++++++ meshroom/ui/qml/GraphEditor/NodeStatus.qml | 170 ++++++++++++++++++ 5 files changed, 363 insertions(+), 109 deletions(-) create mode 100644 meshroom/ui/qml/GraphEditor/ChunksListView.qml create mode 100644 meshroom/ui/qml/GraphEditor/NodeStatistics.qml create mode 100644 meshroom/ui/qml/GraphEditor/NodeStatus.qml diff --git a/meshroom/ui/qml/GraphEditor/ChunksListView.qml b/meshroom/ui/qml/GraphEditor/ChunksListView.qml new file mode 100644 index 0000000000..f0479726b2 --- /dev/null +++ b/meshroom/ui/qml/GraphEditor/ChunksListView.qml @@ -0,0 +1,66 @@ +import QtQuick 2.11 +import QtQuick.Controls 2.3 +import QtQuick.Controls 1.4 as Controls1 // SplitView +import QtQuick.Layouts 1.3 +import MaterialIcons 2.2 +import Controls 1.0 + +import "common.js" as Common + +/** + * ChunkListView + */ +ListView { + id: chunksLV + + // model: node.chunks + + property variant currentChunk: currentItem ? currentItem.chunk : undefined + + width: 60 + Layout.fillHeight: true + highlightFollowsCurrentItem: true + keyNavigationEnabled: true + focus: true + currentIndex: 0 + + signal changeCurrentChunk(int chunkIndex) + + header: Component { + Label { + width: chunksLV.width + elide: Label.ElideRight + text: "Chunks" + padding: 4 + z: 10 + background: Rectangle { color: parent.palette.window } + } + } + + highlight: Component { + Rectangle { + color: activePalette.highlight + opacity: 0.3 + z: 2 + } + } + highlightMoveDuration: 0 + highlightResizeDuration: 0 + + delegate: ItemDelegate { + id: chunkDelegate + property var chunk: object + text: index + width: parent.width + leftPadding: 8 + onClicked: { + chunksLV.forceActiveFocus() + chunksLV.changeCurrentChunk(index) + } + Rectangle { + width: 4 + height: parent.height + color: Common.getChunkColor(parent.chunk) + } + } +} diff --git a/meshroom/ui/qml/GraphEditor/NodeEditor.qml b/meshroom/ui/qml/GraphEditor/NodeEditor.qml index b625302415..b0d1041d24 100644 --- a/meshroom/ui/qml/GraphEditor/NodeEditor.qml +++ b/meshroom/ui/qml/GraphEditor/NodeEditor.qml @@ -19,6 +19,15 @@ Panel { signal attributeDoubleClicked(var mouse, var attribute) signal upgradeRequest() + Item { + id: m + property int chunkCurrentIndex: 0 + } + + onNodeChanged: { + m.chunkCurrentIndex = 0 // Needed to avoid invalid state of ChunksListView + } + title: "Node" + (node !== null ? " - " + node.label + "" : "") icon: MaterialLabel { text: MaterialIcons.tune } @@ -113,7 +122,6 @@ Panel { currentIndex: tabBar.currentIndex AttributeEditor { - Layout.fillWidth: true attributes: root.node.attributes readOnly: root.readOnly || root.isCompatibilityNode onAttributeDoubleClicked: root.attributeDoubleClicked(mouse, attribute) @@ -122,10 +130,23 @@ Panel { NodeLog { id: nodeLog + node: root.node + chunkCurrentIndex: m.chunkCurrentIndex + onChangeCurrentChunk: { m.chunkCurrentIndex = chunkIndex } + } - Layout.fillHeight: true - Layout.fillWidth: true + NodeStatistics { + id: nodeStatistics node: root.node + chunkCurrentIndex: m.chunkCurrentIndex + onChangeCurrentChunk: { m.chunkCurrentIndex = chunkIndex } + } + + NodeStatus { + id: nodeStatus + node: root.node + chunkCurrentIndex: m.chunkCurrentIndex + onChangeCurrentChunk: { m.chunkCurrentIndex = chunkIndex } } } } @@ -152,6 +173,18 @@ Panel { leftPadding: 8 rightPadding: leftPadding } + TabButton { + text: "Statistics" + width: implicitWidth + leftPadding: 8 + rightPadding: leftPadding + } + TabButton { + text: "Status" + width: implicitWidth + leftPadding: 8 + rightPadding: leftPadding + } } } } diff --git a/meshroom/ui/qml/GraphEditor/NodeLog.qml b/meshroom/ui/qml/GraphEditor/NodeLog.qml index 3d5923e66f..bea42622aa 100644 --- a/meshroom/ui/qml/GraphEditor/NodeLog.qml +++ b/meshroom/ui/qml/GraphEditor/NodeLog.qml @@ -14,7 +14,10 @@ import "common.js" as Common * if the related NodeChunk is being computed. */ FocusScope { + id: root property variant node + property alias chunkCurrentIndex: chunksLV.currentIndex + signal changeCurrentChunk(int chunkIndex) SystemPalette { id: activePalette } @@ -22,126 +25,43 @@ FocusScope { anchors.fill: parent // The list of chunks - ListView { + ChunksListView { id: chunksLV - - property variant currentChunk: currentItem ? currentItem.chunk : undefined - - width: 60 Layout.fillHeight: true model: node.chunks - highlightFollowsCurrentItem: true - keyNavigationEnabled: true - focus: true - currentIndex: 0 - - header: Component { - Label { - width: chunksLV.width - elide: Label.ElideRight - text: "Chunks" - padding: 4 - z: 10 - background: Rectangle { color: parent.palette.window } - } - } - - highlight: Component { - Rectangle { - color: activePalette.highlight - opacity: 0.3 - z: 2 - } - } - highlightMoveDuration: 0 - highlightResizeDuration: 0 - - delegate: ItemDelegate { - id: chunkDelegate - property var chunk: object - text: index - width: parent.width - leftPadding: 8 - onClicked: { - chunksLV.forceActiveFocus() - chunksLV.currentIndex = index - } - Rectangle { - width: 4 - height: parent.height - color: Common.getChunkColor(parent.chunk) - } - } + onChangeCurrentChunk: root.changeCurrentChunk(chunkIndex) } - ColumnLayout { + Loader { + id: componentLoader + clip: true Layout.fillWidth: true Layout.fillHeight: true - Layout.margins: 1 - - spacing: 1 + property url source - TabBar { - id: fileSelector - Layout.fillWidth: true - property string currentFile: chunksLV.currentChunk ? chunksLV.currentChunk[currentItem.fileProperty] : "" - onCurrentFileChanged: { - // only set text file viewer source when ListView is fully ready - // (either empty or fully populated with a valid currentChunk) - // to avoid going through an empty url when switching between two nodes - - if(!chunksLV.count || chunksLV.currentChunk) - logComponentLoader.source = Filepath.stringToUrl(currentFile); - - } + property string currentFile: chunksLV.currentChunk ? chunksLV.currentChunk["logFile"] : "" + onCurrentFileChanged: { + // only set text file viewer source when ListView is fully ready + // (either empty or fully populated with a valid currentChunk) + // to avoid going through an empty url when switching between two nodes - TabButton { - property string fileProperty: "logFile" - text: "Output" - padding: 4 - } + if(!chunksLV.count || chunksLV.currentChunk) + componentLoader.source = Filepath.stringToUrl(currentFile); - TabButton { - property string fileProperty: "statisticsFile" - text: "Statistics" - padding: 4 - } - TabButton { - property string fileProperty: "statusFile" - text: "Status" - padding: 4 - } } - Loader { - id: logComponentLoader - clip: true + sourceComponent: textFileViewerComponent + } + + Component { + id: textFileViewerComponent + TextFileViewer { + id: textFileViewer + source: componentLoader.source Layout.fillWidth: true Layout.fillHeight: true - property url source - sourceComponent: fileSelector.currentItem.fileProperty === "statisticsFile" ? statViewerComponent : textFileViewerComponent - } - - Component { - id: textFileViewerComponent - TextFileViewer { - id: textFileViewer - source: logComponentLoader.source - Layout.fillWidth: true - Layout.fillHeight: true - autoReload: chunksLV.currentChunk !== undefined && chunksLV.currentChunk.statusName === "RUNNING" - // source is set in fileSelector - } - } - - Component { - id: statViewerComponent - StatViewer { - id: statViewer - Layout.fillWidth: true - Layout.fillHeight: true - source: logComponentLoader.source - } + autoReload: chunksLV.currentChunk !== undefined && chunksLV.currentChunk.statusName === "RUNNING" + // source is set in fileSelector } } } diff --git a/meshroom/ui/qml/GraphEditor/NodeStatistics.qml b/meshroom/ui/qml/GraphEditor/NodeStatistics.qml new file mode 100644 index 0000000000..5e4ec3e80a --- /dev/null +++ b/meshroom/ui/qml/GraphEditor/NodeStatistics.qml @@ -0,0 +1,65 @@ +import QtQuick 2.11 +import QtQuick.Controls 2.3 +import QtQuick.Controls 1.4 as Controls1 // SplitView +import QtQuick.Layouts 1.3 +import MaterialIcons 2.2 +import Controls 1.0 + +import "common.js" as Common + +/** + * NodeLog displays log and statistics data of Node's chunks (NodeChunks) + * + * To ease monitoring, it provides periodic auto-reload of the opened file + * if the related NodeChunk is being computed. + */ +FocusScope { + id: root + property variant node + property alias chunkCurrentIndex: chunksLV.currentIndex + signal changeCurrentChunk(int chunkIndex) + + SystemPalette { id: activePalette } + + Controls1.SplitView { + anchors.fill: parent + + // The list of chunks + ChunksListView { + id: chunksLV + Layout.fillHeight: true + model: node.chunks + onChangeCurrentChunk: root.changeCurrentChunk(chunkIndex) + } + + Loader { + id: componentLoader + clip: true + Layout.fillWidth: true + Layout.fillHeight: true + property url source + + property string currentFile: chunksLV.currentChunk ? chunksLV.currentChunk["statisticsFile"] : "" + onCurrentFileChanged: { + // only set text file viewer source when ListView is fully ready + // (either empty or fully populated with a valid currentChunk) + // to avoid going through an empty url when switching between two nodes + + if(!chunksLV.count || chunksLV.currentChunk) + componentLoader.source = Filepath.stringToUrl(currentFile); + } + + sourceComponent: statViewerComponent + } + + Component { + id: statViewerComponent + StatViewer { + id: statViewer + Layout.fillWidth: true + Layout.fillHeight: true + source: componentLoader.source + } + } + } +} diff --git a/meshroom/ui/qml/GraphEditor/NodeStatus.qml b/meshroom/ui/qml/GraphEditor/NodeStatus.qml new file mode 100644 index 0000000000..62f87beaaf --- /dev/null +++ b/meshroom/ui/qml/GraphEditor/NodeStatus.qml @@ -0,0 +1,170 @@ +import QtQuick 2.11 +import QtQuick.Controls 2.3 +import QtQuick.Controls 1.4 as Controls1 // SplitView +import QtQuick.Layouts 1.3 +import MaterialIcons 2.2 +import Controls 1.0 + +import "common.js" as Common + +/** + * NodeLog displays log and statistics data of Node's chunks (NodeChunks) + * + * To ease monitoring, it provides periodic auto-reload of the opened file + * if the related NodeChunk is being computed. + */ +FocusScope { + id: root + property variant node + property alias chunkCurrentIndex: chunksLV.currentIndex + signal changeCurrentChunk(int chunkIndex) + + SystemPalette { id: activePalette } + + Controls1.SplitView { + anchors.fill: parent + + // The list of chunks + ChunksListView { + id: chunksLV + Layout.fillHeight: true + model: node.chunks + onChangeCurrentChunk: root.changeCurrentChunk(chunkIndex) + } + + Loader { + id: componentLoader + clip: true + Layout.fillWidth: true + Layout.fillHeight: true + property url source + + property string currentFile: chunksLV.currentChunk ? chunksLV.currentChunk["statusFile"] : "" + onCurrentFileChanged: { + // only set text file viewer source when ListView is fully ready + // (either empty or fully populated with a valid currentChunk) + // to avoid going through an empty url when switching between two nodes + + if(!chunksLV.count || chunksLV.currentChunk) + componentLoader.source = Filepath.stringToUrl(currentFile); + } + + sourceComponent: statViewerComponent + } + + Component { + id: statViewerComponent + Item { + + id: statusViewer + Layout.fillWidth: true + Layout.fillHeight: true + property url source: componentLoader.source + property var lastModified: undefined + + onSourceChanged: { + statusListModel.readSourceFile() + } + + ListModel { + id: statusListModel + + function readSourceFile() { + // make sure we are trying to load a statistics file + if(!Filepath.urlToString(source).endsWith("status")) + return; + + var xhr = new XMLHttpRequest; + xhr.open("GET", source); + + xhr.onreadystatechange = function() { + if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { + // console.warn("StatusListModel: read valid file") + if(lastModified === undefined || lastModified !== xhr.getResponseHeader('Last-Modified')) { + lastModified = xhr.getResponseHeader('Last-Modified') + try { + var jsonObject = JSON.parse(xhr.responseText); + + var entries = []; + // prepare data to populate the ListModel from the input json object + for(var key in jsonObject) + { + var entry = {}; + entry["key"] = key; + entry["value"] = String(jsonObject[key]); + entries.push(entry); + } + // reset the model with prepared data (limit to one update event) + statusListModel.clear(); + statusListModel.append(entries); + } + catch(exc) + { + // console.warn("StatusListModel: failed to read file") + lastModified = undefined; + statusListModel.clear(); + } + } + } + else + { + // console.warn("StatusListModel: invalid file") + lastModified = undefined; + statusListModel.clear(); + } + }; + xhr.send(); + } + } + + ListView { + id: statusListView + anchors.fill: parent + // spacing: 3 + model: statusListModel + + delegate: Rectangle { + color: activePalette.window + width: childrenRect.width + height: childrenRect.height + RowLayout { + Label { + text: key + padding: 4 + leftPadding: 6 + Layout.preferredWidth: sizeHandle.x + elide: Text.ElideRight + background: Rectangle { color: Qt.darker(activePalette.window, 1.1) } + } + TextArea { + text: value + Layout.fillWidth: true + wrapMode: Label.WrapAtWordBoundaryOrAnywhere + } + } + } + } + + // Categories resize handle + Rectangle { + id: sizeHandle + height: parent.contentHeight + width: 1 + x: parent.width * 0.2 + MouseArea { + anchors.fill: parent + anchors.margins: -4 + cursorShape: Qt.SizeHorCursor + drag { + target: parent + axis: Drag.XAxis + threshold: 0 + minimumX: statusListView.width * 0.2 + maximumX: statusListView.width * 0.8 + } + } + } + } + } + } +}