From 0ef9931bc0ac73a292071e0babb2953ff5f89074 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Fri, 22 Jan 2021 09:40:07 +0100 Subject: [PATCH] [ui] NodeEditor: refactor ChunksList and add global stats --- meshroom/core/node.py | 94 ++++--- meshroom/ui/qml/Controls/KeyValue.qml | 52 ++++ meshroom/ui/qml/Controls/qmldir | 1 + .../ui/qml/GraphEditor/ChunksListView.qml | 112 +++++--- meshroom/ui/qml/GraphEditor/NodeEditor.qml | 81 ++++-- meshroom/ui/qml/GraphEditor/NodeLog.qml | 60 ++-- .../ui/qml/GraphEditor/NodeStatistics.qml | 63 ++--- meshroom/ui/qml/GraphEditor/NodeStatus.qml | 260 ++++++++---------- meshroom/ui/qml/Utils/format.js | 9 + 9 files changed, 420 insertions(+), 312 deletions(-) create mode 100644 meshroom/ui/qml/Controls/KeyValue.qml diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 00646e6c6e..bb67c3fd68 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -58,12 +58,12 @@ class ExecMode(Enum): EXTERN = 2 -class StatusData: +class StatusData(BaseObject): """ """ dateTimeFormatting = '%Y-%m-%d %H:%M:%S.%f' - def __init__(self, nodeName, nodeType, packageName, packageVersion): + def __init__(self, nodeName='', nodeType='', packageName='', packageVersion=''): self.status = Status.NONE self.execMode = ExecMode.NONE self.nodeName = nodeName @@ -79,6 +79,11 @@ def __init__(self, nodeName, nodeType, packageName, packageVersion): self.hostname = "" self.sessionUid = meshroom.core.sessionUid + def merge(self, other): + self.startDateTime = min(self.startDateTime, other.startDateTime) + self.endDateTime = max(self.endDateTime, other.endDateTime) + self.elapsedTime += other.elapsedTime + def reset(self): self.status = Status.NONE self.execMode = ExecMode.NONE @@ -112,8 +117,12 @@ def toDict(self): return d def fromDict(self, d): - self.status = getattr(Status, d.get('status', ''), Status.NONE) - self.execMode = getattr(ExecMode, d.get('execMode', ''), ExecMode.NONE) + self.status = d.get('status', Status.NONE) + if not isinstance(self.status, Status): + self.status = Status[self.status] + self.execMode = d.get('execMode', ExecMode.NONE) + if not isinstance(self.execMode, ExecMode): + self.execMode = ExecMode[self.execMode] self.nodeName = d.get('nodeName', '') self.nodeType = d.get('nodeType', '') self.packageName = d.get('packageName', '') @@ -236,7 +245,7 @@ def __init__(self, node, range, parent=None): self.node = node self.range = range self.logManager = LogManager(self) - self.status = StatusData(node.name, node.nodeType, node.packageName, node.packageVersion) + self._status = StatusData(node.name, node.nodeType, node.packageName, node.packageVersion) self.statistics = stats.Statistics() self.statusFileLastModTime = -1 self._subprocess = None @@ -258,7 +267,7 @@ def name(self): @property def statusName(self): - return self.status.status.name + return self._status.status.name @property def logger(self): @@ -266,24 +275,24 @@ def logger(self): @property def execModeName(self): - return self.status.execMode.name + return self._status.execMode.name def updateStatusFromCache(self): """ Update node status based on status file content/existence. """ statusFile = self.statusFile - oldStatus = self.status.status + oldStatus = self._status.status # No status file => reset status to Status.None if not os.path.exists(statusFile): self.statusFileLastModTime = -1 - self.status.reset() + self._status.reset() else: with open(statusFile, 'r') as jsonFile: statusData = json.load(jsonFile) - self.status.fromDict(statusData) + self._status.fromDict(statusData) self.statusFileLastModTime = os.path.getmtime(statusFile) - if oldStatus != self.status.status: + if oldStatus != self._status.status: self.statusChanged.emit() @property @@ -311,7 +320,7 @@ def saveStatusFile(self): """ Write node status on disk. """ - data = self.status.toDict() + data = self._status.toDict() statusFilepath = self.statusFile folder = os.path.dirname(statusFilepath) if not os.path.exists(folder): @@ -322,16 +331,16 @@ def saveStatusFile(self): renameWritingToFinalPath(statusFilepathWriting, statusFilepath) def upgradeStatusTo(self, newStatus, execMode=None): - if newStatus.value <= self.status.status.value: - print('WARNING: downgrade status on node "{}" from {} to {}'.format(self.name, self.status.status, - newStatus)) + if newStatus.value <= self._status.status.value: + logging.warning('Downgrade status on node "{}" from {} to {}'.format(self.name, self._status.status, + newStatus)) if newStatus == Status.SUBMITTED: - self.status = StatusData(self.node.name, self.node.nodeType, self.node.packageName, self.node.packageVersion) + self._status = StatusData(self.node.name, self.node.nodeType, self.node.packageName, self.node.packageVersion) if execMode is not None: - self.status.execMode = execMode + self._status.execMode = execMode self.execModeNameChanged.emit() - self.status.status = newStatus + self._status.status = newStatus self.saveStatusFile() self.statusChanged.emit() @@ -360,24 +369,24 @@ def saveStatistics(self): renameWritingToFinalPath(statisticsFilepathWriting, statisticsFilepath) def isAlreadySubmitted(self): - return self.status.status in (Status.SUBMITTED, Status.RUNNING) + return self._status.status in (Status.SUBMITTED, Status.RUNNING) def isAlreadySubmittedOrFinished(self): - return self.status.status in (Status.SUBMITTED, Status.RUNNING, Status.SUCCESS) + return self._status.status in (Status.SUBMITTED, Status.RUNNING, Status.SUCCESS) def isFinishedOrRunning(self): - return self.status.status in (Status.SUCCESS, Status.RUNNING) + return self._status.status in (Status.SUCCESS, Status.RUNNING) def isStopped(self): - return self.status.status == Status.STOPPED + return self._status.status == Status.STOPPED def process(self, forceCompute=False): - if not forceCompute and self.status.status == Status.SUCCESS: - print("Node chunk already computed:", self.name) + if not forceCompute and self._status.status == Status.SUCCESS: + logging.info("Node chunk already computed: {}".format(self.name)) return global runningProcesses runningProcesses[self.name] = self - self.status.initStartCompute() + self._status.initStartCompute() startTime = time.time() self.upgradeStatusTo(Status.RUNNING) self.statThread = stats.StatisticsThread(self) @@ -385,16 +394,16 @@ def process(self, forceCompute=False): try: self.node.nodeDesc.processChunk(self) except Exception as e: - if self.status.status != Status.STOPPED: + if self._status.status != Status.STOPPED: self.upgradeStatusTo(Status.ERROR) raise except (KeyboardInterrupt, SystemError, GeneratorExit) as e: self.upgradeStatusTo(Status.STOPPED) raise finally: - self.status.initEndCompute() - self.status.elapsedTime = time.time() - startTime - print(' - elapsed time:', self.status.elapsedTimeStr) + self._status.initEndCompute() + self._status.elapsedTime = time.time() - startTime + logging.info(' - elapsed time: {}'.format(self._status.elapsedTimeStr)) # ask and wait for the stats thread to stop self.statThread.stopRequest() self.statThread.join() @@ -408,9 +417,10 @@ def stopProcess(self): self.node.nodeDesc.stopProcess(self) def isExtern(self): - return self.status.execMode == ExecMode.EXTERN + return self._status.execMode == ExecMode.EXTERN statusChanged = Signal() + status = Property(Variant, lambda self: self._status, notify=statusChanged) statusName = Property(str, statusName.fget, notify=statusChanged) execModeNameChanged = Signal() execModeName = Property(str, execModeName.fget, notify=execModeNameChanged) @@ -422,7 +432,7 @@ def isExtern(self): statisticsFile = Property(str, statisticsFile.fget, notify=nodeFolderChanged) nodeName = Property(str, lambda self: self.node.name, constant=True) - statusNodeName = Property(str, lambda self: self.status.nodeName, constant=True) + statusNodeName = Property(str, lambda self: self._status.nodeName, constant=True) # simple structure for storing node position @@ -837,6 +847,24 @@ def getGlobalStatus(self): return Status.NONE + @Slot(result=StatusData) + def getFusedStatus(self): + fusedStatus = StatusData() + if self._chunks: + fusedStatus.fromDict(self._chunks[0].status.toDict()) + for chunk in self._chunks[1:]: + fusedStatus.merge(chunk.status) + fusedStatus.status = self.getGlobalStatus() + return fusedStatus + + @Slot(result=StatusData) + def getRecursiveFusedStatus(self): + fusedStatus = self.getFusedStatus() + nodes = self.getInputNodes(recursive=True, dependenciesOnly=True) + for node in nodes: + fusedStatus.merge(node.fusedStatus) + return fusedStatus + @property def globalExecMode(self): return self._chunks.at(0).execModeName @@ -1000,6 +1028,10 @@ def canBeCanceled(self): size = Property(int, getSize, notify=sizeChanged) globalStatusChanged = Signal() globalStatus = Property(str, lambda self: self.getGlobalStatus().name, notify=globalStatusChanged) + fusedStatus = Property(StatusData, getFusedStatus, notify=globalStatusChanged) + elapsedTime = Property(float, lambda self: self.getFusedStatus().elapsedTime, notify=globalStatusChanged) + recursiveElapsedTime = Property(float, lambda self: self.getRecursiveFusedStatus().elapsedTime, notify=globalStatusChanged) + globalExecModeChanged = Signal() globalExecMode = Property(str, globalExecMode.fget, notify=globalExecModeChanged) isComputed = Property(bool, _isComputed, notify=globalStatusChanged) diff --git a/meshroom/ui/qml/Controls/KeyValue.qml b/meshroom/ui/qml/Controls/KeyValue.qml new file mode 100644 index 0000000000..0f0f10592e --- /dev/null +++ b/meshroom/ui/qml/Controls/KeyValue.qml @@ -0,0 +1,52 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 +import MaterialIcons 2.2 + +/** + * KeyValue allows to create a list of key/value, like a table. + */ +Rectangle { + property alias key: keyLabel.text + property alias value: valueText.text + + color: activePalette.window + + width: parent.width + height: childrenRect.height + + RowLayout { + width: parent.width + Rectangle { + anchors.margins: 2 + color: Qt.darker(activePalette.window, 1.1) + // Layout.preferredWidth: sizeHandle.x + Layout.minimumWidth: 10.0 * Qt.application.font.pixelSize + Layout.maximumWidth: 15.0 * Qt.application.font.pixelSize + Layout.fillWidth: false + Layout.fillHeight: true + Label { + id: keyLabel + text: "test" + anchors.fill: parent + anchors.top: parent.top + topPadding: 4 + leftPadding: 6 + verticalAlignment: TextEdit.AlignTop + elide: Text.ElideRight + } + } + TextArea { + id: valueText + text: "" + anchors.margins: 2 + Layout.fillWidth: true + wrapMode: Label.WrapAtWordBoundaryOrAnywhere + textFormat: TextEdit.PlainText + + readOnly: true + selectByMouse: true + background: Rectangle { anchors.fill: parent; color: Qt.darker(activePalette.window, 1.05) } + } + } +} \ No newline at end of file diff --git a/meshroom/ui/qml/Controls/qmldir b/meshroom/ui/qml/Controls/qmldir index a1fc1f6a87..c1e59083b1 100644 --- a/meshroom/ui/qml/Controls/qmldir +++ b/meshroom/ui/qml/Controls/qmldir @@ -3,6 +3,7 @@ module Controls ColorChart 1.0 ColorChart.qml FloatingPane 1.0 FloatingPane.qml Group 1.0 Group.qml +KeyValue 1.0 KeyValue.qml MessageDialog 1.0 MessageDialog.qml Panel 1.0 Panel.qml SearchBar 1.0 SearchBar.qml diff --git a/meshroom/ui/qml/GraphEditor/ChunksListView.qml b/meshroom/ui/qml/GraphEditor/ChunksListView.qml index f0479726b2..a7335f0154 100644 --- a/meshroom/ui/qml/GraphEditor/ChunksListView.qml +++ b/meshroom/ui/qml/GraphEditor/ChunksListView.qml @@ -10,57 +10,87 @@ import "common.js" as Common /** * ChunkListView */ -ListView { - id: chunksLV +ColumnLayout { + id: root + property variant chunks + property int currentIndex: -1 + property variant currentChunk: (chunks && currentIndex >= 0) ? chunks.at(currentIndex) : undefined - // model: node.chunks + onChunksChanged: { + // When the list changes, ensure the current index is in the new range + if(currentIndex >= chunks.count) + currentIndex = chunks.count-1 + } - property variant currentChunk: currentItem ? currentItem.chunk : undefined + // chunksSummary is in sync with allChunks button (but not directly accessible as it is in a Component) + property bool chunksSummary: (currentIndex === -1) width: 60 - Layout.fillHeight: true - highlightFollowsCurrentItem: true - keyNavigationEnabled: true - focus: true - currentIndex: 0 - signal changeCurrentChunk(int chunkIndex) + ListView { + id: chunksLV + Layout.fillWidth: true + Layout.fillHeight: true - header: Component { - Label { - width: chunksLV.width - elide: Label.ElideRight - text: "Chunks" - padding: 4 - z: 10 - background: Rectangle { color: parent.palette.window } - } - } + model: root.chunks - highlight: Component { - Rectangle { - color: activePalette.highlight - opacity: 0.3 - z: 2 + highlightFollowsCurrentItem: (root.chunksSummary === false) + keyNavigationEnabled: true + focus: true + currentIndex: root.currentIndex + onCurrentIndexChanged: { + if(chunksLV.currentIndex !== root.currentIndex) + { + // When the list is resized, the currentIndex is reset to 0. + // So here we force it to keep the binding. + chunksLV.currentIndex = Qt.binding(function() { return root.currentIndex }) + } } - } - 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) + header: Component { + Button { + id: allChunks + text: "Chunks" + width: parent.width + flat: true + checkable: true + property bool summaryEnabled: root.chunksSummary + checked: summaryEnabled + onSummaryEnabledChanged: { + checked = summaryEnabled + } + onClicked: { + root.currentIndex = -1 + checked = true + } + } + } + highlight: Component { + Rectangle { + visible: true // !root.chunksSummary + color: activePalette.highlight + opacity: 0.3 + z: 2 + } } - Rectangle { - width: 4 - height: parent.height - color: Common.getChunkColor(parent.chunk) + highlightMoveDuration: 0 + highlightResizeDuration: 0 + + delegate: ItemDelegate { + id: chunkDelegate + property var chunk: object + text: index + width: parent.width + leftPadding: 8 + onClicked: { + chunksLV.forceActiveFocus() + root.currentIndex = 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 ba3e33bf61..42c361db8a 100644 --- a/meshroom/ui/qml/GraphEditor/NodeEditor.qml +++ b/meshroom/ui/qml/GraphEditor/NodeEditor.qml @@ -1,5 +1,6 @@ import QtQuick 2.9 import QtQuick.Controls 2.4 +import QtQuick.Controls 1.4 as Controls1 // SplitView import QtQuick.Layouts 1.3 import MaterialIcons 2.2 import Controls 1.0 @@ -19,15 +20,6 @@ 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 } @@ -114,7 +106,16 @@ Panel { Component { id: editor_component - ColumnLayout { + Controls1.SplitView { + anchors.fill: parent + + // The list of chunks + ChunksListView { + id: chunksLV + visible: (tabBar.currentIndex >= 1 && tabBar.currentIndex <= 3) + chunks: root.node.chunks + } + StackLayout { Layout.fillHeight: true Layout.fillWidth: true @@ -122,35 +123,65 @@ Panel { currentIndex: tabBar.currentIndex AttributeEditor { + Layout.fillHeight: true + Layout.fillWidth: true model: root.node.attributes readOnly: root.readOnly || root.isCompatibilityNode onAttributeDoubleClicked: root.attributeDoubleClicked(mouse, attribute) onUpgradeRequest: root.upgradeRequest() } - NodeLog { - id: nodeLog - node: root.node - chunkCurrentIndex: m.chunkCurrentIndex - onChangeCurrentChunk: { m.chunkCurrentIndex = chunkIndex } + Loader { + active: (tabBar.currentIndex === 1) + Layout.fillHeight: true + Layout.fillWidth: true + sourceComponent: NodeLog { + // anchors.fill: parent + Layout.fillHeight: true + Layout.fillWidth: true + width: parent.width + height: parent.height + id: nodeLog + node: root.node + currentChunkIndex: chunksLV.currentIndex + currentChunk: chunksLV.currentChunk + } } - NodeStatistics { - id: nodeStatistics - node: root.node - chunkCurrentIndex: m.chunkCurrentIndex - onChangeCurrentChunk: { m.chunkCurrentIndex = chunkIndex } + Loader { + active: (tabBar.currentIndex === 2) + Layout.fillHeight: true + Layout.fillWidth: true + sourceComponent: NodeStatistics { + id: nodeStatistics + + Layout.fillHeight: true + Layout.fillWidth: true + node: root.node + currentChunkIndex: chunksLV.currentIndex + currentChunk: chunksLV.currentChunk + } } - NodeStatus { - id: nodeStatus - node: root.node - chunkCurrentIndex: m.chunkCurrentIndex - onChangeCurrentChunk: { m.chunkCurrentIndex = chunkIndex } + Loader { + active: (tabBar.currentIndex === 3) + Layout.fillHeight: true + Layout.fillWidth: true + sourceComponent: NodeStatus { + id: nodeStatus + + Layout.fillHeight: true + Layout.fillWidth: true + node: root.node + currentChunkIndex: chunksLV.currentIndex + currentChunk: chunksLV.currentChunk + } } NodeDocumentation { id: nodeDocumentation + + Layout.fillHeight: true Layout.fillWidth: true node: root.node } diff --git a/meshroom/ui/qml/GraphEditor/NodeLog.qml b/meshroom/ui/qml/GraphEditor/NodeLog.qml index bea42622aa..941d5099f9 100644 --- a/meshroom/ui/qml/GraphEditor/NodeLog.qml +++ b/meshroom/ui/qml/GraphEditor/NodeLog.qml @@ -1,6 +1,5 @@ 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 @@ -16,53 +15,34 @@ import "common.js" as Common FocusScope { id: root property variant node - property alias chunkCurrentIndex: chunksLV.currentIndex - signal changeCurrentChunk(int chunkIndex) + property int currentChunkIndex + property variant currentChunk + + Layout.fillWidth: true + Layout.fillHeight: true SystemPalette { id: activePalette } - Controls1.SplitView { + Loader { + id: componentLoader + clip: true 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["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 + property string currentFile: (root.currentChunkIndex >= 0 && root.currentChunk) ? root.currentChunk["logFile"] : "" + property url source: Filepath.stringToUrl(currentFile) - if(!chunksLV.count || chunksLV.currentChunk) - componentLoader.source = Filepath.stringToUrl(currentFile); - - } + sourceComponent: textFileViewerComponent + } - sourceComponent: textFileViewerComponent - } + Component { + id: textFileViewerComponent - Component { - id: textFileViewerComponent - TextFileViewer { - id: textFileViewer - source: componentLoader.source - Layout.fillWidth: true - Layout.fillHeight: true - autoReload: chunksLV.currentChunk !== undefined && chunksLV.currentChunk.statusName === "RUNNING" - // source is set in fileSelector - } + TextFileViewer { + id: textFileViewer + anchors.fill: parent + source: componentLoader.source + autoReload: root.currentChunk !== undefined && root.currentChunk.statusName === "RUNNING" + // source is set in fileSelector } } } diff --git a/meshroom/ui/qml/GraphEditor/NodeStatistics.qml b/meshroom/ui/qml/GraphEditor/NodeStatistics.qml index 5e4ec3e80a..b93902b917 100644 --- a/meshroom/ui/qml/GraphEditor/NodeStatistics.qml +++ b/meshroom/ui/qml/GraphEditor/NodeStatistics.qml @@ -4,6 +4,7 @@ import QtQuick.Controls 1.4 as Controls1 // SplitView import QtQuick.Layouts 1.3 import MaterialIcons 2.2 import Controls 1.0 +import Utils 1.0 import "common.js" as Common @@ -15,50 +16,44 @@ import "common.js" as Common */ FocusScope { id: root + property variant node - property alias chunkCurrentIndex: chunksLV.currentIndex - signal changeCurrentChunk(int chunkIndex) + property variant currentChunkIndex + property variant currentChunk SystemPalette { id: activePalette } - Controls1.SplitView { + Loader { + id: componentLoader + clip: true anchors.fill: parent + property string currentFile: currentChunk ? currentChunk["statisticsFile"] : "" + property url source: Filepath.stringToUrl(currentFile) - // The list of chunks - ChunksListView { - id: chunksLV - Layout.fillHeight: true - model: node.chunks - onChangeCurrentChunk: root.changeCurrentChunk(chunkIndex) - } + sourceComponent: chunksLV.chunksSummary ? statViewerComponent : chunkStatViewerComponent + } - Loader { - id: componentLoader - clip: true - Layout.fillWidth: true - Layout.fillHeight: true - property url source + Component { + id: chunkStatViewerComponent + StatViewer { + id: statViewer + anchors.fill: parent + source: componentLoader.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 + Component { + id: statViewerComponent - if(!chunksLV.count || chunksLV.currentChunk) - componentLoader.source = Filepath.stringToUrl(currentFile); + Column { + spacing: 2 + KeyValue { + key: "Time" + value: Format.sec2time(node.elapsedTime) } - - sourceComponent: statViewerComponent - } - - Component { - id: statViewerComponent - StatViewer { - id: statViewer - Layout.fillWidth: true - Layout.fillHeight: true - source: componentLoader.source + KeyValue { + key: "Cumulated Time" + value: Format.sec2time(node.recursiveElapsedTime) } } } diff --git a/meshroom/ui/qml/GraphEditor/NodeStatus.qml b/meshroom/ui/qml/GraphEditor/NodeStatus.qml index 063bcae583..8de87fbc3e 100644 --- a/meshroom/ui/qml/GraphEditor/NodeStatus.qml +++ b/meshroom/ui/qml/GraphEditor/NodeStatus.qml @@ -1,6 +1,5 @@ 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 @@ -16,169 +15,148 @@ import "common.js" as Common FocusScope { id: root property variant node - property alias chunkCurrentIndex: chunksLV.currentIndex - signal changeCurrentChunk(int chunkIndex) + property variant currentChunkIndex + property variant currentChunk SystemPalette { id: activePalette } - Controls1.SplitView { + Loader { + id: componentLoader + clip: true anchors.fill: parent - // The list of chunks - ChunksListView { - id: chunksLV - Layout.fillHeight: true - model: node.chunks - onChangeCurrentChunk: root.changeCurrentChunk(chunkIndex) - } + property string currentFile: (root.currentChunkIndex >= 0) ? root.currentChunk["statusFile"] : "" + property url source: Filepath.stringToUrl(currentFile) + + sourceComponent: statViewerComponent + } - 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); + Component { + id: statViewerComponent + Item { + id: statusViewer + property url source: componentLoader.source + property var lastModified: undefined + + onSourceChanged: { + statusListModel.readSourceFile() } - sourceComponent: statViewerComponent - } + ListModel { + id: statusListModel - Component { - id: statViewerComponent - Item { - id: statusViewer - property url source: componentLoader.source - property var lastModified: undefined + function readSourceFile() { + // make sure we are trying to load a statistics file + if(!Filepath.urlToString(source).endsWith("status")) + return; - onSourceChanged: { - statusListModel.readSourceFile() - } + var xhr = new XMLHttpRequest; + xhr.open("GET", source); - 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) + 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) { - // console.warn("StatusListModel: failed to read file") - lastModified = undefined; - statusListModel.clear(); + 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(); - } + } + 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 + ListView { + id: statusListView + anchors.fill: parent + spacing: 3 + model: statusListModel + + delegate: Rectangle { + color: activePalette.window + width: parent.width + height: childrenRect.height + RowLayout { width: parent.width - height: childrenRect.height - RowLayout { - width: parent.width - Rectangle { - id: statusKey - anchors.margins: 2 - // height: statusValue.height - color: Qt.darker(activePalette.window, 1.1) - Layout.preferredWidth: sizeHandle.x - Layout.minimumWidth: 10.0 * Qt.application.font.pixelSize - Layout.maximumWidth: 15.0 * Qt.application.font.pixelSize - Layout.fillWidth: false - Layout.fillHeight: true - Label { - text: key - anchors.fill: parent - anchors.top: parent.top - topPadding: 4 - leftPadding: 6 - verticalAlignment: TextEdit.AlignTop - elide: Text.ElideRight - } - } - TextArea { - id: statusValue - text: value - anchors.margins: 2 - Layout.fillWidth: true - wrapMode: Label.WrapAtWordBoundaryOrAnywhere - textFormat: TextEdit.PlainText - - readOnly: true - selectByMouse: true - background: Rectangle { anchors.fill: parent; color: Qt.darker(activePalette.window, 1.05) } + Rectangle { + id: statusKey + anchors.margins: 2 + // height: statusValue.height + color: Qt.darker(activePalette.window, 1.1) + Layout.preferredWidth: sizeHandle.x + Layout.minimumWidth: 10.0 * Qt.application.font.pixelSize + Layout.maximumWidth: 15.0 * Qt.application.font.pixelSize + Layout.fillWidth: false + Layout.fillHeight: true + Label { + text: key + anchors.fill: parent + anchors.top: parent.top + topPadding: 4 + leftPadding: 6 + verticalAlignment: TextEdit.AlignTop + elide: Text.ElideRight } } + TextArea { + id: statusValue + text: value + anchors.margins: 2 + Layout.fillWidth: true + wrapMode: Label.WrapAtWordBoundaryOrAnywhere + textFormat: TextEdit.PlainText + + readOnly: true + selectByMouse: true + background: Rectangle { anchors.fill: parent; color: Qt.darker(activePalette.window, 1.05) } + } } } + } - // 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 - } + // 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 } } } diff --git a/meshroom/ui/qml/Utils/format.js b/meshroom/ui/qml/Utils/format.js index 72b732e08d..c439c0b1d4 100644 --- a/meshroom/ui/qml/Utils/format.js +++ b/meshroom/ui/qml/Utils/format.js @@ -13,3 +13,12 @@ function plainToHtml(t) { var escaped = t.replace(/&/g, '&').replace(//g, '>'); // escape text return escaped.replace(/\n/g, '
'); // replace line breaks } + +function sec2time(time) { + var pad = function(num, size) { return ('000' + num).slice(size * -1); }, + hours = Math.floor(time / 60 / 60), + minutes = Math.floor(time / 60) % 60, + seconds = Math.floor(time - minutes * 60); + + return pad(hours, 2) + ':' + pad(minutes, 2) + ':' + pad(seconds, 2) +}