From 13c5cee372548c8a3cbaba332c39433e54cbb3bf Mon Sep 17 00:00:00 2001 From: Ulf Hermann Date: Fri, 5 Dec 2014 16:00:03 +0100 Subject: [PATCH] QmlProfiler: Untangle QML code Split it up into some more files and remove all implicit cross referencing by ID. Change-Id: I3f829d6701906b1b054d18680d9e670b35b1716a Reviewed-by: Kai Koehne --- src/plugins/qmlprofiler/qml/CategoryLabel.qml | 4 +- src/plugins/qmlprofiler/qml/MainView.qml | 553 ++++++------------ src/plugins/qmlprofiler/qml/Overview.qml | 35 +- src/plugins/qmlprofiler/qml/RangeDetails.qml | 37 +- .../qmlprofiler/qml/SelectionRange.qml | 77 +-- .../qmlprofiler/qml/SelectionRangeDetails.qml | 51 +- .../qmlprofiler/qml/TimelineContent.qml | 215 +++++++ .../qmlprofiler/qml/TimelineLabels.qml | 165 ++++++ src/plugins/qmlprofiler/qml/qmlprofiler.qrc | 2 + src/plugins/qmlprofiler/qmlprofiler.pro | 2 + src/plugins/qmlprofiler/qmlprofiler.qbs | 2 + 11 files changed, 641 insertions(+), 502 deletions(-) create mode 100644 src/plugins/qmlprofiler/qml/TimelineContent.qml create mode 100644 src/plugins/qmlprofiler/qml/TimelineLabels.qml diff --git a/src/plugins/qmlprofiler/qml/CategoryLabel.qml b/src/plugins/qmlprofiler/qml/CategoryLabel.qml index 5b635964740..367c2c8730a 100644 --- a/src/plugins/qmlprofiler/qml/CategoryLabel.qml +++ b/src/plugins/qmlprofiler/qml/CategoryLabel.qml @@ -36,6 +36,7 @@ Item { id: labelContainer property QtObject model + property QtObject notesModel property bool mockup property string text: model ? model.displayName : "" property bool expanded: model && model.expanded @@ -192,9 +193,8 @@ Item { property var eventIds: [] property var texts: [] property int currentNote: -1 - property var notesModel: qmlProfilerModelProxy.notes Connections { - target: notesButton.notesModel + target: notesModel onChanged: { if (arguments[1] === -1 || arguments[1] === model.modelId) notesButton.updateNotes(); diff --git a/src/plugins/qmlprofiler/qml/MainView.qml b/src/plugins/qmlprofiler/qml/MainView.qml index e019ae091ad..4366b6bc332 100644 --- a/src/plugins/qmlprofiler/qml/MainView.qml +++ b/src/plugins/qmlprofiler/qml/MainView.qml @@ -29,33 +29,29 @@ ****************************************************************************/ import QtQuick 2.1 -import Monitor 1.0 import QtQuick.Controls 1.0 -import QtQml.Models 2.1 Rectangle { id: root // ***** properties - property bool selectionLocked : true property bool lockItemSelection : false - property real mainviewTimePerPixel: zoomControl.rangeDuration / root.width - signal updateCursorPosition property string fileName: "" property int lineNumber: -1 property int columnNumber: 0 - property int typeId: -1 property int selectedModel: -1 property int selectedItem: -1 property bool selectionRangeMode: false - property bool selectionRangeReady: selectionRange.ready property real selectionRangeStart: selectionRange.startTime property real selectionRangeEnd: selectionRange.startTime + selectionRange.duration + property int typeId: content.typeId + + onTypeIdChanged: updateCursorPosition() color: "#dcdcdc" @@ -64,10 +60,23 @@ Rectangle { target: zoomControl onRangeChanged: { zoomSliderToolBar.updateZoomLevel(); - flick.updateWindow(); + content.scroll(); + + // If you select something in the main view and then resize the current range by some + // other means, the selection should stay where it was. + var oldTimePerPixel = selectionRange.viewTimePerPixel; + selectionRange.viewTimePerPixel = zoomControl.rangeDuration / content.width; + if (selectionRange.creationState === selectionRange.creationFinished && + oldTimePerPixel != selectionRange.viewTimePerPixel) { + var newWidth = selectionRange.rangeWidth * oldTimePerPixel / + selectionRange.viewTimePerPixel; + selectionRange.rangeLeft = selectionRange.rangeLeft * oldTimePerPixel / + selectionRange.viewTimePerPixel; + selectionRange.rangeRight = selectionRange.rangeLeft + newWidth; + } } onWindowChanged: { - flick.updateWindow(); + content.scroll(); } } @@ -75,30 +84,20 @@ Rectangle { Connections { target: qmlProfilerModelProxy onDataAvailable: { - timelineView.clearChildren(); + content.clearChildren(); zoomControl.setRange(zoomControl.traceStart, zoomControl.traceStart + zoomControl.traceDuration / 10); } } + onSelectionRangeModeChanged: { + selectionRange.reset(); + buttonsBar.updateRangeButton(selectionRangeMode); + } // ***** functions - function gotoSourceLocation(file,line,column) { - if (file !== undefined) { - root.fileName = file; - root.lineNumber = line; - root.columnNumber = column; - } - } - - function recenterOnItem() { - timelineView.select(selectedModel, selectedItem); - } - function clear() { - flick.contentY = 0; - flick.contentX = 0; - timelineView.clearChildren(); + content.clearChildren(); rangeDetails.hide(); selectionRangeMode = false; zoomSlider.externalUpdate = true; @@ -106,33 +105,6 @@ Rectangle { overview.clear(); } - function propagateSelection(newModel, newItem) { - if (lockItemSelection || (newModel === selectedModel && newItem === selectedItem)) - return; - - lockItemSelection = true; - if (selectedModel !== -1 && selectedModel !== newModel) - timelineView.select(selectedModel, -1); - - selectedItem = newItem - selectedModel = newModel - if (selectedItem !== -1) { - // display details - rangeDetails.showInfo(selectedModel, selectedItem); - - // update in other views - var model = qmlProfilerModelProxy.models[selectedModel]; - var eventLocation = model.location(selectedItem); - gotoSourceLocation(eventLocation.file, eventLocation.line, - eventLocation.column); - typeId = model.typeId(selectedItem); - updateCursorPosition(); - } else { - rangeDetails.hide(); - } - lockItemSelection = false; - } - function enableButtonsBar(enable) { buttonsBar.enabled = enable; } @@ -166,152 +138,32 @@ Rectangle { if (itemIndex !== -1) { // select an item, lock to it, and recenter if necessary - timelineView.select(modelIndex, itemIndex); - root.selectionLocked = true; + content.select(modelIndex, itemIndex); + content.selectionLocked = true; } } - // ***** slots - onSelectionRangeModeChanged: { - selectionRangeControl.enabled = selectionRangeMode; - selectionRange.reset(); - buttonsBar.updateRangeButton(selectionRangeMode); - } - - onSelectionLockedChanged: { - buttonsBar.updateLockButton(selectionLocked); - } - focus: true property bool shiftPressed: false; Keys.onPressed: shiftPressed = (event.key === Qt.Key_Shift); Keys.onReleased: shiftPressed = false; - Flickable { + TimelineLabels { id: categories - flickableDirection: Flickable.VerticalFlick - interactive: false anchors.top: buttonsBar.bottom anchors.bottom: overview.top anchors.left: parent.left anchors.right: parent.right - contentY: flick.contentY + contentY: content.contentY + selectedModel: root.selectedModel + selectedItem: root.selectedItem + color: root.color + modelProxy: qmlProfilerModelProxy + zoomer: zoomControl + reverseSelect: shiftPressed - // reserve some more space than needed to prevent weird effects when resizing - 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: 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: parent.visualIndex - dragging: categories.dragging - reverseSelect: root.shiftPressed - onDragStarted: categories.dragging = true - onDragStopped: categories.dragging = false - draggerParent: categories - width: 150 - dragOffset: parent.y - - onDropped: { - timelineModel.items.move(sourceIndex, targetIndex); - labelsModel.items.move(sourceIndex, targetIndex); - } - - onSelectById: { - timelineView.select(index, eventId) - } - - onSelectNextBySelectionId: { - timelineView.select(index, modelData.nextItemBySelectionId(selectionId, - zoomControl.rangeStart, - root.selectedModel === index ? root.selectedItem : -1)); - } - - onSelectPrevBySelectionId: { - timelineView.select(index, modelData.prevItemBySelectionId(selectionId, - 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 - } - } - - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: categoryContent.bottom - height: 1 - color: "#B0B0B0" - } + onMoveCategories: content.moveCategories(sourceIndex, targetIndex) + onSelectItem: content.select(modelIndex, eventIndex) } TimeDisplay { @@ -321,7 +173,7 @@ Rectangle { anchors.right: parent.right anchors.bottom: overview.top zoomer: zoomControl - contentX: flick.contentX + contentX: content.contentX clip: true } @@ -337,217 +189,173 @@ Rectangle { onJumpToNext: { var next = qmlProfilerModelProxy.nextItem(root.selectedModel, root.selectedItem, zoomControl.rangeStart); - timelineView.select(next.model, next.item); + content.select(next.model, next.item); } onJumpToPrev: { var prev = qmlProfilerModelProxy.prevItem(root.selectedModel, root.selectedItem, zoomControl.rangeEnd); - timelineView.select(prev.model, prev.item); + content.select(prev.model, prev.item); } onRangeSelectChanged: selectionRangeMode = rangeButtonChecked(); - onLockChanged: selectionLocked = !lockButtonChecked(); + onLockChanged: content.selectionLocked = !lockButtonChecked(); + } + + TimelineContent { + id: content + anchors.left: buttonsBar.right + anchors.top: buttonsBar.bottom + anchors.bottom: overview.top + anchors.right: parent.right + selectionLocked: true + zoomer: zoomControl + modelProxy: qmlProfilerModelProxy + + onSelectionLockedChanged: { + buttonsBar.updateLockButton(selectionLocked); + } + + onPropagateSelection: { + if (lockItemSelection || (newModel === selectedModel && newItem === selectedItem)) + return; + + lockItemSelection = true; + if (selectedModel !== -1 && selectedModel !== newModel) + select(selectedModel, -1); + + selectedItem = newItem + selectedModel = newModel + if (selectedItem !== -1) { + // display details + rangeDetails.showInfo(selectedModel, selectedItem); + + // update in other views + var model = qmlProfilerModelProxy.models[selectedModel]; + var eventLocation = model.location(selectedItem); + gotoSourceLocation(eventLocation.file, eventLocation.line, + eventLocation.column); + typeId = model.typeId(selectedItem); + } else { + rangeDetails.hide(); + } + lockItemSelection = false; + } + + onGotoSourceLocation: { + if (file !== undefined) { + root.fileName = file; + root.lineNumber = line; + root.columnNumber = column; + } + } + } + + MouseArea { + id: selectionRangeControl + enabled: selectionRangeMode && + selectionRange.creationState !== selectionRange.creationFinished + anchors.right: content.right + anchors.left: buttonsBar.right + anchors.top: content.top + anchors.bottom: content.bottom + hoverEnabled: enabled + z: 2 + + onReleased: { + if (selectionRange.creationState === selectionRange.creationSecondLimit) { + content.stayInteractive = true; + selectionRange.creationState = selectionRange.creationFinished; + } + } + onPressed: { + if (selectionRange.creationState === selectionRange.creationFirstLimit) { + content.stayInteractive = false; + selectionRange.setPos(selectionRangeControl.mouseX + content.contentX); + selectionRange.creationState = selectionRange.creationSecondLimit; + } + } + onPositionChanged: { + if (selectionRange.creationState === selectionRange.creationInactive) + selectionRange.creationState = selectionRange.creationFirstLimit; + + if (selectionRangeControl.pressed || + selectionRange.creationState !== selectionRange.creationFinished) + selectionRange.setPos(selectionRangeControl.mouseX + content.contentX); + } + onCanceled: pressed() } Flickable { - id: flick - contentHeight: categoryContent.height - flickableDirection: Flickable.HorizontalAndVerticalFlick - boundsBehavior: Flickable.StopAtBounds - pixelAligned: true + flickableDirection: Flickable.HorizontalFlick + clip: true + visible: selectionRangeMode && + selectionRange.creationState !== selectionRange.creationInactive + interactive: false + x: content.x + content.flickableItem.x + y: content.y + content.flickableItem.y + height: content.flickableItem.height + width: content.flickableItem.width + contentX: content.contentX + contentWidth: content.contentWidth - // ScrollView will try to deinteractivate it. We don't want that - // as the horizontal flickable is interactive, too. We do occasionally - // switch to non-interactive ourselves, though. - property bool stayInteractive: true - onInteractiveChanged: interactive = stayInteractive - onStayInteractiveChanged: interactive = stayInteractive - - property bool recursionGuard: false - - - // Update the zoom control on srolling. - onContentXChanged: { - if (recursionGuard) - return; - - recursionGuard = true; - - var newStartTime = contentX * zoomControl.rangeDuration / scroller.width + - zoomControl.windowStart; - if (isFinite(newStartTime) && Math.abs(newStartTime - zoomControl.rangeStart) >= 1) { - var newEndTime = (contentX + scroller.width) * zoomControl.rangeDuration / - scroller.width + zoomControl.windowStart; - if (isFinite(newEndTime)) - zoomControl.setRange(newStartTime, newEndTime); - } - recursionGuard = false; - } - - // Scroll when the zoom control is updated - function updateWindow() { - if (recursionGuard || zoomControl.rangeDuration <= 0) - return; - recursionGuard = true; - - var newWidth = zoomControl.windowDuration * scroller.width / - zoomControl.rangeDuration; - if (isFinite(newWidth) && Math.abs(newWidth - contentWidth) >= 1) - contentWidth = newWidth; - - var newStartX = (zoomControl.rangeStart - zoomControl.windowStart) * scroller.width / - zoomControl.rangeDuration; - if (isFinite(newStartX) && Math.abs(newStartX - contentX) >= 1) - contentX = newStartX; - - recursionGuard = false; - } - - // ***** child items SelectionRange { id: selectionRange - visible: root.selectionRangeMode && creationState !== 0 - z: 2 + zoomer: zoomControl + visible: parent.visible + + onRangeDoubleClicked: { + zoomControl.setRange(startTime, endTime); + root.selectionRangeMode = false; + } + } - - Column { - id: timelineView - - signal clearChildren - signal select(int modelIndex, int eventIndex) - - DelegateModel { - id: timelineModel - model: qmlProfilerModelProxy.models - delegate: TimelineRenderer { - id: renderer - model: modelData - notes: qmlProfilerModelProxy.notes - zoomer: zoomControl - selectionLocked: root.selectionLocked - x: 0 - - height: modelData.height - property int visualIndex: DelegateModel.itemsIndex - - // paint "under" the vertical scrollbar, so that it always matches with the - // timemarks - width: flick.contentWidth - - Connections { - target: timelineView - onClearChildren: renderer.clearData() - onSelect: { - if (modelIndex === index || modelIndex === -1) { - renderer.selectedItem = eventIndex; - if (eventIndex !== -1) - renderer.recenter(); - } - } - } - - Connections { - target: root - onSelectionLockedChanged: { - renderer.selectionLocked = root.selectionLocked; - } - } - - onSelectionLockedChanged: { - root.selectionLocked = renderer.selectionLocked; - } - - function recenter() { - if (modelData.endTime(selectedItem) < zoomer.rangeStart || - modelData.startTime(selectedItem) > zoomer.rangeEnd) { - - var newStart = (modelData.startTime(selectedItem) + - modelData.endTime(selectedItem) - - zoomer.rangeDuration) / 2; - zoomer.setRange(Math.max(newStart, zoomer.traceStart), - Math.min(newStart + zoomer.rangeDuration, - zoomer.traceEnd)); - } - - var row = modelData.row(selectedItem); - var rowStart = modelData.rowOffset(row) + y; - var rowEnd = rowStart + modelData.rowHeight(row); - if (rowStart < flick.contentY || rowEnd - scroller.height > flick.contentY) - flick.contentY = (rowStart + rowEnd - scroller.height) / 2; - } - - onSelectedItemChanged: { - root.propagateSelection(index, selectedItem); - } - - onItemPressed: { - if (pressedItem === -1) { - // User clicked on empty space. Remove selection. - root.propagateSelection(-1, -1); - } else { - var location = model.location(pressedItem); - if (location.hasOwnProperty("file")) // not empty - root.gotoSourceLocation(location.file, location.line, location.column); - root.typeId = model.typeId(pressedItem); - root.updateCursorPosition(); - } - } - } - } - - Repeater { - id: repeater - model: timelineModel - } - } - MouseArea { - id: selectionRangeControl - enabled: false - width: flick.width - height: flick.height - x: flick.contentX - y: flick.contentY - hoverEnabled: enabled - z: 2 - - onReleased: { - selectionRange.releasedOnCreation(); - } - onPressed: { - selectionRange.pressedOnCreation(); - } - onCanceled: { - selectionRange.releasedOnCreation(); - } - onPositionChanged: { - selectionRange.movedOnCreation(); - } - } - } - - ScrollView { - id: scroller - contentItem: flick - anchors.left: buttonsBar.right - anchors.top: categories.top - anchors.bottom: overview.top - anchors.right: parent.right } SelectionRangeDetails { + z: 1 + x: 200 + y: 125 + id: selectionRangeDetails visible: selectionRange.visible - startTime: selectionRange.startTimeString - duration: selectionRange.durationString - endTime: selectionRange.endTimeString + startTime: selectionRange.startTime + duration: selectionRange.duration + endTime: selectionRange.endTime showDuration: selectionRange.rangeWidth > 1 + + onRecenter: { + if ((selectionRange.startTime < zoomControl.rangeStart) ^ + (selectionRange.endTime > zoomControl.rangeEnd)) { + var center = selectionRange.startTime + selectionRange.duration / 2; + var halfDuration = Math.max(selectionRange.duration, zoomControl.rangeDuration / 2); + zoomControl.setRange(center - halfDuration, center + halfDuration); + } + } + + onClose: selectionRangeMode = false; } RangeDetails { id: rangeDetails - property alias locked: root.selectionLocked + + z: 1 + visible: false + x: 200 + y: 25 + + property alias locked: content.selectionLocked models: qmlProfilerModelProxy.models notes: qmlProfilerModelProxy.notes + onRecenterOnItem: { + content.gotoSourceLocation(file, line, column); + content.select(selectedModel, selectedItem) + } + onToggleSelectionLocked: { + content.selectionLocked = !content.selectionLocked; + } + onClearSelection: { + content.propagateSelection(-1, -1); + } } Rectangle { @@ -591,7 +399,7 @@ Rectangle { color: "#9b9b9b" enabled: buttonsBar.enabled visible: false - width: categoryContent.width + width: buttonsBar.width height: buttonsBar.height anchors.left: parent.left anchors.top: buttonsBar.bottom @@ -603,7 +411,6 @@ Rectangle { 1 / zoomSlider.exponent) * zoomSlider.maximumValue; } - Slider { id: zoomSlider anchors.fill: parent @@ -651,5 +458,7 @@ Rectangle { anchors.bottom: parent.bottom anchors.right: parent.right anchors.left: parent.left + modelProxy: qmlProfilerModelProxy + zoomer: zoomControl } } diff --git a/src/plugins/qmlprofiler/qml/Overview.qml b/src/plugins/qmlprofiler/qml/Overview.qml index d1db5814729..3fd01825d0b 100644 --- a/src/plugins/qmlprofiler/qml/Overview.qml +++ b/src/plugins/qmlprofiler/qml/Overview.qml @@ -37,12 +37,15 @@ Canvas { objectName: "Overview" contextType: "2d" + property QtObject modelProxy + property QtObject zoomer + readonly property int eventsPerPass: 512 property int increment: -1 property int offset: -1 readonly property int bump: 10; - readonly property int blockHeight: (height - bump) / qmlProfilerModelProxy.models.length; - readonly property double spacing: width / zoomControl.traceDuration + readonly property int blockHeight: (height - bump) / modelProxy.models.length; + readonly property double spacing: width / zoomer.traceDuration // ***** properties height: 50 @@ -64,12 +67,12 @@ Canvas { if (recursionGuard) return; recursionGuard = true; - var newStartTime = rangeMover.rangeLeft * zoomControl.traceDuration / width + - zoomControl.traceStart; - var newEndTime = rangeMover.rangeRight * zoomControl.traceDuration / width + - zoomControl.traceStart; + var newStartTime = rangeMover.rangeLeft * zoomer.traceDuration / width + + zoomer.traceStart; + var newEndTime = rangeMover.rangeRight * zoomer.traceDuration / width + + zoomer.traceStart; if (isFinite(newStartTime) && isFinite(newEndTime) && newEndTime - newStartTime > 500) - zoomControl.setRange(newStartTime, newEndTime); + zoomer.setRange(newStartTime, newEndTime); recursionGuard = false; } @@ -79,14 +82,14 @@ Canvas { // ***** connections to external objects Connections { - target: zoomControl + target: zoomer onRangeChanged: { if (recursionGuard) return; recursionGuard = true; - var newRangeX = (zoomControl.rangeStart - zoomControl.traceStart) * width / - zoomControl.traceDuration; - var newWidth = zoomControl.rangeDuration * width / zoomControl.traceDuration; + var newRangeX = (zoomer.rangeStart - zoomer.traceStart) * width / + zoomer.traceDuration; + var newWidth = zoomer.rangeDuration * width / zoomer.traceDuration; var widthChanged = Math.abs(newWidth - rangeMover.rangeWidth) > 1; var leftChanged = Math.abs(newRangeX - rangeMover.rangeLeft) > 1; if (leftChanged) @@ -99,11 +102,11 @@ Canvas { } Connections { - target: qmlProfilerModelProxy + target: modelProxy onDataAvailable: { dataReady = true; increment = 0; - var models = qmlProfilerModelProxy.models; + var models = modelProxy.models; for (var i = 0; i < models.length; ++i) increment += models[i].count; increment = Math.ceil(increment / eventsPerPass); @@ -125,9 +128,9 @@ Canvas { onPaint: { var context = (canvas.context === null) ? getContext("2d") : canvas.context; - Plotter.models = qmlProfilerModelProxy.models; - Plotter.zoomControl = zoomControl; - Plotter.notes = qmlProfilerModelProxy.notes; + Plotter.models = modelProxy.models; + Plotter.zoomControl = zoomer; + Plotter.notes = modelProxy.notes; if (offset < 0) { context.reset(); diff --git a/src/plugins/qmlprofiler/qml/RangeDetails.qml b/src/plugins/qmlprofiler/qml/RangeDetails.qml index 5719bcd248a..7bab74a0cfe 100644 --- a/src/plugins/qmlprofiler/qml/RangeDetails.qml +++ b/src/plugins/qmlprofiler/qml/RangeDetails.qml @@ -50,12 +50,12 @@ Item { property var models property var notes + signal recenterOnItem + signal toggleSelectionLocked + signal clearSelection + width: col.width + 25 height: col.height + 30 - z: 1 - visible: false - x: 200 - y: 25 function hide() { noteEdit.focus = false; @@ -70,9 +70,9 @@ Item { isBindingLoop = false; } - // keep inside view Connections { - target: root + target: rangeDetails.parent + // keep inside view onWidthChanged: fitInView(); onHeightChanged: fitInView(); } @@ -119,15 +119,15 @@ Item { function fitInView() { // don't reposition if it does not fit - if (root.width < width || root.height < height) + if (parent.width < width || parent.height < height) return; - if (x + width > root.width) - x = root.width - width; + if (x + width > parent.width) + x = parent.width - width; if (x < 0) x = 0; - if (y + height > root.height) - y = root.height - height; + if (y + height > parent.height) + y = parent.height - height; if (y < 0) y = 0; } @@ -249,13 +249,10 @@ Item { anchors.fill: parent drag.target: parent drag.minimumX: 0 - drag.maximumX: root.width - parent.width + drag.maximumX: rangeDetails.parent.width - rangeDetails.width drag.minimumY: 0 - drag.maximumY: root.height - parent.height - onClicked: { - root.gotoSourceLocation(file, line, column); - root.recenterOnItem(); - } + drag.maximumY: rangeDetails.parent.height - rangeDetails.height + onClicked: rangeDetails.recenterOnItem() } Image { @@ -282,9 +279,7 @@ Item { height: 12 MouseArea { anchors.fill: parent - onClicked: { - root.selectionLocked = !root.selectionLocked; - } + onClicked: rangeDetails.toggleSelectionLocked() } } @@ -298,7 +293,7 @@ Item { renderType: Text.NativeRendering MouseArea { anchors.fill: parent - onClicked: root.propagateSelection(-1, -1); + onClicked: rangeDetails.clearSelection() } } diff --git a/src/plugins/qmlprofiler/qml/SelectionRange.qml b/src/plugins/qmlprofiler/qml/SelectionRange.qml index 84a5d389fe1..5e05f15ec42 100644 --- a/src/plugins/qmlprofiler/qml/SelectionRange.qml +++ b/src/plugins/qmlprofiler/qml/SelectionRange.qml @@ -32,42 +32,26 @@ import QtQuick 2.1 RangeMover { id: selectionRange + property QtObject zoomer; - property bool ready: visible && creationState === 3 + readonly property int creationInactive: 0 + readonly property int creationFirstLimit: 1 + readonly property int creationSecondLimit: 2 + readonly property int creationFinished: 3 - property string startTimeString: detailedPrintTime(startTime) - property string endTimeString: detailedPrintTime(endTime) - property string durationString: detailedPrintTime(duration) + property bool ready: visible && creationState === creationFinished - property double startTime: rangeLeft * viewTimePerPixel + zoomControl.windowStart + property double startTime: rangeLeft * viewTimePerPixel + zoomer.windowStart property double duration: Math.max(rangeWidth * viewTimePerPixel, 500) property double endTime: startTime + duration property double viewTimePerPixel: 1 property double creationReference : 0 - property int creationState : 0 - - Connections { - target: zoomControl - onRangeChanged: { - var oldTimePerPixel = selectionRange.viewTimePerPixel; - selectionRange.viewTimePerPixel = zoomControl.rangeDuration / scroller.width; - if (creationState === 3 && oldTimePerPixel != selectionRange.viewTimePerPixel) { - var newWidth = rangeWidth * oldTimePerPixel / viewTimePerPixel; - rangeLeft = rangeLeft * oldTimePerPixel / viewTimePerPixel; - rangeRight = rangeLeft + newWidth; - } - } - } - - onRangeDoubleClicked: { - zoomControl.setRange(startTime, endTime); - root.selectionRangeMode = false; - } + property int creationState : creationInactive function reset() { rangeRight = rangeLeft + 1; - creationState = 0; + creationState = creationInactive; creationReference = 0; } @@ -78,12 +62,12 @@ RangeMover { pos = width; switch (creationState) { - case 1: + case creationFirstLimit: creationReference = pos; rangeLeft = pos; rangeRight = pos + 1; break; - case 2: + case creationSecondLimit: if (pos > creationReference) { rangeLeft = creationReference; rangeRight = pos; @@ -94,43 +78,4 @@ RangeMover { break; } } - - - function detailedPrintTime( t ) - { - if (t <= 0) return "0"; - if (t<1000) return t+" ns"; - t = Math.floor(t/1000); - if (t<1000) return t+" μs"; - if (t<1e6) return (t/1000) + " ms"; - return (t/1e6) + " s"; - } - - // creation control - function releasedOnCreation() { - if (selectionRange.creationState === 2) { - flick.stayInteractive = true; - selectionRange.creationState = 3; - selectionRangeControl.enabled = false; - } - } - - function pressedOnCreation() { - if (selectionRange.creationState === 1) { - flick.stayInteractive = false; - selectionRange.setPos(selectionRangeControl.mouseX + flick.contentX); - selectionRange.creationState = 2; - } - } - - function movedOnCreation() { - if (selectionRange.creationState === 0) { - selectionRange.creationState = 1; - } - - if (!selectionRangeControl.pressed && selectionRange.creationState==3) - return; - - selectionRange.setPos(selectionRangeControl.mouseX + flick.contentX); - } } diff --git a/src/plugins/qmlprofiler/qml/SelectionRangeDetails.qml b/src/plugins/qmlprofiler/qml/SelectionRangeDetails.qml index ff7f903c3b5..9e8dfcff093 100644 --- a/src/plugins/qmlprofiler/qml/SelectionRangeDetails.qml +++ b/src/plugins/qmlprofiler/qml/SelectionRangeDetails.qml @@ -34,6 +34,9 @@ import Monitor 1.0 Item { id: selectionRangeDetails + signal recenter + signal close + property string startTime property string endTime property string duration @@ -41,29 +44,35 @@ Item { width: Math.max(150, col.width + 25) height: col.height + 30 - z: 1 - visible: false - x: 200 - y: 125 // keep inside view Connections { - target: root + target: selectionRangeDetails.parent onWidthChanged: fitInView(); onHeightChanged: fitInView(); } + function detailedPrintTime( t ) + { + if (t <= 0) return "0"; + if (t<1000) return t+" ns"; + t = Math.floor(t/1000); + if (t<1000) return t+" μs"; + if (t<1e6) return (t/1000) + " ms"; + return (t/1e6) + " s"; + } + function fitInView() { // don't reposition if it does not fit - if (root.width < width || root.height < height) + if (parent.width < width || parent.height < height) return; - if (x + width > root.width) - x = root.width - width; + if (x + width > parent.width) + x = parent.width - width; if (x < 0) x = 0; - if (y + height > root.height) - y = root.height - height; + if (y + height > parent.height) + y = parent.height - height; if (y < 0) y = 0; } @@ -138,11 +147,11 @@ Item { Repeater { model: [ qsTr("Start") + ":", - startTime, + detailedPrintTime(startTime), showDuration ? (qsTr("End") + ":") : "", - showDuration ? endTime : "", + showDuration ? detailedPrintTime(endTime) : "", showDuration ? (qsTr("Duration") + ":") : "", - showDuration ? duration : "" + showDuration ? detailedPrintTime(duration) : "" ] Detail { isLabel: index % 2 === 0 @@ -156,17 +165,10 @@ Item { anchors.fill: parent drag.target: parent drag.minimumX: 0 - drag.maximumX: root.width - parent.width + drag.maximumX: selectionRangeDetails.parent.width - width drag.minimumY: 0 - drag.maximumY: root.height - parent.height + 0 - onClicked: { - if ((selectionRange.startTime < zoomControl.rangeStart) ^ - (selectionRange.endTime > zoomControl.rangeEnd)) { - var center = selectionRange.startTime + selectionRange.duration / 2; - var halfDuration = Math.max(selectionRange.duration, zoomControl.rangeDuration / 2); - zoomControl.setRange(center - halfDuration, center + halfDuration); - } - } + drag.maximumY: selectionRangeDetails.parent.height - height + onClicked: selectionRangeDetails.recenter() } Text { @@ -179,8 +181,7 @@ Item { MouseArea { anchors.fill: parent anchors.leftMargin: -8 - onClicked: root.selectionRangeMode = false; + onClicked: selectionRangeDetails.close() } } - } diff --git a/src/plugins/qmlprofiler/qml/TimelineContent.qml b/src/plugins/qmlprofiler/qml/TimelineContent.qml new file mode 100644 index 00000000000..39593f9a454 --- /dev/null +++ b/src/plugins/qmlprofiler/qml/TimelineContent.qml @@ -0,0 +1,215 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://www.qt.io/licensing. For further information +** use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + +import QtQuick 2.0 +import QtQuick.Controls 1.2 +import Monitor 1.0 +import QtQml.Models 2.1 + +ScrollView { + id: scroller + + property QtObject zoomer + property QtObject modelProxy + + property int contentX: flick.contentX + property int contentY: flick.contentY + property int contentWidth: flick.contentWidth + property int contentHeight: flick.contentHeight + property bool selectionLocked + property int typeId + + signal propagateSelection(int newModel, int newItem) + signal gotoSourceLocation(string file, int line, int column) + + function clearChildren() + { + timelineView.clearChildren(); + } + + function select(modelIndex, eventIndex) + { + timelineView.select(modelIndex, eventIndex); + } + + function moveCategories(sourceIndex, targetIndex) + { + timelineModel.items.move(sourceIndex, targetIndex) + } + + function scroll() + { + flick.scroll(); + } + + // ScrollView will try to deinteractivate it. We don't want that + // as the horizontal flickable is interactive, too. We do occasionally + // switch to non-interactive ourselves, though. + property bool stayInteractive: true + onStayInteractiveChanged: flick.interactive = stayInteractive + + Flickable { + id: flick + contentHeight: timelineView.height + height + flickableDirection: Flickable.HorizontalAndVerticalFlick + boundsBehavior: Flickable.StopAtBounds + pixelAligned: true + + onInteractiveChanged: interactive = stayInteractive + + property bool recursionGuard: false + + // Update the zoom control on srolling. + onContentXChanged: { + if (recursionGuard) + return; + + recursionGuard = true; + + var newStartTime = contentX * zoomer.rangeDuration / scroller.width + + zoomer.windowStart; + if (isFinite(newStartTime) && Math.abs(newStartTime - zoomer.rangeStart) >= 1) { + var newEndTime = (contentX + scroller.width) * zoomer.rangeDuration / + scroller.width + zoomer.windowStart; + if (isFinite(newEndTime)) + zoomer.setRange(newStartTime, newEndTime); + } + recursionGuard = false; + } + + // Scroll when the zoom control is updated + function scroll() { + if (recursionGuard || zoomer.rangeDuration <= 0) + return; + recursionGuard = true; + + var newWidth = zoomer.windowDuration * scroller.width / zoomer.rangeDuration; + if (isFinite(newWidth) && Math.abs(newWidth - contentWidth) >= 1) + contentWidth = newWidth; + + var newStartX = (zoomer.rangeStart - zoomer.windowStart) * scroller.width / + zoomer.rangeDuration; + if (isFinite(newStartX) && Math.abs(newStartX - contentX) >= 1) + contentX = newStartX; + + recursionGuard = false; + } + + Column { + id: timelineView + + signal clearChildren + signal select(int modelIndex, int eventIndex) + + DelegateModel { + id: timelineModel + model: modelProxy.models + delegate: TimelineRenderer { + id: renderer + model: modelData + notes: modelProxy.notes + zoomer: scroller.zoomer + selectionLocked: scroller.selectionLocked + x: 0 + height: modelData.height + property int visualIndex: DelegateModel.itemsIndex + + // paint "under" the vertical scrollbar, so that it always matches with the + // timemarks + width: flick.contentWidth + + Connections { + target: timelineView + onClearChildren: renderer.clearData() + onSelect: { + if (modelIndex === index || modelIndex === -1) { + renderer.selectedItem = eventIndex; + if (eventIndex !== -1) + renderer.recenter(); + } + } + } + + Connections { + target: scroller + onSelectionLockedChanged: { + renderer.selectionLocked = scroller.selectionLocked; + } + } + + onSelectionLockedChanged: { + scroller.selectionLocked = renderer.selectionLocked; + } + + function recenter() { + if (modelData.endTime(selectedItem) < zoomer.rangeStart || + modelData.startTime(selectedItem) > zoomer.rangeEnd) { + + var newStart = (modelData.startTime(selectedItem) + + modelData.endTime(selectedItem) - + zoomer.rangeDuration) / 2; + zoomer.setRange(Math.max(newStart, zoomer.traceStart), + Math.min(newStart + zoomer.rangeDuration, + zoomer.traceEnd)); + } + + var row = modelData.row(selectedItem); + var rowStart = modelData.rowOffset(row) + y; + var rowEnd = rowStart + modelData.rowHeight(row); + if (rowStart < flick.contentY || rowEnd - scroller.height > flick.contentY) + flick.contentY = (rowStart + rowEnd - scroller.height) / 2; + } + + onSelectedItemChanged: { + scroller.propagateSelection(index, selectedItem); + } + + onItemPressed: { + if (pressedItem === -1) { + // User clicked on empty space. Remove selection. + scroller.propagateSelection(-1, -1); + } else { + var location = model.location(pressedItem); + if (location.hasOwnProperty("file")) // not empty + scroller.gotoSourceLocation(location.file, location.line, + location.column); + scroller.typeId = model.typeId(pressedItem); + } + } + } + } + + Repeater { + id: repeater + model: timelineModel + } + } + } +} diff --git a/src/plugins/qmlprofiler/qml/TimelineLabels.qml b/src/plugins/qmlprofiler/qml/TimelineLabels.qml new file mode 100644 index 00000000000..34f0accad3b --- /dev/null +++ b/src/plugins/qmlprofiler/qml/TimelineLabels.qml @@ -0,0 +1,165 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://www.qt.io/licensing. For further information +** use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + +import QtQuick 2.0 +import QtQml.Models 2.1 + +Flickable { + id: categories + flickableDirection: Flickable.VerticalFlick + interactive: false + property color color + + property int selectedModel + property int selectedItem + property bool reverseSelect + property QtObject modelProxy + property QtObject zoomer + + signal selectItem(int modelIndex, int eventIndex) + signal moveCategories(int sourceIndex, int targetIndex) + + // reserve some more space than needed to prevent weird effects when resizing + 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(modelProxy.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: modelProxy.models + delegate: Rectangle { + color: categories.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: modelProxy.height === 0 + notesModel: modelProxy.notes + visualIndex: parent.visualIndex + dragging: categories.dragging + reverseSelect: categories.reverseSelect + onDragStarted: categories.dragging = true + onDragStopped: categories.dragging = false + draggerParent: categories + width: 150 + dragOffset: parent.y + + onDropped: { + categories.moveCategories(sourceIndex, targetIndex); + labelsModel.items.move(sourceIndex, targetIndex); + } + + onSelectById: { + categories.selectItem(index, eventId) + } + + onSelectNextBySelectionId: { + categories.selectItem(index, modelData.nextItemBySelectionId(selectionId, + zoomer.rangeStart, + categories.selectedModel === index ? categories.selectedItem : -1)); + } + + onSelectPrevBySelectionId: { + categories.selectItem(index, modelData.prevItemBySelectionId(selectionId, + zoomer.rangeStart, + categories.selectedModel === index ? categories.selectedItem : -1)); + } + } + + TimeMarks { + id: timeMarks + model: modelData + mockup: modelProxy.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 + } + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: categoryContent.bottom + height: 1 + color: "#B0B0B0" + } +} diff --git a/src/plugins/qmlprofiler/qml/qmlprofiler.qrc b/src/plugins/qmlprofiler/qml/qmlprofiler.qrc index 70c6dc13549..7eb625495c0 100644 --- a/src/plugins/qmlprofiler/qml/qmlprofiler.qrc +++ b/src/plugins/qmlprofiler/qml/qmlprofiler.qrc @@ -36,5 +36,7 @@ bindingloops.frag notes.vert notes.frag + TimelineLabels.qml + TimelineContent.qml diff --git a/src/plugins/qmlprofiler/qmlprofiler.pro b/src/plugins/qmlprofiler/qmlprofiler.pro index c96ce91d51c..75e90ce924f 100644 --- a/src/plugins/qmlprofiler/qmlprofiler.pro +++ b/src/plugins/qmlprofiler/qmlprofiler.pro @@ -96,6 +96,8 @@ DISTFILES += \ qml/RangeDetails.qml \ qml/RangeMover.qml \ qml/TimeDisplay.qml \ + qml/TimelineContent.qml \ + qml/TimelineLabels.qml \ qml/TimeMarks.qml \ qml/SelectionRange.qml \ qml/SelectionRangeDetails.qml \ diff --git a/src/plugins/qmlprofiler/qmlprofiler.qbs b/src/plugins/qmlprofiler/qmlprofiler.qbs index f7c67c847a4..c309383a92d 100644 --- a/src/plugins/qmlprofiler/qmlprofiler.qbs +++ b/src/plugins/qmlprofiler/qmlprofiler.qbs @@ -77,6 +77,8 @@ QtcPlugin { "SelectionRange.qml", "SelectionRangeDetails.qml", "TimeDisplay.qml", + "TimelineContent.qml", + "TimelineLabels.qml", "TimeMarks.qml", "qmlprofiler.qrc",