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(); } }