From bc4586ffbe59373a47f9d63a2742a8f0f7ef4c05 Mon Sep 17 00:00:00 2001 From: Ulf Hermann Date: Fri, 7 Nov 2014 18:13:31 +0100 Subject: [PATCH] QmlProfiler: Make TimeDisplay and TimeMarks declarative Create the vertical elements only once and stretch them over the whole vertical space, but keep all horizontal elements distinct per model. Also, add horizontal elements to the labels group and rename that accordingly, so that we don't need a third Flickable or ListView Change-Id: I4fe3bd526767e8ff5d0ebcd70e9343f65fcc787f Reviewed-by: Kai Koehne --- src/plugins/qmlprofiler/qml/CategoryLabel.qml | 8 - src/plugins/qmlprofiler/qml/MainView.qml | 182 +++++++++-------- src/plugins/qmlprofiler/qml/TimeDisplay.qml | 190 ++++++++++++------ src/plugins/qmlprofiler/qml/TimeMarks.qml | 186 +++++++---------- 4 files changed, 307 insertions(+), 259 deletions(-) diff --git a/src/plugins/qmlprofiler/qml/CategoryLabel.qml b/src/plugins/qmlprofiler/qml/CategoryLabel.qml index c5bbb7bc872..885125ba532 100644 --- a/src/plugins/qmlprofiler/qml/CategoryLabel.qml +++ b/src/plugins/qmlprofiler/qml/CategoryLabel.qml @@ -124,14 +124,6 @@ Item { renderType: Text.NativeRendering } - Rectangle { - height: 1 - width: parent.width - color: "#999999" - anchors.bottom: parent.bottom - z: 2 - } - Column { id: column property QtObject parentModel: model diff --git a/src/plugins/qmlprofiler/qml/MainView.qml b/src/plugins/qmlprofiler/qml/MainView.qml index 29d33742161..171e4c11706 100644 --- a/src/plugins/qmlprofiler/qml/MainView.qml +++ b/src/plugins/qmlprofiler/qml/MainView.qml @@ -105,7 +105,6 @@ Rectangle { zoomSlider.externalUpdate = true; zoomSlider.value = zoomSlider.minimumValue; overview.clear(); - timeDisplay.clear(); } function propagateSelection(newModel, newItem) { @@ -190,46 +189,64 @@ Rectangle { Keys.onReleased: shiftPressed = false; Flickable { - id: labelsflick + id: categories flickableDirection: Flickable.VerticalFlick interactive: false anchors.top: buttonsBar.bottom anchors.bottom: overview.top anchors.left: parent.left - width: labels.width + anchors.right: parent.right contentY: flick.contentY // reserve some more space than needed to prevent weird effects when resizing - contentHeight: labels.height + height + contentHeight: categoryContent.height + height - Rectangle { - id: labels + // Dispatch the cursor shape to all labels. When dragging the DropArea receiving + // the drag events is not necessarily related to the MouseArea receiving the mouse + // events, so we can't use the drag events to determine the cursor shape. + property bool dragging: false + + Column { + id: categoryContent anchors.left: parent.left - width: 150 - color: root.color - height: col.height + anchors.right: parent.right - Column { - id: col + DelegateModel { + id: labelsModel - // Dispatch the cursor shape to all labels. When dragging the DropArea receiving - // the drag events is not necessarily related to the MouseArea receiving the mouse - // events, so we can't use the drag events to determine the cursor shape. - property bool dragging: false + // As we cannot retrieve items by visible index we keep an array of row counts here, + // for the time marks to draw the row backgrounds in the right colors. + property var rowCounts: new Array(qmlProfilerModelProxy.models.length) - DelegateModel { - id: labelsModel - model: qmlProfilerModelProxy.models - delegate: CategoryLabel { + function updateRowCount(visualIndex, rowCount) { + if (rowCounts[visualIndex] !== rowCount) { + rowCounts[visualIndex] = rowCount; + // Array don't "change" if entries change. We have to signal manually. + rowCountsChanged(); + } + } + + model: qmlProfilerModelProxy.models + delegate: Rectangle { + color: root.color + + anchors.left: parent.left + anchors.right: parent.right + property int visualIndex: DelegateModel.itemsIndex + height: label.visible ? label.height : 0 + + CategoryLabel { + id: label model: modelData - mockup: qmlProfilerModelProxy.height == 0 - visualIndex: DelegateModel.itemsIndex - dragging: col.dragging + mockup: qmlProfilerModelProxy.height === 0 + visualIndex: parent.visualIndex + dragging: categories.dragging reverseSelect: root.shiftPressed - onDragStarted: col.dragging = true - onDragStopped: col.dragging = false - draggerParent: labels - dragOffset: y + onDragStarted: categories.dragging = true + onDragStopped: categories.dragging = false + draggerParent: categories + width: 150 + dragOffset: parent.y onDropped: { timelineModel.items.move(sourceIndex, targetIndex); @@ -251,26 +268,62 @@ Rectangle { zoomControl.rangeStart, root.selectedModel === index ? root.selectedItem : -1)); } + } + TimeMarks { + id: timeMarks + model: modelData + mockup: qmlProfilerModelProxy.height === 0 + anchors.right: parent.right + anchors.left: label.right + anchors.top: parent.top + anchors.bottom: parent.bottom + property int visualIndex: parent.visualIndex + // Quite a mouthful, but works fine: Add up all the row counts up to the one + // for this visual index and check if the result is even or odd. + startOdd: (labelsModel.rowCounts.slice(0, visualIndex).reduce( + function(prev, rows) {return prev + rows}, 0) % 2) === 0 + + onRowCountChanged: labelsModel.updateRowCount(visualIndex, rowCount) + onVisualIndexChanged: labelsModel.updateRowCount(visualIndex, rowCount) + } + + Rectangle { + visible: label.visible + opacity: parent.y == 0 ? 0 : 1 + color: "#B0B0B0" + height: 1 + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top } } - - Repeater { - model: labelsModel - } } + + Repeater { + model: labelsModel + } + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: categoryContent.bottom + height: 1 + color: "#B0B0B0" } } - // border between labels and timeline - Rectangle { - id: labelsborder - anchors.left: labelsflick.right + TimeDisplay { + id: timeDisplay anchors.top: parent.top + anchors.left: buttonsBar.right + anchors.right: parent.right anchors.bottom: overview.top - width: 1 - color: "#858585" + zoomer: zoomControl + contentX: flick.contentX + clip: true } ButtonsBar { @@ -297,21 +350,12 @@ Rectangle { onLockChanged: selectionLocked = !lockButtonChecked(); } - TimeDisplay { - id: timeDisplay - anchors.top: parent.top - anchors.left: labelsborder.right - anchors.right: parent.right - height: 24 - } - Flickable { id: flick - contentHeight: labels.height + contentHeight: categoryContent.height contentWidth: zoomControl.windowDuration * width / Math.max(1, zoomControl.rangeDuration) flickableDirection: Flickable.HorizontalAndVerticalFlick boundsBehavior: Flickable.StopAtBounds - clip:true // ScrollView will try to deinteractivate it. We don't want that // as the horizontal flickable is interactive, too. We do occasionally @@ -375,9 +419,6 @@ Rectangle { signal clearChildren signal select(int modelIndex, int eventIndex) - // As we cannot retrieve items by visible index we keep an array of row counts here, - // for the time marks to draw the row backgrounds in the right colors. - property var rowCounts: new Array(qmlProfilerModelProxy.models.length) DelegateModel { id: timelineModel @@ -386,35 +427,8 @@ Rectangle { id: spacer height: modelData.height width: flick.contentWidth - property int rowCount: (modelData.empty || modelData.hidden) ? - 0 : modelData.rowCount property int visualIndex: DelegateModel.itemsIndex - function updateRowParentCount() { - if (timelineView.rowCounts[visualIndex] !== rowCount) { - timelineView.rowCounts[visualIndex] = rowCount; - // Array don't "change" if entries change. We have to signal manually. - timelineView.rowCountsChanged(); - } - } - - onRowCountChanged: updateRowParentCount() - onVisualIndexChanged: updateRowParentCount() - - TimeMarks { - model: modelData - id: backgroundMarks - anchors.fill: renderer - startTime: zoomControl.rangeStart - endTime: zoomControl.rangeEnd - - // Quite a mouthful, but works fine: Add up all the row counts up to the one - // for this visual index and check if the result is even or odd. - startOdd: (timelineView.rowCounts.slice(0, spacer.visualIndex).reduce( - function(prev, rows) {return prev + rows}, 0) % 2) === 0 - onStartOddChanged: requestPaint() - } - TimelineRenderer { id: renderer model: modelData @@ -543,8 +557,8 @@ Rectangle { ScrollView { id: scroller contentItem: flick - anchors.left: labelsborder.right - anchors.top: timeDisplay.bottom + anchors.left: buttonsBar.right + anchors.top: categories.top anchors.bottom: overview.top anchors.right: parent.right } @@ -565,12 +579,20 @@ Rectangle { notes: qmlProfilerModelProxy.notes } + Rectangle { + anchors.left: buttonsBar.right + anchors.bottom: overview.top + anchors.top: parent.top + width: 1 + color: "#B0B0B0" + } + Rectangle { id: filterMenu color: "#9b9b9b" enabled: buttonsBar.enabled visible: false - width: labels.width + width: buttonsBar.width anchors.left: parent.left anchors.top: buttonsBar.bottom height: qmlProfilerModelProxy.models.length * buttonsBar.height @@ -598,7 +620,7 @@ Rectangle { color: "#9b9b9b" enabled: buttonsBar.enabled visible: false - width: labels.width + width: categoryContent.width height: buttonsBar.height anchors.left: parent.left anchors.top: buttonsBar.bottom diff --git a/src/plugins/qmlprofiler/qml/TimeDisplay.qml b/src/plugins/qmlprofiler/qml/TimeDisplay.qml index 31f863d1043..d6182c2fc70 100644 --- a/src/plugins/qmlprofiler/qml/TimeDisplay.qml +++ b/src/plugins/qmlprofiler/qml/TimeDisplay.qml @@ -29,87 +29,44 @@ ****************************************************************************/ import QtQuick 2.1 -import Monitor 1.0 -Canvas { +Item { id: timeDisplay - objectName: "TimeDisplay" - contextType: "2d" - Connections { - target: zoomControl - onRangeChanged: requestPaint(); - } + property QtObject zoomer - onPaint: { - var context = (timeDisplay.context === null) ? getContext("2d") : timeDisplay.context; + readonly property int labelsHeight: 24 - context.reset(); - context.fillStyle = "white"; - context.fillRect(0, 0, width, height); + readonly property int initialBlockLength: 120 - var totalTime = Math.max(1, zoomControl.rangeDuration); - var spacing = width / totalTime; + property double spacing: width / rangeDuration - var initialBlockLength = 120; - var timePerBlock = Math.pow(2, Math.floor( Math.log( totalTime / width * initialBlockLength ) / Math.LN2 ) ); - var pixelsPerBlock = timePerBlock * spacing; - var pixelsPerSection = pixelsPerBlock / 5; - var blockCount = width / pixelsPerBlock; + property double timePerBlock: Math.pow(2, Math.floor(Math.log(initialBlockLength / spacing) / + Math.LN2)) - var realStartTime = Math.floor(zoomControl.rangeStart / timePerBlock) * timePerBlock; - var startPos = (zoomControl.rangeStart - realStartTime) * spacing; + property double rangeDuration: Math.max(1, Math.round(zoomer.rangeDuration)) + property double alignedWindowStart: Math.round(zoomer.windowStart - (zoomer.windowStart % timePerBlock)) + property double pixelsPerBlock: timeDisplay.timePerBlock * timeDisplay.spacing + property double pixelsPerSection: pixelsPerBlock / 5 - var timePerPixel = timePerBlock/pixelsPerBlock; + property int contentX + property int offsetX: contentX + Math.round((zoomer.windowStart % timePerBlock) * spacing) - var initialColor = Math.floor(realStartTime/timePerBlock) % 2; - - context.fillStyle = "#000000"; - context.font = "8px sans-serif"; - for (var ii = 0; ii < blockCount+1; ii++) { - var x = Math.floor(ii*pixelsPerBlock - startPos); - - context.fillStyle = (ii+initialColor)%2 ? "#E6E6E6":"white"; - context.fillRect(x, 0, pixelsPerBlock, height); - - context.strokeStyle = "#B0B0B0"; - context.beginPath(); - context.moveTo(x, 0); - context.lineTo(x, height); - context.stroke(); - - context.fillStyle = "#000000"; - context.fillText(prettyPrintTime(ii*timePerBlock + realStartTime), x + 5, height/2 + 5); - } - - context.strokeStyle = "#525252"; - context.beginPath(); - context.moveTo(0, height-1); - context.lineTo(width, height-1); - context.stroke(); - } - - function clear() - { - requestPaint(); - } - - function prettyPrintTime( t ) - { + readonly property var timeUnits: ["μs", "ms", "s"] + function prettyPrintTime(t, rangeDuration) { var round = 1; var barrier = 1; - var units = ["μs", "ms", "s"]; - for (var i = 0; i < units.length; ++i) { + for (var i = 0; i < timeUnits.length; ++i) { barrier *= 1000; - if (zoomControl.rangeDuration < barrier) + if (rangeDuration < barrier) round *= 1000; - else if (zoomControl.rangeDuration < barrier * 10) + else if (rangeDuration < barrier * 10) round *= 100; - else if (zoomControl.rangeDuration < barrier * 100) + else if (rangeDuration < barrier * 100) round *= 10; if (t < barrier * 1000) - return Math.floor(t / (barrier / round)) / round + units[i]; + return Math.floor(t / (barrier / round)) / round + timeUnits[i]; } t /= barrier; @@ -117,4 +74,111 @@ Canvas { var s = Math.floor((t - m * 60) * round) / round; return m + "m" + s + "s"; } + + Item { + x: Math.floor(firstBlock * timeDisplay.pixelsPerBlock - timeDisplay.offsetX) + y: 0 + id: row + + property int firstBlock: timeDisplay.offsetX / timeDisplay.pixelsPerBlock + property int offset: firstBlock % repeater.model + + Repeater { + id: repeater + model: Math.floor(timeDisplay.width / timeDisplay.initialBlockLength * 2 + 2) + + Item { + id: column + + // Changing the text in text nodes is expensive. We minimize the number of changes + // by rotating the nodes during scrolling. + property int stableIndex: row.offset > index ? repeater.model - row.offset + index : + index - row.offset + height: timeDisplay.height + y: 0 + x: width * stableIndex + width: timeDisplay.pixelsPerBlock + + // Manually control this. We don't want it to happen when firstBlock + // changes before stableIndex changes. + onStableIndexChanged: block = row.firstBlock + stableIndex + property int block: -1 + property double blockStartTime: block * timeDisplay.timePerBlock + + timeDisplay.alignedWindowStart + + Rectangle { + id: timeLabel + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: timeDisplay.labelsHeight + + color: (Math.round(column.block + timeDisplay.alignedWindowStart / + timeDisplay.timePerBlock) % 2) ? + "#E6E6E6" : "white"; + + Text { + id: labelText + renderType: Text.NativeRendering + font.pixelSize: 8 + font.family: "sans-serif" + anchors.fill: parent + anchors.leftMargin: 5 + anchors.bottomMargin: 5 + verticalAlignment: Text.AlignBottom + text: prettyPrintTime(column.blockStartTime, timeDisplay.rangeDuration) + } + } + + Row { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: timeLabel.bottom + anchors.bottom: parent.bottom + + Repeater { + model: 4 + Item { + anchors.top: parent.top + anchors.bottom: parent.bottom + width: timeDisplay.pixelsPerSection + + Rectangle { + color: "#CCCCCC" + width: 1 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + } + } + } + } + + Rectangle { + color: "#B0B0B0" + width: 1 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + } + } + } + } + + Rectangle { + height: 2 + anchors.left: parent.left + anchors.right: parent.right + y: labelsHeight - 2 + color: "#B0B0B0" + } + + Rectangle { + height: 1 + anchors.left: parent.left + anchors.right: parent.right + anchors.top: row.bottom + color: "#B0B0B0" + } } diff --git a/src/plugins/qmlprofiler/qml/TimeMarks.qml b/src/plugins/qmlprofiler/qml/TimeMarks.qml index 897840596dc..7fc2bf6e43e 100644 --- a/src/plugins/qmlprofiler/qml/TimeMarks.qml +++ b/src/plugins/qmlprofiler/qml/TimeMarks.qml @@ -29,72 +29,20 @@ ****************************************************************************/ import QtQuick 2.1 -import Monitor 1.0 -Canvas { +Item { id: timeMarks - objectName: "TimeMarks" - contextType: "2d" - + visible: model && (mockup || (!model.hidden && !model.empty)) property QtObject model property bool startOdd + property bool mockup readonly property int scaleMinHeight: 60 readonly property int scaleStepping: 30 readonly property string units: " kMGT" - property real startTime - property real endTime - property real timePerPixel - Connections { - target: model - onHeightChanged: requestPaint() - } - - onStartTimeChanged: requestPaint() - onEndTimeChanged: requestPaint() - onYChanged: requestPaint() - onHeightChanged: requestPaint() - - onPaint: { - var context = (timeMarks.context === null) ? getContext("2d") : timeMarks.context; - context.reset(); - drawBackgroundBars( context, region ); - - var totalTime = endTime - startTime; - var spacing = width / totalTime; - - var initialBlockLength = 120; - var timePerBlock = Math.pow(2, Math.floor( Math.log( totalTime / width * initialBlockLength ) / Math.LN2 ) ); - var pixelsPerBlock = timePerBlock * spacing; - var pixelsPerSection = pixelsPerBlock / 5; - var blockCount = width / pixelsPerBlock; - - var realStartTime = Math.floor(startTime/timePerBlock) * timePerBlock; - var realStartPos = (startTime-realStartTime) * spacing; - - timePerPixel = timePerBlock/pixelsPerBlock; - - - for (var ii = 0; ii < blockCount+1; ii++) { - var x = Math.floor(ii*pixelsPerBlock - realStartPos); - context.strokeStyle = "#B0B0B0"; - context.beginPath(); - context.moveTo(x, 0); - context.lineTo(x, height); - context.stroke(); - - context.strokeStyle = "#CCCCCC"; - for (var jj=1; jj < 5; jj++) { - var xx = Math.floor(ii*pixelsPerBlock + jj*pixelsPerSection - realStartPos); - context.beginPath(); - context.moveTo(xx, 0); - context.lineTo(xx, height); - context.stroke(); - } - } - } + property int rowCount: model ? model.rowCount : 0 function prettyPrintScale(amount) { var unitOffset = 0; @@ -112,66 +60,88 @@ Canvas { } } - function drawBackgroundBars( context, region ) { - var colorIndex = startOdd; + Connections { + target: model + onRowHeightChanged: { + if (row >= 0) + rowRepeater.itemAt(row).height = height; + } + } - context.font = "8px sans-serif"; - // separators - var cumulatedHeight = 0; + Column { + id: rows + anchors.left: parent.left + anchors.right: parent.right + Repeater { + id: rowRepeater + model: timeMarks.rowCount + Rectangle { + id: row + color: ((index + (startOdd ? 1 : 0)) % 2) ? "#f0f0f0" : "white" + anchors.left: rows.left + anchors.right: rows.right + height: timeMarks.model ? timeMarks.model.rowHeight(index) : 0 - for (var row = 0; row < model.rowCount; ++row) { - // row background - var rowHeight = model.rowHeight(row) - cumulatedHeight += rowHeight; - colorIndex = !colorIndex; - if (cumulatedHeight < y - rowHeight) - continue; - context.strokeStyle = context.fillStyle = colorIndex ? "#f0f0f0" : "white"; - context.fillRect(0, cumulatedHeight - rowHeight - y, width, rowHeight); + property int minVal: timeMarks.model ? timeMarks.model.rowMinValue(index) : 0 + property int maxVal: timeMarks.model ? timeMarks.model.rowMaxValue(index) : 0 + property int valDiff: maxVal - minVal + property bool scaleVisible: timeMarks.model && timeMarks.model.expanded && + height > scaleMinHeight && valDiff > 0 - if (rowHeight >= scaleMinHeight) { - var minVal = model.rowMinValue(row); - var maxVal = model.rowMaxValue(row); - if (minVal !== maxVal) { - context.strokeStyle = context.fillStyle = "#B0B0B0"; - - var stepValUgly = Math.ceil((maxVal - minVal) / - Math.floor(rowHeight / scaleStepping)); - - // align to clean 2**x - var stepVal = 1; - while (stepValUgly >>= 1) - stepVal <<= 1; - - var stepHeight = rowHeight / (maxVal - minVal); - - for (var step = minVal; step <= maxVal - stepVal; step += stepVal) { - var offset = cumulatedHeight - step * stepHeight - y; - context.beginPath(); - context.moveTo(0, offset); - context.lineTo(width, offset); - context.stroke(); - context.fillText(prettyPrintScale(step), 5, offset - 2); + property int stepVal: { + var ret = 1; + var ugly = Math.ceil(valDiff / Math.floor(height / scaleStepping)); + while (isFinite(ugly) && ugly > 1) { + ugly >>= 1; + ret <<= 1; } - context.beginPath(); - context.moveTo(0, cumulatedHeight - rowHeight - y); - context.lineTo(width, cumulatedHeight - rowHeight - y); - context.stroke(); - context.fillText(prettyPrintScale(maxVal), 5, - cumulatedHeight - rowHeight - y + 8); + return ret; + } + Text { + id: scaleTopLabel + renderType: Text.NativeRendering + visible: parent.scaleVisible + color: "#B0B0B0" + font.pixelSize: 8 + anchors.top: parent.top + anchors.leftMargin: 2 + anchors.topMargin: 2 + anchors.left: parent.left + text: prettyPrintScale(row.maxVal) + } + + Repeater { + model: parent.scaleVisible ? row.valDiff / row.stepVal : 0 + + Item { + anchors.left: row.left + anchors.right: row.right + height: row.stepVal * row.height / row.valDiff + y: row.height - (index + 1) * height + visible: y > scaleTopLabel.height + Text { + renderType: Text.NativeRendering + color: "#B0B0B0" + font.pixelSize: 8 + anchors.bottom: parent.bottom + anchors.bottomMargin: 2 + anchors.leftMargin: 2 + anchors.left: parent.left + text: prettyPrintScale(index * row.stepVal) + } + + Rectangle { + height: 1 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + color: "#B0B0B0" + } + } } } - - if (cumulatedHeight > y + height) - return; } - - context.strokeStyle = "#B0B0B0"; - context.beginPath(); - context.moveTo(0, cumulatedHeight - y); - context.lineTo(width, cumulatedHeight - y); - context.stroke(); } }