diff --git a/monitoring/MonitorEntryPoint.qml b/monitoring/MonitorEntryPoint.qml index 4159f65f3..e99f5cabc 100644 --- a/monitoring/MonitorEntryPoint.qml +++ b/monitoring/MonitorEntryPoint.qml @@ -6,35 +6,437 @@ import Monitoring 1.0 Component { - ColumnLayout { - anchors.fill: parent - anchors.margins: 10 + SplitView { + id: root - spacing: 15 - - Label { - Layout.fillWidth: true - - text: "Context properties:" - font.bold: true - } - - ListView { - id: lv - - Layout.fillWidth: true - Layout.fillHeight: true - - clip: true - - - model: Monitor.contexPropertiesNames + ColumnLayout { + SplitView.fillHeight: true + SplitView.preferredWidth: 450 spacing: 5 - delegate: Text { - text: modelData + 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) + } + } + } + } + } + + Component { + id: modelInspectionComponent + + Pane { + property string name + property var model + readonly property var rootModel: model + + 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} `)) + })) + + property int columnsTotalWidth: + rolesModelContent.reduce((a, x) => a + x.width, 0) + + ListModel { + id: rolesModel + + Component.onCompleted: append(rolesModelContent) + } + + Control { + id: helperControl + + font.bold: true + } + + FontMetrics { + id: fontMetrics + + font.bold: true + } + + ColumnLayout { + anchors.fill: parent + + RowLayout { + Layout.fillWidth: true + + RoundButton { + text: "⬅️" + + onClicked: { + inspectionStackView.pop(StackView.Immediate) + } + } + + Label { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + + text: name + font.pixelSize: 20 + font.bold: true + } + } + + MenuSeparator { + Layout.fillWidth: true + } + + Label { + 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: topModel[model.name].toString() + 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 + } + + 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 + + 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 } } } diff --git a/monitoring/MonitorUtils.qml b/monitoring/MonitorUtils.qml new file mode 100644 index 000000000..1ef93dcd4 --- /dev/null +++ b/monitoring/MonitorUtils.qml @@ -0,0 +1,45 @@ +pragma Singleton + +import QtQml 2.14 + +import Monitoring 1.0 + +QtObject { + function typeName(obj) { + const type = Monitor.typeName(obj) + + if (type === "QJSValue") + return typeof obj + + return type + } + + function valueToString(val) { + if (val === undefined) + return "undefined" + + if (val === null) + return "null" + + const str = val.toString() + + if (typeof val === "string") + return `"${str}"` + + if (typeof val !== "object") + return str + + const bracketPos = str.indexOf("(") + + if (bracketPos === -1) + return str + + return str.substring(bracketPos + 1, str.length - 1) + } + + function contextPropertyBindingHelper(name, parent) { + return Qt.createQmlObject( + `import QtQml 2.14; QtObject { readonly property var value: ${name} }`, + parent, `ctxPropHelperSnippet_${name}`) + } +} diff --git a/monitoring/qmldir b/monitoring/qmldir new file mode 100644 index 000000000..676ef0a8c --- /dev/null +++ b/monitoring/qmldir @@ -0,0 +1 @@ +singleton MonitorUtils 1.0 MonitorUtils.qml diff --git a/vendor/DOtherSide b/vendor/DOtherSide index 7f2dd4383..06a00bc8c 160000 --- a/vendor/DOtherSide +++ b/vendor/DOtherSide @@ -1 +1 @@ -Subproject commit 7f2dd438325925ae3db073e19eec63a49253e735 +Subproject commit 06a00bc8c3dd2c75cab7c084da84f213ffe1d70c