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.Controls 2.14
import QtQuick.Layouts 1.14
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
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 {
SplitView {
id: root
ColumnLayout {
SplitView.fillHeight: true
SplitView.preferredWidth: 450
ColumnLayout {
spacing: 5
spacing: 0
Label {
Layout.fillWidth: true
Layout.margins: 5
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)
}
}
}
}
Settings {
property alias tabIndex: tabBar.currentIndex
property alias modelObjectName: objectNameTextFiled.text
property alias modelObjectRootName: rootTextField.text
}
Component {
id: modelInspectionComponent
TabBar {
id: tabBar
Layout.fillWidth: true
Layout.bottomMargin: 10
Pane {
property string name
property var model
readonly property var rootModel: model
TabButton {
text: "Context properties inspection"
}
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 => ({
visible: true,
name: role.name,
width: Math.ceil(fontMetrics.advanceWidth(` ${role.name} `))
}))
currentIndex: tabBar.currentIndex
//anchors.fill: parent
Layout.fillWidth: true
Layout.fillHeight: true
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
}
SplitView {
id: root
ColumnLayout {
anchors.fill: parent
SplitView.fillHeight: true
SplitView.preferredWidth: 450
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
}
spacing: 5
Label {
visible: showControls
Layout.fillWidth: true
Layout.margins: 5
text: "Hint: use right/left button click on a column " +
"header to ajust width, press cell content to " +
"see full value"
}
Label {
text: `rows count: ${model.rowCount()}`
text: "Context properties:"
font.bold: true
}
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.margins: 5
model: Monitor.contexPropertiesModel
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
clip: true
model: ListModel {
id: inspectionModel
}
delegate: Item {
implicitWidth: delegateRow.implicitWidth
implicitHeight: delegateRow.implicitHeight
readonly property var contextPropertyValue:
MonitorUtils.contextPropertyBindingHelper(name, root).value
Row {
id: delegateRow
readonly property var object: objectForInspection[name]
Label {
text: name
}
Loader {
active: type !== "function"
sourceComponent: Label {
text: ` [${type}]`
color: "darkgreen"
}
Label {
text: ` [${MonitorUtils.typeName(contextPropertyValue)}]`
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
}
Label {
text: ` (${MonitorUtils.valueToString(contextPropertyValue)})`
color: "darkred"
}
}
@ -464,39 +99,277 @@ Component {
anchors.fill: parent
onClicked: {
if (!isModel)
return
inspectionStackView.clear()
const props = {
name: name,
model: objectForInspection[name]
objectForInspection: contextPropertyValue
}
inspectionStackView.push(modelInspectionComponent,
props, StackView.Immediate)
inspectionStackView.push(inspectionList, props)
}
}
}
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 {
id: inspectionStackView
Item {
ColumnLayout {
anchors.fill: parent
SplitView.fillHeight: true
SplitView.minimumWidth: 100
Label {
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 QJSValue modelRoles(QAbstractItemModel *model) const;
Q_INVOKABLE QObject* findChild(QObject* obj, const QString& name) const;
static Monitor& instance();
static QObject* qmlInstance(QQmlEngine *engine, QJSEngine *scriptEngine);

View File

@ -6,7 +6,8 @@
#include <QQmlComponent>
#include <QQuickWindow>
void Monitor::initialize(QQmlApplicationEngine* engine) {
void Monitor::initialize(QQmlApplicationEngine* engine)
{
QObject::connect(engine, &QQmlApplicationEngine::objectCreated, this,
[engine](QObject *obj, const QUrl &objUrl) {
if (!obj) {
@ -46,6 +47,11 @@ bool Monitor::isModel(const QVariant &obj) const
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
{
if (obj.canConvert<QObject*>())