feat(MonitoringTool): Inspect arbitrary model found by objectName

Closes: #14971
This commit is contained in:
Michał Cieślak 2024-05-29 13:10:19 +02:00 committed by Michał
parent 2c1806434a
commit 80eaf6ba89
4 changed files with 582 additions and 436 deletions

View File

@ -0,0 +1,265 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Monitoring 1.0
Pane {
property string name
property var model
readonly property var rootModel: model
property bool showControls: true
readonly property var roles: model ? Monitor.modelRoles(model) : []
readonly property var rolesModelContent: roles.map(role => ({
visible: true,
name: role.name,
width: Math.ceil(fontMetrics.advanceWidth(` ${role.name} `))
}))
onRolesModelContentChanged: {
rolesModel.clear()
rolesModel.append(rolesModelContent)
}
property int columnsTotalWidth:
rolesModelContent.reduce((a, x) => a + x.width, 0)
ListModel {
id: rolesModel
Component.onCompleted: {
clear()
append(rolesModelContent)
}
}
Control {
id: helperControl
font.bold: true
}
FontMetrics {
id: fontMetrics
font.bold: true
}
ColumnLayout {
anchors.fill: parent
RowLayout {
Layout.fillWidth: true
visible: showControls
RoundButton {
text: "⬅️"
onClicked: {
inspectionStackView.pop(StackView.Immediate)
}
}
TextInput {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
text: name
font.pixelSize: 20
font.bold: true
selectByMouse: true
readOnly: true
}
}
MenuSeparator {
Layout.fillWidth: true
}
Label {
visible: showControls
text: "Hint: use right/left button click on a column " +
"header to adjust width, press cell content to " +
"see full value"
}
Label {
text: model ? `rows count: ${model.rowCount()}` : ""
font.bold: true
}
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
ScrollBar.vertical: ScrollBar {}
ScrollBar.horizontal: ScrollBar {}
clip: true
contentWidth: columnsTotalWidth
flickableDirection: Flickable.AutoFlickDirection
model: rootModel
delegate: Rectangle {
implicitWidth: flow.implicitWidth
implicitHeight: flow.implicitHeight
readonly property var topModel: model
Row {
id: flow
Repeater {
model: rolesModel
Label {
id: label
width: model.width
height: implicitHeight * 1.2
text: {
const value = topModel[model.name]
if (value === undefined || value === null)
return ""
const isModel = Monitor.isModel(value)
let text = value.toString()
if (isModel) {
text += " (" + value.rowCount() + ")"
}
return text
}
elide: Text.ElideRight
maximumLineCount: 1
verticalAlignment: Text.AlignVCenter
leftPadding: 2
rightPadding: 1
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: 1
color: "gray"
}
Rectangle {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
width: 1
color: "gray"
}
MouseArea {
id: labelMouseArea
anchors.fill: parent
onClicked: {
const value = topModel[model.name]
const isModel = Monitor.isModel(value)
if (isModel)
loader.active = true
}
}
Loader {
id: loader
active: false
sourceComponent: ApplicationWindow {
width: 500
height: 400
visible: true
onClosing: loader.active = false
Loader {
anchors.fill: parent
sourceComponent: modelInspectionComponent
Component.onCompleted: {
item.showControls = false
item.model = topModel[model.name]
}
}
}
}
ToolTip.visible: labelMouseArea.pressed
ToolTip.text: label.text
}
}
}
}
headerPositioning: ListView.OverlayHeader
header: Item {
implicitWidth: headerFlow.implicitWidth
implicitHeight: headerFlow.implicitHeight * 1.5
z: 2
Rectangle {
color: "whitesmoke"
anchors.fill: parent
border.color: "gray"
}
Row {
id: headerFlow
anchors.verticalCenter: parent.verticalCenter
Repeater {
model: rolesModel
Label {
text: ` ${model.name} `
font.bold: true
width: model.width
elide: Text.ElideRight
maximumLineCount: 1
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
const factor = 1.5
const oldWidth = model.width
const leftBtn = mouse.button === Qt.LeftButton
const newWidth
= Math.ceil(leftBtn ? oldWidth * factor : oldWidth / factor)
model.width = newWidth
columnsTotalWidth += newWidth - oldWidth
}
}
}
}
}
}
}
}
}

View File

