diff --git a/monitoring/ModelInspectionPane.qml b/monitoring/ModelInspectionPane.qml new file mode 100644 index 0000000000..ce161883bb --- /dev/null +++ b/monitoring/ModelInspectionPane.qml @@ -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 + } + } + } + } + } + } + } + } +} diff --git a/monitoring/MonitorEntryPoint.qml b/monitoring/MonitorEntryPoint.qml index a4eedf0a81..50171747b4 100644 --- a/monitoring/MonitorEntryPoint.qml +++ b/monitoring/MonitorEntryPoint.qml @@ -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 + } + } + } } } } diff --git a/ui/include/StatusDesktop/Monitoring/Monitor.h b/ui/include/StatusDesktop/Monitoring/Monitor.h index 742649e7bb..69620d607b 100644 --- a/ui/include/StatusDesktop/Monitoring/Monitor.h +++ b/ui/include/StatusDesktop/Monitoring/Monitor.h @@ -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); diff --git a/ui/src/StatusDesktop/Monitoring/Monitor.cpp b/ui/src/StatusDesktop/Monitoring/Monitor.cpp index 3bbda94926..d8fa838981 100644 --- a/ui/src/StatusDesktop/Monitoring/Monitor.cpp +++ b/ui/src/StatusDesktop/Monitoring/Monitor.cpp @@ -6,7 +6,8 @@ #include #include -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(obj.value()) != nullptr; } +QObject* Monitor::findChild(QObject* obj, const QString& name) const +{ + return obj == nullptr ? nullptr : obj->findChild(name); +} + QString Monitor::typeName(const QVariant &obj) const { if (obj.canConvert())