diff --git a/meshroom/ui/qml/Charts/ChartViewCheckBox.qml b/meshroom/ui/qml/Charts/ChartViewCheckBox.qml new file mode 100644 index 0000000000..0b395b72e7 --- /dev/null +++ b/meshroom/ui/qml/Charts/ChartViewCheckBox.qml @@ -0,0 +1,34 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.3 + + +/** + * A custom CheckBox designed to be used in ChartView's legend. + */ +CheckBox { + id: root + + property color color + + leftPadding: 0 + font.pointSize: 8 + + indicator: Rectangle { + width: 11 + height: width + border.width: 1 + border.color: root.color + color: "transparent" + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + anchors.fill: parent + anchors.margins: parent.border.width + 1 + visible: parent.parent.checkState != Qt.Unchecked + anchors.topMargin: parent.parent.checkState === Qt.PartiallyChecked ? 5 : 2 + anchors.bottomMargin: anchors.topMargin + color: parent.border.color + anchors.centerIn: parent + } + } +} diff --git a/meshroom/ui/qml/Charts/ChartViewLegend.qml b/meshroom/ui/qml/Charts/ChartViewLegend.qml new file mode 100644 index 0000000000..1244871559 --- /dev/null +++ b/meshroom/ui/qml/Charts/ChartViewLegend.qml @@ -0,0 +1,105 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.9 +import QtCharts 2.3 + + +/** + * ChartViewLegend is an interactive legend component for ChartViews. + * It provides a CheckBox for each series that can control its visibility, + * and highlight on hovering. + */ +Flow { + id: root + + // The ChartView to create the legend for + property ChartView chartView + // Currently hovered series + property var hoveredSeries: null + + readonly property ButtonGroup buttonGroup: ButtonGroup { + id: legendGroup + exclusive: false + } + + /// Shortcut function to clear legend + function clear() { + seriesModel.clear(); + } + + // Update internal ListModel when ChartView's series change + Connections { + target: chartView + onSeriesAdded: seriesModel.append({"series": series}) + onSeriesRemoved: { + for(var i = 0; i < seriesModel.count; ++i) + { + if(seriesModel.get(i)["series"] === series) + { + seriesModel.remove(i); + return; + } + } + } + } + + onChartViewChanged: { + clear(); + for(var i = 0; i < chartView.count; ++i) + seriesModel.append({"series": chartView.series(i)}); + } + + Repeater { + + // ChartView series can't be accessed directly as a model. + // Use an intermediate ListModel populated with those series. + model: ListModel { + id: seriesModel + } + + ChartViewCheckBox { + ButtonGroup.group: legendGroup + + checked: series.visible + text: series.name + color: series.color + + onHoveredChanged: { + if(hovered && series.visible) + root.hoveredSeries = series; + else + root.hoveredSeries = null; + } + + // hovered serie properties override + states: [ + State { + when: series && root.hoveredSeries === series + PropertyChanges { target: series; width: 5.0 } + }, + State { + when: series && root.hoveredSeries && root.hoveredSeries !== series + PropertyChanges { target: series; width: 0.2 } + } + ] + + MouseArea { + anchors.fill: parent + onClicked: { + if(mouse.modifiers & Qt.ControlModifier) + root.soloSeries(index); + else + series.visible = !series.visible; + } + } + } + } + + /// Hide all series but the one at index 'idx' + function soloSeries(idx) { + for(var i = 0; i < seriesModel.count; i++) { + chartView.series(i).visible = false; + } + chartView.series(idx).visible = true; + } + +} diff --git a/meshroom/ui/qml/Charts/qmldir b/meshroom/ui/qml/Charts/qmldir new file mode 100644 index 0000000000..32ea2d3271 --- /dev/null +++ b/meshroom/ui/qml/Charts/qmldir @@ -0,0 +1,4 @@ +module Charts + +ChartViewLegend 1.0 ChartViewLegend.qml +ChartViewCheckBox 1.0 ChartViewCheckBox.qml diff --git a/meshroom/ui/qml/GraphEditor/NodeLog.qml b/meshroom/ui/qml/GraphEditor/NodeLog.qml index 8254a08bb3..3d5923e66f 100644 --- a/meshroom/ui/qml/GraphEditor/NodeLog.qml +++ b/meshroom/ui/qml/GraphEditor/NodeLog.qml @@ -93,11 +93,6 @@ FocusScope { if(!chunksLV.count || chunksLV.currentChunk) logComponentLoader.source = Filepath.stringToUrl(currentFile); - if(currentItem.fileProperty === "statisticsFile") { - logComponentLoader.componentNb = 1 - } else { - logComponentLoader.componentNb = 0 - } } TabButton { @@ -123,9 +118,8 @@ FocusScope { clip: true Layout.fillWidth: true Layout.fillHeight: true - property int componentNb: 0 property url source - sourceComponent: componentNb === 0 ? textFileViewerComponent : statViewerComponent + sourceComponent: fileSelector.currentItem.fileProperty === "statisticsFile" ? statViewerComponent : textFileViewerComponent } Component { diff --git a/meshroom/ui/qml/GraphEditor/StatViewer.qml b/meshroom/ui/qml/GraphEditor/StatViewer.qml index 5db15c9287..e322f5e73f 100644 --- a/meshroom/ui/qml/GraphEditor/StatViewer.qml +++ b/meshroom/ui/qml/GraphEditor/StatViewer.qml @@ -3,6 +3,7 @@ import QtQuick.Controls 2.3 import QtCharts 2.2 import QtQuick.Layouts 1.11 import Utils 1.0 +import Charts 1.0 import MaterialIcons 2.2 Item { @@ -11,13 +12,14 @@ Item { implicitWidth: 500 implicitHeight: 500 + /// Statistics source file property url source + property var sourceModified: undefined property var jsonObject property int nbReads: 1 - property var deltaTime: 1 + property real deltaTime: 1 - property var cpuLineSeries: [] property int nbCores: 0 property int cpuFrequency: 0 @@ -29,6 +31,7 @@ Item { property color textColor: Colors.sysPalette.text + readonly property var colors: [ "#f44336", "#e91e63", @@ -64,81 +67,79 @@ Item { "#BF360C" ] - onSourceChanged: function() { + onSourceChanged: { + sourceModified = undefined; resetCharts() readSourceFile() } Timer { - interval: root.deltaTime * 60000; running: true; repeat: true - onTriggered: function() { - var xhr = new XMLHttpRequest; - xhr.open("GET", source); - - xhr.onreadystatechange = function() { - if (xhr.readyState === XMLHttpRequest.DONE) { - - if(sourceModified === undefined || sourceModified < xhr.getResponseHeader('Last-Modified')) { - var jsonString = xhr.responseText; - - jsonObject= JSON.parse(jsonString); - root.jsonObject = jsonObject; - resetCharts() - sourceModified = xhr.getResponseHeader('Last-Modified') - root.createCharts() - } - } - }; - xhr.send(); - } + id: reloadTimer + interval: root.deltaTime * 60000; running: true; repeat: false + onTriggered: readSourceFile() + } function readSourceFile() { + // make sure we are trying to load a statistics file + if(!Filepath.urlToString(source).endsWith("statistics")) + return; + var xhr = new XMLHttpRequest; xhr.open("GET", source); xhr.onreadystatechange = function() { - if (xhr.readyState === XMLHttpRequest.DONE) { - var jsonString = xhr.responseText; + if (xhr.readyState === XMLHttpRequest.DONE && xhr.status == 200) { - jsonObject= JSON.parse(jsonString); - root.jsonObject = jsonObject; + if(sourceModified === undefined || sourceModified < xhr.getResponseHeader('Last-Modified')) { + var jsonObject; - root.createCharts() + try { + jsonObject = JSON.parse(xhr.responseText); + } + catch(exc) + { + console.warning("Failed to parse statistics file: " + source) + root.jsonObject = {}; + return; + } + root.jsonObject = jsonObject; + resetCharts(); + sourceModified = xhr.getResponseHeader('Last-Modified') + root.createCharts(); + reloadTimer.restart(); + } } }; xhr.send(); } function resetCharts() { - cpuLineSeries = [] + cpuLegend.clear() cpuChart.removeAllSeries() - cpuCheckboxModel.clear() ramChart.removeAllSeries() gpuChart.removeAllSeries() } function createCharts() { + root.deltaTime = jsonObject.interval / 60.0; initCpuChart() initRamChart() initGpuChart() - - root.deltaTime = jsonObject.interval /60 } - - /************************** *** CPU *** **************************/ function initCpuChart() { + var categories = [] var categoryCount = 0 var category do { - category = jsonObject.computer.curves["cpuUsage." + categoryCount] + category = root.jsonObject.computer.curves["cpuUsage." + categoryCount] if(category !== undefined) { categories.push(category) categoryCount++ @@ -153,7 +154,6 @@ Item { root.nbReads = categories[0].length-1 for(var j = 0; j < nbCores; j++) { - cpuCheckboxModel.append({ name: "CPU" + j, index: j, indicColor: colors[j % colors.length] }) var lineSerie = cpuChart.createSeries(ChartView.SeriesTypeLine, "CPU" + j, valueAxisX, valueAxisY) if(categories[j].length === 1) { @@ -165,11 +165,8 @@ Item { } } lineSerie.color = colors[j % colors.length] - - root.cpuLineSeries.push(lineSerie) } - cpuCheckboxModel.append({ name: "AVERAGE", index: nbCores, indicColor: colors[0] }) var averageLine = cpuChart.createSeries(ChartView.SeriesTypeLine, "AVERAGE", valueAxisX, valueAxisY) var average = [] @@ -184,56 +181,21 @@ Item { } for(var q = 0; q < average.length; q++) { - average[q] = average[q] / (categories.length-1) + average[q] = average[q] / (categories.length) averageLine.append(q * root.deltaTime, average[q]) } - averageLine.color = colors[0] - - root.cpuLineSeries.push(averageLine) - } - - function showCpu(index) { - let serie = cpuLineSeries[index] - if(!serie.visible) { - serie.visible = true - } - } - - function hideCpu(index) { - let serie = cpuLineSeries[index] - if(serie.visible) { - serie.visible = false - } + averageLine.color = colors[colors.length-1] } function hideOtherCpu(index) { - for(var i = 0; i < cpuLineSeries.length; i++) { - cpuLineSeries[i].visible = false - } - - cpuLineSeries[i].visible = true - } - - function higlightCpu(index) { - for(var i = 0; i < cpuLineSeries.length; i++) { - if(i === index) { - cpuLineSeries[i].width = 5.0 - } else { - cpuLineSeries[i].width = 0.2 - } + for(var i = 0; i < cpuChart.count; i++) { + cpuChart.series(i).visible = false; } + cpuChart.series(index).visible = true; } - function stopHighlightCpu(index) { - for(var i = 0; i < cpuLineSeries.length; i++) { - cpuLineSeries[i].width = 2.0 - } - } - - - /************************** *** RAM *** @@ -244,7 +206,7 @@ Item { var ram = jsonObject.computer.curves.ramUsage - var ramSerie = ramChart.createSeries(ChartView.SeriesTypeLine, "RAM", valueAxisX2, valueAxisRam) + var ramSerie = ramChart.createSeries(ChartView.SeriesTypeLine, "RAM: " + root.ramTotal + "GB", valueAxisX2, valueAxisRam) if(ram.length === 1) { ramSerie.append(0, ram[0] / 100 * root.ramTotal) @@ -323,16 +285,14 @@ Item { Button { id: toggleCpuBtn Layout.fillWidth: true - height: 30 text: "Toggle CPU's" state: "closed" onClicked: state === "opened" ? state = "closed" : state = "opened" - Text { + MaterialLabel { text: MaterialIcons.arrow_drop_down - font.pixelSize: 24 - color: "#eee" + font.pointSize: 14 anchors.right: parent.right } @@ -361,111 +321,30 @@ Item { width: parent.width anchors.horizontalCenter: parent.horizontalCenter - ButtonGroup { - id: cpuGroup - exclusive: false - checkState: allCPU.checkState - } - CheckBox { - width: 80 - checked: true + ChartViewCheckBox { id: allCPU text: "ALL" - checkState: cpuGroup.checkState - - indicator: Rectangle { - width: 20 - height: 20 - border.color: textColor - border.width: 2 - color: "transparent" - anchors.verticalCenter: parent.verticalCenter - - Rectangle { - anchors.centerIn: parent - width: 10 - height: allCPU.checkState === 1 ? 4 : 10 - color: allCPU.checkState === 0 ? "transparent" : textColor + color: textColor + checkState: cpuLegend.buttonGroup.checkState + leftPadding: 0 + onClicked: { + var _checked = checked; + for(var i = 0; i < cpuChart.count; ++i) + { + cpuChart.series(i).visible = _checked; } } - - leftPadding: indicator.width + 5 - - contentItem: Label { - text: allCPU.text - font: allCPU.font - verticalAlignment: Text.AlignVCenter - } - - Layout.fillHeight: true } - ListModel { - id: cpuCheckboxModel - } - - Flow { + ChartViewLegend { + id: cpuLegend Layout.fillWidth: true - - Repeater { - model: cpuCheckboxModel - - CheckBox { - width: 80 - checked: true - text: name - ButtonGroup.group: cpuGroup - - indicator: Rectangle { - width: 20 - height: 20 - border.color: indicColor - border.width: 2 - color: "transparent" - anchors.verticalCenter: parent.verticalCenter - - Rectangle { - anchors.centerIn: parent - width: 10 - height: parent.parent.checkState === 1 ? 4 : 10 - color: parent.parent.checkState === 0 ? "transparent" : indicColor - } - } - - leftPadding: indicator.width + 5 - - contentItem: Label { - text: name - verticalAlignment: Text.AlignVCenter - } - - onCheckStateChanged: function() { - if(checkState === 2) { - root.showCpu(index) - } else { - root.hideCpu(index) - } - } - - onHoveredChanged: function() { - if(hovered) { - root.higlightCpu(index) - } else { - root.stopHighlightCpu(index) - } - } - - onDoubleClicked: function() { - name.checked = false - root.hideOtherCpu(index) - } - } - } + Layout.fillHeight: true + chartView: cpuChart } - } - + } } ChartView { @@ -473,7 +352,10 @@ Item { Layout.fillWidth: true Layout.preferredHeight: width/2 + margins.top: 0 + margins.bottom: 0 antialiasing: true + legend.visible: false theme: ChartView.ChartThemeLight backgroundColor: "transparent" @@ -519,15 +401,16 @@ Item { ColumnLayout { - ChartView { id: ramChart - + margins.top: 0 + margins.bottom: 0 Layout.fillWidth: true Layout.preferredHeight: width/2 antialiasing: true legend.color: textColor legend.labelColor: textColor + legend.visible: false theme: ChartView.ChartThemeLight backgroundColor: "transparent" plotAreaColor: "transparent" @@ -590,6 +473,8 @@ Item { Layout.fillWidth: true Layout.preferredHeight: width/2 + margins.top: 0 + margins.bottom: 0 antialiasing: true legend.color: textColor legend.labelColor: textColor