@ -1,462 +1,97 @@
import QtQuick 2.14 import QtQuick 2.15
import QtQuick.Controls 2.14 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.14 import QtQuick.Layouts 1.15
import Monitoring 1.0 import Monitoring 1.0
import Qt.labs.settings 1.0
import AppLayouts.Wallet.stores 1.0 as WalletStores
import shared.stores 1.0 as SharedStores
Component { Component {
SplitView {
id: root
ColumnLayout { ColumnLayout {
SplitView.fillHeight: true
SplitView.preferredWidth: 450
spacing: 5 spacing: 0
Label { Settings {
Layout.fillWidth: true property alias tabIndex: tabBar.currentIndex
Layout.margins: 5 property alias modelObjectName: objectNameTextFiled.text
property alias modelObjectRootName: rootTextField.text
text: "Context properties:"
font.bold: true
}
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.margins: 5
model: Monitor.contexPropertiesModel
clip: true
spacing: 5
delegate: Item {
implicitWidth: delegateRow.implicitWidth
implicitHeight: delegateRow.implicitHeight
readonly property var contextPropertyValue:
MonitorUtils.contextPropertyBindingHelper(name, root).value
Row {
id: delegateRow
Label {
text: name
}
Label {
text: ` [${MonitorUtils.typeName(contextPropertyValue)}]`
color: "darkgreen"
}
Label {
text: ` (${MonitorUtils.valueToString(contextPropertyValue)})`
color: "darkred"
}
}
MouseArea {
anchors.fill: parent
onClicked: {
inspectionStackView.clear()
const props = {
name: name,
objectForInspection: contextPropertyValue
}
inspectionStackView.push(inspectionList, props)
}
}
}
}
} }
Component { TabBar {
id: modelInspectionComponent id: tabBar
Layout.fillWidth: true
Layout.bottomMargin: 10
Pane { TabButton {
property string name text: "Context properties inspection"
property var model }
readonly property var rootModel: model TabButton {
text: "Models inspection"
}
property bool showControls: true currentIndex: swipeView.currentIndex
}
readonly property var roles: Monitor.modelRoles(model) StackLayout {
id: swipeView
readonly property var rolesModelContent: roles.map(role => ({ currentIndex: tabBar.currentIndex
visible: true, //anchors.fill: parent
name: role.name, Layout.fillWidth: true
width: Math.ceil(fontMetrics.advanceWidth(` ${role.name} `)) Layout.fillHeight: true
}))
onRolesModelContentChanged: { SplitView {
rolesModel.clear() id: root
rolesModel.append(rolesModelContent)
}
property int columnsTotalWidth:
rolesModelContent.reduce((a, x) => a + x.width, 0)
ListModel {
id: rolesModel
Component.onCompleted: {
clear()
append(rolesModelContent)
}
}
Control {
id: helperControl
font.bold: true
}
FontMetrics {
id: fontMetrics
font.bold: true
}
ColumnLayout { ColumnLayout {
anchors.fill: parent SplitView.fillHeight: true
SplitView.preferredWidth: 450
RowLayout { spacing: 5
Layout.fillWidth: true
visible: showControls
RoundButton {
text: "⬅️"
onClicked: {
inspectionStackView.pop(StackView.Immediate)
}
}
TextInput {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
text: name
font.pixelSize: 20
font.bold: true
selectByMouse: true
readOnly: true
}
}
MenuSeparator {
Layout.fillWidth: true
}
Label { Label {
visible: showControls Layout.fillWidth: true
Layout.margins: 5
text: "Hint: use right/left button click on a column " + text: "Context properties:"
"header to ajust width, press cell content to " +
"see full value"
}
Label {
text: `rows count: ${model.rowCount()}`
font.bold: true font.bold: true
} }
ListView { ListView {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Layout.margins: 5
model: Monitor.contexPropertiesModel
clip: true clip: true
contentWidth: columnsTotalWidth
flickableDirection: Flickable.AutoFlickDirection
model: rootModel
delegate: Rectangle {
implicitWidth: flow.implicitWidth
implicitHeight: flow.implicitHeight
readonly property var topModel: model
Row {
id: flow
Repeater {
model: rolesModel
Label {
id: label
width: model.width
height: implicitHeight * 1.2
text: {
const value = topModel[model.name]
const isModel = Monitor.isModel(value)
let text = value.toString()
if (isModel) {
text += " (" + value.rowCount() + ")"
}
return text
}
elide: Text.ElideRight
maximumLineCount: 1
verticalAlignment: Text.AlignVCenter
leftPadding: 2
rightPadding: 1
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: 1
color: "gray"
}
Rectangle {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
width: 1
color: "gray"
}
MouseArea {
id: labelMouseArea
anchors.fill: parent
onClicked: {
const value = topModel[model.name]
const isModel = Monitor.isModel(value)
if (isModel)
loader.active = true
}
}
Loader {
id: loader
active: false
sourceComponent: ApplicationWindow {
width: 500
height: 400
visible: true
onClosing: loader.active = false
Loader {
anchors.fill: parent
sourceComponent: modelInspectionComponent
Component.onCompleted: {
item.showControls = false
item.model = topModel[model.name]
}
}
}
}
ToolTip.visible: labelMouseArea.pressed
ToolTip.text: label.text
}
}
}
}
headerPositioning: ListView.OverlayHeader
header: Item {
implicitWidth: headerFlow.implicitWidth
implicitHeight: headerFlow.implicitHeight * 1.5
z: 2
Rectangle {
color: "whitesmoke"
anchors.fill: parent
border.color: "gray"
}
Row {
id: headerFlow
anchors.verticalCenter: parent.verticalCenter
Repeater {
model: rolesModel
Label {
text: ` ${model.name} `
font.bold: true
width: model.width
elide: Text.ElideRight
maximumLineCount: 1
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
const factor = 1.5
const oldWidth = model.width
const leftBtn = mouse.button === Qt.LeftButton
const newWidth
= Math.ceil(leftBtn ? oldWidth * factor : oldWidth / factor)
model.width = newWidth
columnsTotalWidth += newWidth - oldWidth
}
}
}
}
}
}
}
}
}
}
Component {
id: inspectionList
Pane {
id: inspectionPanel
property var objectForInspection
property string name
onObjectForInspectionChanged: {
inspectionModel.clear()
if (!objectForInspection)
return
const items = []
for (const property in objectForInspection) {
const type = typeof objectForInspection[property]
if (type === "function") {
items.push({
name: property,
category: "functions",
isModel: false,
type: type
})
} else {
const value = objectForInspection[property]
const detailedType = MonitorUtils.typeName(value)
const isModel = Monitor.isModel(value)
items.push({
name: property,
type: detailedType,
category: isModel? "models" : "properties",
isModel: isModel
})
}
}
items.sort((a, b) => {
const nameA = a.category
const nameB = b.category
if (nameA === nameB)
return 0
if (nameA === "models")
return -1
if (nameB === "models")
return 1
if (nameA < nameB)
return -1
if (nameA > nameB)
return 1
})
inspectionModel.append(items)
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
Label {
text: name
font.pixelSize: 20
font.bold: true
}
MenuSeparator {
Layout.fillWidth: true
}
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 5 spacing: 5
clip: true
model: ListModel {
id: inspectionModel
}
delegate: Item { delegate: Item {
implicitWidth: delegateRow.implicitWidth implicitWidth: delegateRow.implicitWidth
implicitHeight: delegateRow.implicitHeight implicitHeight: delegateRow.implicitHeight
readonly property var contextPropertyValue:
MonitorUtils.contextPropertyBindingHelper(name, root).value
Row { Row {
id: delegateRow id: delegateRow
readonly property var object: objectForInspection[name]
Label { Label {
text: name text: name
} }
Loader { Label {
active: type !== "function" text: ` [${MonitorUtils.typeName(contextPropertyValue)}]`
sourceComponent: Label { color: "darkgreen"
text: ` [${type}]`
color: "darkgreen"
}
} }
Loader { Label {
active: type !== "function" text: ` (${MonitorUtils.valueToString(contextPropertyValue)})`
sourceComponent: Label { color: "darkred"
text: ` (${MonitorUtils.valueToString(delegateRow.object)})`
color: "darkred"
}
}
Loader {
active: isModel
sourceComponent: Label {
text: `, ${delegateRow.object.rowCount()} items`
color: "darkred"
font.bold: true
}
} }
} }
@ -464,39 +99,277 @@ Component {
anchors.fill: parent anchors.fill: parent
onClicked: { onClicked: {
if (!isModel) inspectionStackView.clear()
return
const props = { const props = {
name: name, name: name,
model: objectForInspection[name] objectForInspection: contextPropertyValue
} }
inspectionStackView.push(modelInspectionComponent, inspectionStackView.push(inspectionList, props)
props, StackView.Immediate)
} }
} }
} }
section.property: "category"
section.delegate: Pane {
leftPadding: 0
Label {
text: section
font.bold: true
}
}
} }
} }
Component {
id: modelInspectionComponent
ModelInspectionPane {}
}
Component {
id: inspectionList
Pane {
id: inspectionPanel
property var objectForInspection
property string name
onObjectForInspectionChanged: {
inspectionModel.clear()
if (!objectForInspection)
return
const items = []
for (const property in objectForInspection) {
const type = typeof objectForInspection[property]
if (type === "function") {
items.push({
name: property,
category: "functions",
isModel: false,
type: type
})
} else {
const value = objectForInspection[property]
const detailedType = MonitorUtils.typeName(value)
const isModel = Monitor.isModel(value)
items.push({
name: property,
type: detailedType,
category: isModel? "models" : "properties",
isModel: isModel
})
}
}
items.sort((a, b) => {
const nameA = a.category
const nameB = b.category
if (nameA === nameB)
return 0
if (nameA === "models")
return -1
if (nameB === "models")
return 1
if (nameA < nameB)
return -1
if (nameA > nameB)
return 1
})
inspectionModel.append(items)
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
Label {
text: name
font.pixelSize: 20
font.bold: true
}
MenuSeparator {
Layout.fillWidth: true
}
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 5
clip: true
model: ListModel {
id: inspectionModel
}
delegate: Item {
implicitWidth: delegateRow.implicitWidth
implicitHeight: delegateRow.implicitHeight
Row {
id: delegateRow
readonly property var object: objectForInspection[name]
Label {
text: name
}
Loader {
active: type !== "function"
sourceComponent: Label {
text: ` [${type}]`
color: "darkgreen"
}
}
Loader {
active: type !== "function"
sourceComponent: Label {
text: ` (${MonitorUtils.valueToString(delegateRow.object)})`
color: "darkred"
}
}
Loader {
active: isModel
sourceComponent: Label {
text: `, ${delegateRow.object.rowCount()} items`
color: "darkred"
font.bold: true
}
}
}
MouseArea {
anchors.fill: parent
onClicked: {
if (!isModel)
return
const props = {
name: name,
model: objectForInspection[name]
}
inspectionStackView.push(modelInspectionComponent,
props, StackView.Immediate)
}
}
}
section.property: "category"
section.delegate: Pane {
leftPadding: 0
Label {
text: section
font.bold: true
}
}
}
}
}
}
StackView {
id: inspectionStackView
SplitView.fillHeight: true
SplitView.minimumWidth: 100
}
} }
}
StackView { Item {
id: inspectionStackView ColumnLayout {
anchors.fill: parent
SplitView.fillHeight: true Label {
SplitView.minimumWidth: 100 Layout.fillWidth: true
wrapMode: Text.Wrap
text: "Note: 'applicationWindow' is good root object in"
+ " most cases. 'WalletStores.RootStore' and"
+ " `SharedStores.RootStore` are also exposed for"
+ " convenience for models created within those singletons."
}
RowLayout {
Layout.fillHeight: false
Layout.fillWidth: true
Label {
text: "Model's object name:"
}
TextField {
id: objectNameTextFiled
selectByMouse: true
}
Label {
text: "Root:"
}
TextField {
id: rootTextField
text: "applicationWindow"
selectByMouse: true
}
Button {
text: "Search"
onClicked: {
let rootObj = null
try {
rootObj = eval(rootTextField.text)
} catch (error) {
objLabel.objStr = "Root object not found!"
return
}
const obj = Monitor.findChild(
rootObj, objectNameTextFiled.text)
objLabel.objStr = obj && Monitor.isModel(obj)
? obj.toString() : "Model not found!"
rolesModelContent.model = obj
}
}
}
Label {
id: objLabel
property string objStr
Layout.fillWidth: true
visible: objStr !== ""
text: "Object: " + objStr
}
ModelInspectionPane {
id: rolesModelContent
showControls: false
Layout.fillWidth: true
Layout.fillHeight: true
}
}
}
} }
} }
} }

