summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorShawn Rutledge <shawn.rutledge@qt.io>2023-05-30 10:49:58 +0200
committerShawn Rutledge <shawn.rutledge@qt.io>2023-07-05 19:40:34 +0000
commite6a2eb1eafdcdb9f493370fc211e483804a78184 (patch)
tree530145816f30f36be5011304ae93f32afe244322
parent3f9f09855c3cb2075833446e448f4f699e62b0ed (diff)
Add ObjectInstances: generate an Object Diagram with Graphviz
This takes care of many common 2D and 3D use cases, and probably other scenarios with QObjects too: - root can be any QObject - different shapes for Items, Handlers, 3D objects, QAIM and DeliveryAgents - shows objectName, text/label/title - shape (outline) color can correspond to item (fill) color if desired - several new working examples Change-Id: Ica9a312ee6ad7e49c872ea52e6e8ab03fc41bc63 Reviewed-by: Shawn Rutledge <shawn.rutledge@qt.io>
-rw-r--r--examples/BusyBox.qml148
-rw-r--r--examples/hover.qml9
-rw-r--r--examples/item2d.qml68
-rw-r--r--examples/sidebar.qml204
-rw-r--r--examples/windowBusyBox.qml31
-rw-r--r--src/generators/CMakeLists.txt5
-rw-r--r--src/generators/objectinstances.cpp286
-rw-r--r--src/generators/objectinstances_p.h72
8 files changed, 822 insertions, 1 deletions
diff --git a/examples/BusyBox.qml b/examples/BusyBox.qml
new file mode 100644
index 0000000..bafe783
--- /dev/null
+++ b/examples/BusyBox.qml
@@ -0,0 +1,148 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick.Controls
+import Qt.labs.UmlQuick.Generators
+
+Rectangle {
+ id: root
+ objectName: "busybox"
+ height: 256
+ width: 256
+
+ color: "#444"
+ border.color: "cyan"
+ border.width: 2
+ antialiasing: true
+
+ component HoverTapButton: Rectangle {
+ id: htbutton
+ property string name: ""
+ objectName: name + " handlers button"
+ property alias pressed: utap.pressed
+ property point lastPressPos: Qt.point(-1, -1)
+ property point lastReleasePos: Qt.point(-1, -1)
+ property int clickedCount: 0
+ onPressedChanged: {
+ if (pressed)
+ lastPressPos = utap.point.position;
+ else
+ lastReleasePos = utap.point.position;
+ }
+ width: 100; height: 40
+ color: utap.pressed ? "tomato" : "beige"
+ radius: 5
+ border.width: 3
+ border.color: uhover.hovered ? "orange" : "black"
+ antialiasing: true
+ HoverHandler {
+ id: uhover
+ objectName: htbutton.name + " HoverHandler"
+ cursorShape: Qt.OpenHandCursor
+ }
+ TapHandler {
+ id: utap
+ objectName: htbutton.name + " TapHandler"
+ onTapped: ++clickedCount
+ }
+ Text {
+ anchors.centerIn: parent
+ text: "hover & tap"
+ }
+ }
+
+ HoverHandler {
+ id: feedback
+ objectName: "hover position feedback"
+ target: Rectangle {
+ color: "orange"
+ x: feedback.point.position.x - width / 2
+ y: feedback.point.position.y - height / 2
+ width: 10; height: width; radius: width
+ }
+ }
+
+ Text {
+ color: "orange"
+ text: "hover " + feedback.point.position.x.toFixed(1) + ", " + feedback.point.position.y.toFixed(1)
+ anchors.bottom: parent.bottom
+ anchors.margins: 3
+ x: 3
+ }
+
+ Row {
+ x: 10; y: 10; z: 1
+ spacing: 10
+ Column {
+ spacing: 10
+ HoverTapButton {
+ name: "upper"
+ }
+ HoverTapButton {
+ name: "lower"
+ }
+ Rectangle {
+ objectName: "mousearea button"
+ width: 100; height: 40
+ color: ma.pressed ? "tomato" : "beige"
+ radius: 5
+ border.width: 3
+ border.color: ma.containsMouse ? "orange" : "black"
+ antialiasing: true
+ MouseArea {
+ id: ma
+ objectName: "button mousearea"
+ anchors.fill: parent
+ hoverEnabled: true
+ property point lastPressPos: Qt.point(-1, -1)
+ property point lastReleasePos: Qt.point(-1, -1)
+ property int clickedCount: 0
+ onPressed: (mouse) => { lastPressPos = Qt.point(mouse.x, mouse.y) }
+ onReleased: (mouse) => { lastReleasePos = Qt.point(mouse.x, mouse.y) }
+ onClicked: ++clickedCount
+ }
+ Text {
+ anchors.centerIn: parent
+ text: "hover & click"
+ }
+ }
+ TextInput {
+ text: "Editable Text"
+ color: "cyan"
+ focus: true
+ }
+ }
+
+ Slider {
+ objectName: "toy slider"
+ orientation: Qt.Vertical
+ }
+
+ Rectangle {
+ width: 100; height: 200
+ color: "#333"
+
+ ListView {
+ model: ListModel {
+ ListElement {
+ name: "Apple"
+ }
+ ListElement {
+ name: "Orange"
+ }
+ ListElement {
+ name: "Banana"
+ }
+ }
+
+ delegate: Text {
+ required property string name
+ color: "#ddd"
+ text: name
+ }
+ anchors.fill: parent
+ }
+ }
+ }
+}
diff --git a/examples/hover.qml b/examples/hover.qml
index a454e38..60f3536 100644
--- a/examples/hover.qml
+++ b/examples/hover.qml
@@ -41,6 +41,7 @@ import QtQuick 2.8
import Qt.labs.UmlQuick.Generators 1.0
Rectangle {
+ id: root
color: outerMA.containsMouse ? "lightsteelblue" : "gray"
width: 100; height: 400
objectName: "outerRect"
@@ -51,6 +52,14 @@ Rectangle {
// outputFormat: MessageTrace.QML
}
+ ObjectInstances {
+ id: oi
+ outputPrefix: "hoverObjects-"
+// outputFormat: ObjectInstances.Graphviz
+ root: root
+ }
+ Component.onCompleted: oi.generate()
+
MouseArea {
id: outerMA
objectName: "outerMA"
diff --git a/examples/item2d.qml b/examples/item2d.qml
new file mode 100644
index 0000000..2c0319c
--- /dev/null
+++ b/examples/item2d.qml
@@ -0,0 +1,68 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick3D
+import Qt.labs.UmlQuick.Generators
+
+Window {
+ id: root
+ width: 1024; height: 480
+ visible: true
+
+ ObjectInstances {
+ id: oi
+ outputPrefix: "item2dObjects-"
+ nodeColorSource: ObjectInstances.ItemColor
+ root: root
+ }
+ Component.onCompleted: oi.generate()
+
+ View3D {
+ anchors.fill: parent
+
+ Shortcut {
+ sequence: StandardKey.Quit
+ onActivated: Qt.quit()
+ }
+
+ environment: SceneEnvironment {
+ clearColor: "#111"
+ backgroundMode: SceneEnvironment.Color
+ }
+
+ PerspectiveCamera {
+ z: 600
+ }
+
+ DirectionalLight {
+
+ }
+
+ DirectionalLight {
+ eulerRotation.y: -90
+ }
+
+ Node {
+ objectName: "left object"
+ x: -256
+ y: 128
+ z: 380
+ eulerRotation.y: 15
+ BusyBox {
+ objectName: "left busybox"
+ }
+ }
+
+ Node {
+ objectName: "right object"
+ x: 32
+ y: 128
+ z: 300
+ eulerRotation.y: -5
+ BusyBox {
+ objectName: "right busybox"
+ }
+ }
+ }
+}
diff --git a/examples/sidebar.qml b/examples/sidebar.qml
new file mode 100644
index 0000000..fab0771
--- /dev/null
+++ b/examples/sidebar.qml
@@ -0,0 +1,204 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick.Controls
+import Qt.labs.UmlQuick.Generators
+
+Rectangle {
+ id: root
+ width: 640
+ height: 480
+ color: rootHover.hovered ? "#555" : "#444"
+
+ ObjectInstances {
+ id: oi
+ outputPrefix: "sidebarObjects-"
+// outputFormat: ObjectInstances.Graphviz
+ nodeColorSource: ObjectInstances.ItemColor
+ root: root
+ }
+ Component.onCompleted: oi.generate()
+
+ component FlashAnimation: SequentialAnimation {
+ id: tapFlash
+ running: false
+ loops: 3
+ PropertyAction { value: false }
+ PauseAnimation { duration: 100 }
+ PropertyAction { value: true }
+ PauseAnimation { duration: 100 }
+ }
+
+ component ButtonsAndStuff: Column {
+ anchors.fill: parent
+ anchors.margins: 8
+ spacing: 8
+
+ CheckBox {
+ id: hoverBlockingCB
+ text: "Button hover is blocking"
+ }
+
+ Rectangle {
+ objectName: "buttonWithMA"
+ width: parent.width
+ height: 30
+ color: buttonMA.pressed ? "lightsteelblue" : "#999"
+ border.color: buttonMA.containsMouse ? "cyan" : "transparent"
+
+ MouseArea {
+ id: buttonMA
+ objectName: "buttonMA"
+ hoverEnabled: true
+ cursorShape: Qt.UpArrowCursor
+ anchors.fill: parent
+ onClicked: console.log("clicked MA")
+ }
+
+ Text {
+ anchors.centerIn: parent
+ text: "MouseArea"
+ }
+ }
+
+ Rectangle {
+ objectName: "buttonWithHH"
+ width: parent.width
+ height: 30
+ color: flash ? "#999" : "white"
+ border.color: buttonHH.hovered ? "cyan" : "transparent"
+ property bool flash: true
+
+ HoverHandler {
+ id: buttonHH
+ objectName: "buttonHH"
+ acceptedDevices: PointerDevice.AllDevices
+ blocking: hoverBlockingCB.checked
+ cursorShape: tapHandler.pressed ? Qt.BusyCursor : Qt.PointingHandCursor
+ }
+
+ TapHandler {
+ id: tapHandler
+ onTapped: tapFlash.start()
+ }
+
+ Text {
+ anchors.centerIn: parent
+ text: "HoverHandler"
+ }
+
+ FlashAnimation on flash {
+ id: tapFlash
+ }
+ }
+ }
+
+ Rectangle {
+ id: paddle
+ width: 100
+ height: 40
+ color: paddleHH.hovered ? "indianred" : "#888"
+ y: parent.height - 100
+ radius: 10
+
+ HoverHandler {
+ id: paddleHH
+ objectName: "paddleHH"
+ }
+
+ SequentialAnimation on x {
+ NumberAnimation {
+ to: root.width - paddle.width
+ duration: 2000
+ easing { type: Easing.InOutQuad }
+ }
+ PauseAnimation { duration: 100 }
+ NumberAnimation {
+ to: 0
+ duration: 2000
+ easing { type: Easing.InOutQuad }
+ }
+ PauseAnimation { duration: 100 }
+ loops: Animation.Infinite
+ }
+ }
+
+ Rectangle {
+ objectName: "topSidebar"
+ radius: 5
+ antialiasing: true
+ x: -10
+ y: -radius
+ width: 200
+ height: 200
+ border.color: topSidebarHH.hovered ? "cyan" : "black"
+ color: "#777"
+
+ Rectangle {
+ color: "cyan"
+ width: 10
+ height: width
+ radius: width / 2
+ visible: topSidebarHH.hovered
+ x: topSidebarHH.point.position.x - width / 2
+ y: topSidebarHH.point.position.y - height / 2
+ z: 100
+ }
+
+ HoverHandler {
+ id: topSidebarHH
+ objectName: "topSidebarHH"
+ cursorShape: Qt.OpenHandCursor
+ }
+
+ ButtonsAndStuff {
+ anchors.fill: parent
+ }
+
+ Text {
+ anchors { left: parent.left; bottom: parent.bottom; margins: 12 }
+ text: "Hover Handler"
+ }
+ }
+
+ Rectangle {
+ objectName: "bottomSidebar"
+ radius: 5
+ antialiasing: true
+ x: -10
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: -radius
+ width: 200
+ height: 200
+ border.color: bottomSidebarMA.containsMouse ? "cyan" : "black"
+ color: "#777"
+
+ MouseArea {
+ id: bottomSidebarMA
+ objectName: "bottomSidebarMA"
+ hoverEnabled: true
+ cursorShape: Qt.ClosedHandCursor
+ anchors.fill: parent
+
+ ButtonsAndStuff {
+ anchors.fill: parent
+ }
+ }
+
+ Text {
+ anchors { left: parent.left; bottom: parent.bottom; margins: 12 }
+ text: "MouseArea"
+ }
+ }
+
+ HoverHandler {
+ id: rootHover
+ }
+
+ Text {
+ anchors.right: parent.right
+ color: "cyan"
+ text: "scene " + rootHover.point.scenePosition.x.toFixed(1) + ", " + rootHover.point.scenePosition.y.toFixed(1)
+ }
+}
diff --git a/examples/windowBusyBox.qml b/examples/windowBusyBox.qml
new file mode 100644
index 0000000..f1c2f6a
--- /dev/null
+++ b/examples/windowBusyBox.qml
@@ -0,0 +1,31 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick.Controls
+import Qt.labs.UmlQuick.Generators
+
+QtObject {
+ id: root
+ objectName: "root"
+
+ property var win: Window {
+ height: 256
+ width: 256
+ color: "#444"
+ title: "BusyBox"
+ visible: true
+
+ ObjectInstances {
+ id: oi
+ outputPrefix: "windowBusyBoxObjects-"
+ nodeColorSource: ObjectInstances.ItemColor
+ root: root
+ }
+ Component.onCompleted: oi.generate()
+
+ BusyBox {
+ anchors.fill: parent
+ }
+ }
+}
diff --git a/src/generators/CMakeLists.txt b/src/generators/CMakeLists.txt
index 6389c83..b8d4725 100644
--- a/src/generators/CMakeLists.txt
+++ b/src/generators/CMakeLists.txt
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.16)
project(generators VERSION 1.0 LANGUAGES CXX)
-find_package(Qt6 REQUIRED COMPONENTS Core Gui Qml Quick)
+find_package(Qt6 REQUIRED COMPONENTS Core Gui Qml Quick Quick3D)
qt_policy(SET QTP0001 NEW)
qt_standard_project_setup(REQUIRES 6.2)
@@ -14,6 +14,7 @@ qt_add_qml_module(generators
VERSION 1.0
SOURCES
messagetrace.cpp messagetrace_p.h
+ objectinstances.cpp objectinstances_p.h
)
target_link_libraries(generators PUBLIC
@@ -21,6 +22,8 @@ target_link_libraries(generators PUBLIC
Qt::Gui
Qt::Quick
Qt::QuickPrivate
+ Qt::Quick3D
+ Qt::Quick3DPrivate
)
qt_query_qml_module(generators
diff --git a/src/generators/objectinstances.cpp b/src/generators/objectinstances.cpp
new file mode 100644
index 0000000..e781691
--- /dev/null
+++ b/src/generators/objectinstances.cpp
@@ -0,0 +1,286 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "objectinstances_p.h"
+#include <QAbstractItemModel>
+#include <QColor>
+#include <QDateTime>
+#include <QDebug>
+#include <QFile>
+#include <QQuickItem>
+#include <QStringList>
+#include <private/qobject_p.h>
+#include <private/qqmlglobal_p.h>
+#include <private/qquickitem_p.h>
+#include <private/qquickpointerhandler_p.h>
+#include <private/qquickrectangle_p.h>
+#include <private/qquick3ditem2d_p.h>
+
+/*!
+ \class ObjectInstances
+ \inmodule UmlQuick
+
+ \brief The ObjectInstances class provides an output stream for rendering
+ a UML \l {https://en.wikipedia.org/wiki/Object_diagram}{Object diagram}.
+ The outputFormat property specifies what syntax to use for that.
+ It may be augmented with relations notation (arrowheads).
+
+ An object diagram can be easily turned into a
+ \l {https://en.wikipedia.org/wiki/Communication_diagram}{Communication diagram}
+ by converting to SVG and adding message annotations manually in a drawing
+ application afterwards (for example Inkscape).
+
+ \section1 Basic Use
+
+ Any QML application can instantiate an ObjectInstances object.
+ It can be triggered to capture the state of a particular subtree
+ at some point in time. Alternatively, it can write the output at the time
+ of application exit; in that case, one can get the application into the
+ desired state, and then simply close the main window, quit via the
+ application's quit Shortcut, etc.
+*/
+
+QT_BEGIN_NAMESPACE
+
+#define MT_DEBUG_ENABLED
+#ifdef MT_DEBUG_ENABLED
+#define MT_DEBUG printf
+#else
+#define MT_DEBUG(format, args...) ((void)0)
+#endif
+
+using namespace Qt::StringLiterals;
+
+static QString pointerHash(const void* ptr)
+{
+ QByteArray ret;
+ ret.setNum((qulonglong)ptr, 16);
+ // qml doesn't like an ID to start with a digit
+ // so, shift 0-9 to g-p
+ for (int i = 0; i < ret.length(); ++i) {
+ QChar ch(ret[i]);
+ if (ch.isDigit())
+ ret[i] = (char)(ret[i] + 55);
+ }
+ if (ret.length() == 1)
+ return QStringLiteral("null");
+ return QLatin1String(ret);
+}
+
+QString ObjectInstances::objectId(const void *obj)
+{
+ return pointerHash(obj);
+}
+
+ObjectInstances::ObjectInstances(QObject *parent)
+ : QObject(parent)
+ , m_outputPrefix("objectInstances")
+{
+}
+
+void ObjectInstances::setOutputPrefix(QString outputPrefix)
+{
+ if (m_outputPrefix == outputPrefix)
+ return;
+
+ m_outputPrefix = outputPrefix;
+ emit outputPrefixChanged();
+}
+
+ObjectInstances::OutputFormat ObjectInstances::outputFormat() const
+{
+ return m_outputFormat;
+}
+
+void ObjectInstances::setOutputFormat(OutputFormat fmt)
+{
+ if (m_outputFormat == fmt)
+ return;
+
+ m_outputFormat = fmt;
+ emit outputFormatChanged();
+}
+
+ObjectInstances::ColorSource ObjectInstances::nodeColorSource() const
+{
+ return m_nodeColorSource;
+}
+
+void ObjectInstances::setNodeColorSource(ColorSource newNodeColorSource)
+{
+ if (m_nodeColorSource == newNodeColorSource)
+ return;
+ m_nodeColorSource = newNodeColorSource;
+ emit nodeColorSourceChanged();
+}
+
+/*!
+ \qmlproperty QObject * root
+ The root object to build the subtree diagram from.
+ */
+QObject *ObjectInstances::root() const
+{
+ return m_root;
+}
+
+void ObjectInstances::setRoot(QObject *newRoot)
+{
+ if (m_root == newRoot)
+ return;
+ m_root = newRoot;
+ emit rootChanged();
+}
+
+void ObjectInstances::generate()
+{
+ if (!m_root) {
+ qWarning() << "nothing to generate: root is not set";
+ return;
+ }
+ QString filePath = m_outputPrefix + QDateTime::currentDateTime().toString(Qt::ISODate);
+ switch (m_outputFormat) {
+ case OutputFormat::Graphviz:
+ writeDot(filePath, m_root);
+ break;
+ }
+}
+
+void ObjectInstances::writeDot(const QString &plainFilePath, const QObject *o)
+{
+ QString filePath = plainFilePath + u".dot"_s;
+ MT_DEBUG("-> %s\n", qPrintable(filePath));
+ QFile f(filePath);
+ if (f.open(QFile::WriteOnly)) {
+ f.write("digraph {\n");
+ writeDotRecur(f, o);
+ f.write("}\n");
+ }
+}
+
+void ObjectInstances::writeDotRecur(QFile &f, const QObject *o)
+{
+ if (!o) {
+ qWarning() << "unexpected null";
+ return;
+ }
+ const auto name = pointerHash(o);
+ QString clname = QString::fromUtf8(o->metaObject()->className());
+ const bool is3D = clname.startsWith("QQuick3D");
+ clname.replace(u"QQuick"_s, u"\u211a"_s); // double-struck Q symbol takes less space on diagram
+ QStringList labels = { clname };
+ const QString oname = o->objectName();
+ if (!oname.isEmpty())
+ labels << oname;
+ QVariant textV = o->property("text");
+ if (!textV.isValid())
+ textV = o->property("label");
+ if (!textV.isValid())
+ textV = o->property("title");
+ if (textV.isValid()) {
+ QString text = textV.toString().trimmed();
+ if (!text.isEmpty()) {
+ if (text.length() > 20)
+ text = text.left(20) + u'\u2026'; // ellipsis
+ labels << u'\u201c' + text + u'\u201d'; // quotes
+ }
+ }
+ QString label = labels.join("\\n");
+ const QObjectPrivate *oPriv = QObjectPrivate::get(o);
+ if (oPriv->isQuickItem) {
+ const auto *item = static_cast<const QQuickItem *>(o);
+ QVariant colorV;
+ if (m_nodeColorSource == ColorSource::ItemColor)
+ colorV = item->property("color");
+ if (colorV.isValid()) {
+ QColor color = colorV.value<QColor>();
+ // ensure the color is opaque and visible on a white background (white becomes light grey)
+ color.setAlpha(255);
+ if (color.lightness() > 224)
+ color.setHsl(color.hslHue(), color.hslSaturation(), 224);
+ f.write(qPrintable(u" %1[color=\"%2\", label=\"%3\", shape=\"box\"]\n"_s
+ .arg(name).arg(color.name()).arg(label)));
+ } else {
+ f.write(qPrintable(u" %1[label=\"%2\", shape=\"box\"]\n"_s
+ .arg(name).arg(label)));
+ }
+ if (auto *par = item->parentItem()) {
+ f.write(qPrintable(u" %1 -> %2[arrowtail=odiamond, dir=back]\n"_s
+ .arg(pointerHash(par)).arg(name)));
+ }
+ f.write("\n");
+ const QQuickItemPrivate *priv = QQuickItemPrivate::get(item);
+ if (priv->extra.isAllocated()) {
+ for (auto *ph : priv->extra->pointerHandlers)
+ writeDotRecur(f, ph);
+ // Item.data / resources: generally redundant if we iterate QObject children
+// for (auto *res : priv->extra->resourcesList)
+// writeDotRecur(f, res);
+ }
+ for (auto *ch : item->childItems())
+ writeDotRecur(f, ch);
+ if (priv->extra.isAllocated())
+ writeDotRecur(f, priv->extra->subsceneDeliveryAgent);
+ if (is3D) {
+ for (auto *ch : o->children())
+ writeDotRecur(f, ch);
+ }
+ } else if (const auto *handler = qobject_cast<const QQuickPointerHandler *>(o)) {
+ f.write(qPrintable(u" %1[label=\"%2\", shape=\"house\"]\n"_s
+ .arg(name).arg(label)));
+ f.write(qPrintable(u" %1 -> %2[arrowtail=odiamond, dir=back]\n"_s
+ .arg(pointerHash(handler->parent())).arg(name)));
+ } else if (const auto *qq3i2 = qobject_cast<const QQuick3DItem2D *>(o)) {
+ f.write(qPrintable(u" %1[label=\"%2\", shape=\"box3d\"]\n"_s
+ .arg(name).arg(label)));
+ f.write(qPrintable(u" %1 -> %2[arrowtail=odiamond, dir=back]\n"_s
+ .arg(pointerHash(qq3i2->parent())).arg(name)));
+ auto *content = qq3i2->contentItem();
+ f.write(qPrintable(u" %1 -> %2[arrowtail=odiamond, dir=back]\n\n"_s
+ .arg(name).arg(pointerHash(content))));
+ writeDotRecur(f, content);
+ } else if (oPriv->isWindow) {
+ if (auto *qwin = qobject_cast<const QQuickWindow *>(o)) {
+ f.write(qPrintable(u" %1[label=\"%2\", shape=\"tab\"]\n"_s
+ .arg(name).arg(label)));
+// if (auto *par = o->parent()) {
+// f.write(qPrintable(u" %1 -> %2[arrowtail=odiamond, dir=back]\n"_s
+// .arg(pointerHash(par)).arg(name)));
+// }
+ auto *content = qwin->contentItem();
+ f.write(qPrintable(u" %1 -> %2[arrowtail=odiamond, dir=back]\n\n"_s
+ .arg(name).arg(pointerHash(content))));
+ writeDotRecur(f, content);
+ writeDotRecur(f, QQuickWindowPrivate::get(qwin)->deliveryAgent);
+ }
+ } else {
+ // generic QObject
+ QString shape = u"invtrapezium"_s;
+ if (is3D) {
+ shape = u"box3d"_s;
+ } else if (clname.endsWith("DeliveryAgent")) {
+ shape = u"invhouse"_s;
+ } else if (const auto *model = qobject_cast<const QAbstractItemModel *>(o)) {
+ shape = u"cylinder"_s;
+ label += u"\\nroles %1 cols %2 rows %3"_s
+ .arg(model->columnCount()).arg(model->rowCount()).arg(model->roleNames().size());
+ }
+ f.write(qPrintable(u" %1[label=\"%2\", shape=\"%3\"]\n"_s
+ .arg(name).arg(label).arg(shape)));
+ if (auto *par = o->parent()) {
+ f.write(qPrintable(u" %1 -> %2[arrowtail=odiamond, dir=back]\n"_s
+ .arg(pointerHash(par)).arg(name)));
+ }
+ f.write("\n");
+ for (auto *ch : o->children())
+ writeDotRecur(f, ch);
+ }
+ // a bit too specialized: there might be other "interesting" properties
+ QVariant modelV = o->property("model");
+ if (modelV.isValid())
+ writeDotRecur(f, modelV.value<QObject *>());
+ // TODO use record-based nodes to show properties and invokables? it might be too cluttered
+ // iterate all QObject children and emit nodes & edges for those that were not already emitted?
+ // emit "generic" edge labels that can be either removed or turned into "message" labels in a scenario diagram?
+}
+
+QT_END_NAMESPACE
diff --git a/src/generators/objectinstances_p.h b/src/generators/objectinstances_p.h
new file mode 100644
index 0000000..7630743
--- /dev/null
+++ b/src/generators/objectinstances_p.h
@@ -0,0 +1,72 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef OBJECTINSTANCES_H
+#define OBJECTINSTANCES_H
+
+#include <QFile>
+#include <QQmlEngine>
+
+QT_BEGIN_NAMESPACE
+
+class ObjectInstances : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QString outputPrefix READ outputPrefix WRITE setOutputPrefix NOTIFY outputPrefixChanged)
+ Q_PROPERTY(OutputFormat outputFormat READ outputFormat WRITE setOutputFormat NOTIFY outputFormatChanged)
+ Q_PROPERTY(QObject* root READ root WRITE setRoot NOTIFY rootChanged)
+ Q_PROPERTY(ColorSource nodeColorSource READ nodeColorSource WRITE setNodeColorSource NOTIFY nodeColorSourceChanged)
+ QML_ELEMENT
+
+public:
+ enum class OutputFormat {
+// QML,
+// PlantUML,
+ Graphviz
+ };
+ Q_ENUM(OutputFormat)
+
+ enum class ColorSource {
+ Default,
+ ItemColor
+ };
+ Q_ENUM(ColorSource)
+
+ ObjectInstances(QObject *parent = nullptr);
+
+ QString outputPrefix() const { return m_outputPrefix; }
+ void setOutputPrefix(QString outputPrefix);
+
+ OutputFormat outputFormat() const;
+ void setOutputFormat(OutputFormat fmt);
+
+ QObject *root() const;
+ void setRoot(QObject *newRoot);
+
+ Q_INVOKABLE void generate();
+
+ ColorSource nodeColorSource() const;
+ void setNodeColorSource(ColorSource newNodeColorSource);
+
+signals:
+ void outputPrefixChanged();
+ void outputFormatChanged();
+ void rootChanged();
+ void nodeColorSourceChanged();
+
+private:
+ void parseClassAndMethod(const QString &classAndMethod, QString &className, QString &methodName);
+ static QString objectId(const void *obj);
+ void writeDot(const QString &plainFilePath, const QObject *o);
+ void writeDotRecur(QFile &f, const QObject *o);
+
+private:
+ QString m_outputPrefix;
+ OutputFormat m_outputFormat = OutputFormat::Graphviz;
+ ColorSource m_nodeColorSource = ColorSource::Default;
+ QObject *m_root = nullptr;
+};
+
+QT_END_NAMESPACE
+
+#endif // OBJECTINSTANCES_H