feat(MonitoringTool): Inspect arbitrary model found by objectName
Closes: #14971
This commit is contained in:
parent
2c1806434a
commit
80eaf6ba89
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,48 @@
|
|||
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 {
|
||||
|
||||
ColumnLayout {
|
||||
|
||||
spacing: 0
|
||||
|
||||
Settings {
|
||||
property alias tabIndex: tabBar.currentIndex
|
||||
property alias modelObjectName: objectNameTextFiled.text
|
||||
property alias modelObjectRootName: rootTextField.text
|
||||
}
|
||||
|
||||
TabBar {
|
||||
id: tabBar
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: 10
|
||||
|
||||
TabButton {
|
||||
text: "Context properties inspection"
|
||||
}
|
||||
TabButton {
|
||||
text: "Models inspection"
|
||||
}
|
||||
|
||||
currentIndex: swipeView.currentIndex
|
||||
}
|
||||
|
||||
StackLayout {
|
||||
id: swipeView
|
||||
|
||||
currentIndex: tabBar.currentIndex
|
||||
//anchors.fill: parent
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
SplitView {
|
||||
id: root
|
||||
|
||||
|
@ -79,257 +116,7 @@ Component {
|
|||
Component {
|
||||
id: modelInspectionComponent
|
||||
|
||||
Pane {
|
||||
property string name
|
||||
property var model
|
||||
readonly property var rootModel: model
|
||||
|
||||
property bool showControls: true
|
||||
|
||||
readonly property var roles: 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 ajust width, press cell content to " +
|
||||
"see full value"
|
||||
}
|
||||
|
||||
Label {
|
||||
text: `rows count: ${model.rowCount()}`
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
ListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ModelInspectionPane {}
|
||||
}
|
||||
|
||||
Component {
|
||||
|
@ -499,4 +286,90 @@ Component {
|
|||
SplitView.minimumWidth: 100
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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*>())
|
||||
|
|
Loading…
Reference in New Issue