View File

@ -26,6 +26,8 @@ public:
Q_INVOKABLE QString typeName(const QVariant &obj) const; Q_INVOKABLE QString typeName(const QVariant &obj) const;
Q_INVOKABLE QJSValue modelRoles(QAbstractItemModel *model) const; Q_INVOKABLE QJSValue modelRoles(QAbstractItemModel *model) const;
Q_INVOKABLE QObject* findChild(QObject* obj, const QString& name) const;
static Monitor& instance(); static Monitor& instance();
static QObject* qmlInstance(QQmlEngine *engine, QJSEngine *scriptEngine); static QObject* qmlInstance(QQmlEngine *engine, QJSEngine *scriptEngine);

View File

@ -6,7 +6,8 @@
#include <QQmlComponent> #include <QQmlComponent>
#include <QQuickWindow> #include <QQuickWindow>
void Monitor::initialize(QQmlApplicationEngine* engine) { void Monitor::initialize(QQmlApplicationEngine* engine)
{
QObject::connect(engine, &QQmlApplicationEngine::objectCreated, this, QObject::connect(engine, &QQmlApplicationEngine::objectCreated, this,
[engine](QObject *obj, const QUrl &objUrl) { [engine](QObject *obj, const QUrl &objUrl) {
if (!obj) { if (!obj) {
@ -46,6 +47,11 @@ bool Monitor::isModel(const QVariant &obj) const
return qobject_cast<QAbstractItemModel*>(obj.value<QObject*>()) != nullptr; return qobject_cast<QAbstractItemModel*>(obj.value<QObject*>()) != nullptr;
} }
QObject* Monitor::findChild(QObject* obj, const QString& name) const
{
return obj == nullptr ? nullptr : obj->findChild<QObject*>(name);
}
QString Monitor::typeName(const QVariant &obj) const QString Monitor::typeName(const QVariant &obj) const
{ {
if (obj.canConvert<QObject*>()) if (obj.canConvert<QObject*>())