diff options
author | Shawn Rutledge <shawn.rutledge@qt.io> | 2023-05-30 10:49:58 +0200 |
---|---|---|
committer | Shawn Rutledge <shawn.rutledge@qt.io> | 2023-07-05 19:40:34 +0000 |
commit | e6a2eb1eafdcdb9f493370fc211e483804a78184 (patch) | |
tree | 530145816f30f36be5011304ae93f32afe244322 | |
parent | 3f9f09855c3cb2075833446e448f4f699e62b0ed (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.qml | 148 | ||||
-rw-r--r-- | examples/hover.qml | 9 | ||||
-rw-r--r-- | examples/item2d.qml | 68 | ||||
-rw-r--r-- | examples/sidebar.qml | 204 | ||||
-rw-r--r-- | examples/windowBusyBox.qml | 31 | ||||
-rw-r--r-- | src/generators/CMakeLists.txt | 5 | ||||
-rw-r--r-- | src/generators/objectinstances.cpp | 286 | ||||
-rw-r--r-- | src/generators/objectinstances_p.h | 72 |
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 |