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 <kai.koehne@theqtcompany.com>
This commit is contained in:
Ulf Hermann
2014-11-07 18:13:31 +01:00
parent 75e4bd7900
commit bc4586ffbe
4 changed files with 307 additions and 259 deletions

View File

@@ -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

View File

@@ -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
Rectangle {
id: labels
anchors.left: parent.left
width: 150
color: root.color
height: col.height
Column {
id: col
contentHeight: categoryContent.height + height
// 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
anchors.right: parent.right
DelegateModel {
id: labelsModel
// 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)
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: CategoryLabel {
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,8 +268,36 @@ 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
}
}
}
@@ -260,17 +305,25 @@ Rectangle {
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

View File

@@ -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"
}
}

View File

@@ -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;
context.font = "8px sans-serif";
// separators
var cumulatedHeight = 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);
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);
}
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);
Connections {
target: model
onRowHeightChanged: {
if (row >= 0)
rowRepeater.itemAt(row).height = height;
}
}
if (cumulatedHeight > y + height)
return;
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
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
property int stepVal: {
var ret = 1;
var ugly = Math.ceil(valDiff / Math.floor(height / scaleStepping));
while (isFinite(ugly) && ugly > 1) {
ugly >>= 1;
ret <<= 1;
}
return ret;
}
context.strokeStyle = "#B0B0B0";
context.beginPath();
context.moveTo(0, cumulatedHeight - y);
context.lineTo(width, cumulatedHeight - y);
context.stroke();
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"
}
}
}
}
}
}
}