basic qt widgets support
This commit is contained in:
@@ -48,6 +48,12 @@ add_executable(qml-sg-mcp-bridge
|
||||
src/quickinspector_proxy.cpp
|
||||
src/quickinspector_proxy.h
|
||||
src/quickinspector_types.h
|
||||
src/property_reader.cpp
|
||||
src/property_reader.h
|
||||
src/widget_tools.cpp
|
||||
src/widget_tools.h
|
||||
src/widget_inspector_proxy.cpp
|
||||
src/widget_inspector_proxy.h
|
||||
)
|
||||
|
||||
target_link_libraries(qml-sg-mcp-bridge PRIVATE
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "gammaray_session.h"
|
||||
|
||||
#include "quickinspector_proxy.h"
|
||||
#include "widget_inspector_proxy.h"
|
||||
|
||||
#include <client/clientconnectionmanager.h>
|
||||
#include <common/objectbroker.h>
|
||||
@@ -39,6 +40,9 @@ GammaRaySession::GammaRaySession(QObject *parent)
|
||||
// GammaRay's Endpoint cannot dispatch the signal.
|
||||
GammaRay::registerQuickInspectorProxy();
|
||||
|
||||
// Register a minimal WidgetInspectorInterface proxy similarly.
|
||||
GammaRay::registerWidgetInspectorProxy();
|
||||
|
||||
// Pre-fetch the remote models we expose as tools. ObjectBroker::model()
|
||||
// lazily creates a RemoteModel which then syncs asynchronously from the
|
||||
// probe (~1-2s for the first rows). Requesting them now at ready() means
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
#include "gammaray_session.h"
|
||||
#include "scenegraph_tools.h"
|
||||
#include "widget_tools.h"
|
||||
|
||||
#include <common/endpoint.h>
|
||||
|
||||
@@ -70,9 +71,9 @@ int main(int argc, char **argv)
|
||||
|
||||
QMcpServer server(QStringLiteral("stdio"));
|
||||
server.setInstructions(QStringLiteral(
|
||||
"QML SceneGraph introspection via GammaRay. Connects to a GammaRay probe "
|
||||
"injected into a Qt/QML application and exposes the scene graph, items, "
|
||||
"geometry, materials and shaders as tools."));
|
||||
"QML SceneGraph & Qt Widget introspection via GammaRay. Connects to a "
|
||||
"GammaRay probe injected into a Qt/QML application and exposes the scene "
|
||||
"graph, QML items, widgets, geometry, materials and shaders as tools."));
|
||||
|
||||
SceneGraphTools tools(&session, &server);
|
||||
server.registerToolSet(&tools, {
|
||||
@@ -115,8 +116,20 @@ int main(int argc, char **argv)
|
||||
{ QStringLiteral("setRenderMode/mode"), QStringLiteral("Render mode: NormalRendering, VisualizeClipping, VisualizeOverdraw, VisualizeBatches, VisualizeChanges, or VisualizeTraces") },
|
||||
{ QStringLiteral("setSlowMode"), QStringLiteral("Toggle slow animations mode (renders continuously instead of on-demand)") },
|
||||
{ QStringLiteral("setSlowMode/enabled"), QStringLiteral("true to enable slow mode, false to disable") },
|
||||
// Widget tree navigation
|
||||
{ QStringLiteral("listWidgets"), QStringLiteral("List the QWidget hierarchy (widget types, names, visibility)") },
|
||||
// Widget selection + properties
|
||||
{ QStringLiteral("selectWidget"), QStringLiteral("Select a widget by address (from listWidgets) so widget properties populate for it") },
|
||||
{ QStringLiteral("selectWidget/address"), QStringLiteral("Widget address from listWidgets") },
|
||||
{ QStringLiteral("getWidgetProperties"), QStringLiteral("Get all Q_PROPERTY values for a selected widget (geometry, font, palette, etc.)") },
|
||||
{ QStringLiteral("getWidgetProperties/address"), QStringLiteral("Widget address (from listWidgets)") },
|
||||
{ QStringLiteral("getWidgetAttributes"), QStringLiteral("Get Qt::WidgetAttribute flags (acceptDrops, enabled, etc.) for a selected widget") },
|
||||
{ QStringLiteral("getWidgetAttributes/address"), QStringLiteral("Widget address (from listWidgets)") },
|
||||
});
|
||||
|
||||
WidgetTools widgetTools(&session, &server);
|
||||
server.registerToolSet(&widgetTools, {});
|
||||
|
||||
QObject::connect(&server, &QMcpServer::finished, &app, &QCoreApplication::quit);
|
||||
|
||||
if (probeUrl.isValid()) {
|
||||
|
||||
142
bridge/src/property_reader.cpp
Normal file
142
bridge/src/property_reader.cpp
Normal file
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
property_reader.cpp — shared AggregatedPropertyModel reader.
|
||||
Extracted from scenegraph_tools.cpp getItemProperties().
|
||||
|
||||
SPDX-FileCopyrightText: 2026 The GammaRay MCP Bridge Authors
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "property_reader.h"
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QCoreApplication>
|
||||
#include <QEventLoop>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QTimer>
|
||||
#include <QVariant>
|
||||
|
||||
// Poll until column-0 data arrives for at least one row (RemoteModel lazy-fetch).
|
||||
static void waitForPropertyData(QAbstractItemModel *m, int rows, int timeoutMs)
|
||||
{
|
||||
QEventLoop loop;
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs);
|
||||
bool gotData = false;
|
||||
while (std::chrono::steady_clock::now() < deadline && !gotData) {
|
||||
for (int r = 0; r < rows; ++r) {
|
||||
const auto val = m->data(m->index(r, 0), Qt::DisplayRole).toString();
|
||||
if (!val.isEmpty() && val != QStringLiteral("Loading...")) {
|
||||
gotData = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!gotData) {
|
||||
QTimer::singleShot(200, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject readAggregatedPropertyModel(QAbstractItemModel *m)
|
||||
{
|
||||
if (!m)
|
||||
return {};
|
||||
|
||||
// Wait for rows to become available (RemoteModel lazy-fetch).
|
||||
{
|
||||
QEventLoop loop;
|
||||
int waited = 0;
|
||||
const int step = 150;
|
||||
while (waited < 3000) {
|
||||
if (m->rowCount() > 0)
|
||||
break;
|
||||
QTimer::singleShot(step, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
waited += step;
|
||||
}
|
||||
}
|
||||
|
||||
const int rows = m->rowCount();
|
||||
if (rows == 0)
|
||||
return {};
|
||||
|
||||
waitForPropertyData(m, rows, 5000);
|
||||
|
||||
const int actualRows = m->rowCount();
|
||||
const int cols = m->columnCount();
|
||||
|
||||
// Pre-fetch children for grouped properties.
|
||||
for (int r = 0; r < actualRows; ++r)
|
||||
m->rowCount({ m->index(r, 0) });
|
||||
|
||||
if (actualRows > 0) {
|
||||
QEventLoop l2;
|
||||
QTimer::singleShot(500, &l2, &QEventLoop::quit);
|
||||
l2.exec();
|
||||
}
|
||||
|
||||
for (int r = 0; r < actualRows; ++r) {
|
||||
const QModelIndex idx0 = m->index(r, 0);
|
||||
if (m->hasChildren(idx0) && m->canFetchMore(idx0))
|
||||
m->fetchMore(idx0);
|
||||
}
|
||||
|
||||
if (actualRows > 0) {
|
||||
QEventLoop l2b;
|
||||
QTimer::singleShot(500, &l2b, &QEventLoop::quit);
|
||||
l2b.exec();
|
||||
}
|
||||
|
||||
QJsonObject simpleProps;
|
||||
QJsonObject groups;
|
||||
|
||||
for (int r = 0; r < actualRows; ++r) {
|
||||
const QString name = m->data(m->index(r, 0), Qt::DisplayRole).toString();
|
||||
const QString val = m->data(m->index(r, 1), Qt::DisplayRole).toString();
|
||||
const QString ptype = cols > 2 ? m->data(m->index(r, 2), Qt::DisplayRole).toString() : QString();
|
||||
|
||||
QJsonObject prop;
|
||||
prop.insert(QStringLiteral("value"), val);
|
||||
prop.insert(QStringLiteral("type"), ptype);
|
||||
|
||||
const QModelIndex idx0 = m->index(r, 0);
|
||||
if (m->hasChildren(idx0)) {
|
||||
const int childRows = m->rowCount(idx0);
|
||||
if (childRows > 0) {
|
||||
for (int cr = 0; cr < childRows; ++cr) {
|
||||
m->data(m->index(cr, 0, idx0), Qt::DisplayRole);
|
||||
m->data(m->index(cr, 1, idx0), Qt::DisplayRole);
|
||||
}
|
||||
{
|
||||
QEventLoop l3;
|
||||
QTimer::singleShot(500, &l3, &QEventLoop::quit);
|
||||
l3.exec();
|
||||
}
|
||||
QJsonObject children;
|
||||
for (int cr = 0; cr < childRows; ++cr) {
|
||||
const QString cname = m->data(m->index(cr, 0, idx0), Qt::DisplayRole).toString();
|
||||
const QString cval = m->data(m->index(cr, 1, idx0), Qt::DisplayRole).toString();
|
||||
const QString ctype = cols > 2 ? m->data(m->index(cr, 2, idx0), Qt::DisplayRole).toString() : QString();
|
||||
if (cname.isEmpty() || cname == QStringLiteral("Loading..."))
|
||||
continue;
|
||||
QJsonObject child;
|
||||
child.insert(QStringLiteral("value"), cval);
|
||||
child.insert(QStringLiteral("type"), ctype);
|
||||
children.insert(cname, child);
|
||||
}
|
||||
if (!children.isEmpty())
|
||||
prop.insert(QStringLiteral("children"), children);
|
||||
}
|
||||
groups.insert(name, prop);
|
||||
} else {
|
||||
simpleProps.insert(name, prop);
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject out;
|
||||
out.insert(QStringLiteral("properties"), simpleProps);
|
||||
if (!groups.isEmpty())
|
||||
out.insert(QStringLiteral("groups"), groups);
|
||||
return out;
|
||||
}
|
||||
16
bridge/src/property_reader.h
Normal file
16
bridge/src/property_reader.h
Normal file
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
property_reader.h — shared AggregatedPropertyModel reader for QML items and widgets.
|
||||
|
||||
SPDX-FileCopyrightText: 2026 The GammaRay MCP Bridge Authors
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef PROPERTY_READER_H
|
||||
#define PROPERTY_READER_H
|
||||
|
||||
class QAbstractItemModel;
|
||||
class QJsonObject;
|
||||
|
||||
QJsonObject readAggregatedPropertyModel(QAbstractItemModel *m);
|
||||
|
||||
#endif // PROPERTY_READER_H
|
||||
@@ -7,7 +7,9 @@
|
||||
|
||||
#include "quickinspector_proxy.h"
|
||||
|
||||
#include <common/endpoint.h>
|
||||
#include <common/objectbroker.h>
|
||||
#include <common/protocol.h>
|
||||
|
||||
#include <QMetaType>
|
||||
|
||||
@@ -32,6 +34,16 @@ void registerQuickInspectorProxy()
|
||||
static bool registered = false;
|
||||
if (registered)
|
||||
return;
|
||||
|
||||
// Only register if the QuickInspector plugin is actually active in the
|
||||
// target process. Check via QuickWindowModel address.
|
||||
auto *ep = Endpoint::instance();
|
||||
if (!ep)
|
||||
return;
|
||||
if (ep->objectAddress(QStringLiteral("com.kdab.GammaRay.QuickWindowModel"))
|
||||
== Protocol::InvalidObjectAddress)
|
||||
return;
|
||||
|
||||
registered = true;
|
||||
|
||||
auto *proxy = new QuickInspectorProxy();
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "scenegraph_tools.h"
|
||||
#include "gammaray_session.h"
|
||||
#include "material_interface.h"
|
||||
#include "property_reader.h"
|
||||
#include "quickinspector_types.h"
|
||||
|
||||
#include <endpoint.h>
|
||||
@@ -40,6 +41,19 @@ static constexpr int kIsCoordinateRole = 257;
|
||||
static constexpr int kDrawingModeRole = 257;
|
||||
static constexpr int kRenderRole = 258;
|
||||
|
||||
// Returns a non-empty error string if the QuickInspector is not active.
|
||||
// Guards QML-specific tools against widget-only target processes.
|
||||
static QString ensureQuickInspector()
|
||||
{
|
||||
auto *ep = GammaRay::Endpoint::instance();
|
||||
if (!ep)
|
||||
return QStringLiteral("no GammaRay endpoint");
|
||||
if (ep->objectAddress(QString::fromUtf8(kQuickInspectorIface))
|
||||
== GammaRay::Protocol::InvalidObjectAddress)
|
||||
return QStringLiteral("QuickInspector not active — target app has no QQuickWindow");
|
||||
return {};
|
||||
}
|
||||
|
||||
// --- async helpers (from earlier scaffold) ---
|
||||
|
||||
static void waitForData(const QAbstractItemModel *m, int column, int timeoutMs)
|
||||
@@ -324,6 +338,8 @@ QString SceneGraphTools::listQuickWindows() const
|
||||
{
|
||||
if (const QString e = ensureSession(); !e.isEmpty())
|
||||
return e;
|
||||
if (const QString e = ensureQuickInspector(); !e.isEmpty())
|
||||
return QString::fromUtf8(QJsonDocument(errorJson(e)).toJson(QJsonDocument::Compact));
|
||||
|
||||
auto *m = m_session->model(QString::fromUtf8(kQuickWindowModel));
|
||||
if (!m)
|
||||
@@ -348,6 +364,8 @@ QString SceneGraphTools::selectQuickWindow(int index) const
|
||||
{
|
||||
if (const QString e = ensureSession(); !e.isEmpty())
|
||||
return e;
|
||||
if (const QString e = ensureQuickInspector(); !e.isEmpty())
|
||||
return QString::fromUtf8(QJsonDocument(errorJson(e)).toJson(QJsonDocument::Compact));
|
||||
|
||||
auto *ep = GammaRay::Endpoint::instance();
|
||||
if (!ep)
|
||||
@@ -367,6 +385,8 @@ QString SceneGraphTools::listQuickItems() const
|
||||
{
|
||||
if (const QString e = ensureSession(); !e.isEmpty())
|
||||
return e;
|
||||
if (const QString e = ensureQuickInspector(); !e.isEmpty())
|
||||
return QString::fromUtf8(QJsonDocument(errorJson(e)).toJson(QJsonDocument::Compact));
|
||||
|
||||
auto *m = m_session->model(QString::fromUtf8(kQuickItemModel));
|
||||
if (!m)
|
||||
@@ -384,6 +404,8 @@ QString SceneGraphTools::listScenegraphNodes() const
|
||||
{
|
||||
if (const QString e = ensureSession(); !e.isEmpty())
|
||||
return e;
|
||||
if (const QString e = ensureQuickInspector(); !e.isEmpty())
|
||||
return QString::fromUtf8(QJsonDocument(errorJson(e)).toJson(QJsonDocument::Compact));
|
||||
|
||||
auto *m = m_session->model(QString::fromUtf8(kQuickSceneGraphModel));
|
||||
if (!m)
|
||||
@@ -444,6 +466,8 @@ QString SceneGraphTools::ensureNodeSelected(const QString &address) const
|
||||
{
|
||||
if (const QString e = ensureSession(); !e.isEmpty())
|
||||
return e;
|
||||
if (const QString e = ensureQuickInspector(); !e.isEmpty())
|
||||
return e;
|
||||
|
||||
if (address.isEmpty())
|
||||
return QString::fromUtf8(QJsonDocument(errorJson(
|
||||
@@ -739,6 +763,8 @@ QString SceneGraphTools::ensureItemSelected(const QString &address) const
|
||||
{
|
||||
if (const QString e = ensureSession(); !e.isEmpty())
|
||||
return e;
|
||||
if (const QString e = ensureQuickInspector(); !e.isEmpty())
|
||||
return e;
|
||||
|
||||
if (address.isEmpty())
|
||||
return QString::fromUtf8(QJsonDocument(errorJson(
|
||||
@@ -840,127 +866,12 @@ QString SceneGraphTools::getItemProperties(const QString &address) const
|
||||
return QString::fromUtf8(QJsonDocument(errorJson(
|
||||
QStringLiteral("item property model not available"))).toJson(QJsonDocument::Compact));
|
||||
|
||||
// Item property properties model has 4 columns:
|
||||
// 0 = property name, 1 = value (display string), 2 = type name, 3 = class name
|
||||
waitForRows(m, 3000);
|
||||
|
||||
const int rows = m->rowCount();
|
||||
if (rows == 0)
|
||||
QJsonObject out = readAggregatedPropertyModel(m);
|
||||
if (out.isEmpty())
|
||||
return QString::fromUtf8(QJsonDocument(errorJson(
|
||||
QStringLiteral("no properties available (did you select a valid QML item?)")))
|
||||
.toJson(QJsonDocument::Compact));
|
||||
|
||||
// The RemoteModel uses lazy-fetch: data() triggers a fetch and returns
|
||||
// "Loading..." until the fetch response arrives on the event loop.
|
||||
// Poll until column 0 data arrives for at least one row.
|
||||
{
|
||||
QEventLoop loop;
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
bool gotData = false;
|
||||
while (std::chrono::steady_clock::now() < deadline && !gotData) {
|
||||
for (int r = 0; r < rows; ++r) {
|
||||
const QString val = m->data(m->index(r, 0), Qt::DisplayRole).toString();
|
||||
if (!val.isEmpty() && val != QStringLiteral("Loading...")) {
|
||||
gotData = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!gotData) {
|
||||
QTimer::singleShot(200, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-read rowCount after polling (more rows may have arrived)
|
||||
const int actualRows = m->rowCount();
|
||||
const int cols = m->columnCount();
|
||||
|
||||
QJsonObject out;
|
||||
QStringList headers;
|
||||
for (int c = 0; c < cols; ++c)
|
||||
headers.append(m->headerData(c, Qt::Horizontal, Qt::DisplayRole).toString());
|
||||
|
||||
// Pre-fetch children for grouped properties. RemoteModel uses lazy-fetch:
|
||||
// rowCount(parent) triggers an async request and returns 0 until the reply
|
||||
// arrives on the event loop. After polling, hasChildren() works correctly.
|
||||
for (int r = 0; r < actualRows; ++r) {
|
||||
m->rowCount(m->index(r, 0));
|
||||
}
|
||||
// Poll to let row count responses arrive
|
||||
if (actualRows > 0) {
|
||||
QEventLoop l2;
|
||||
QTimer::singleShot(500, &l2, &QEventLoop::quit);
|
||||
l2.exec();
|
||||
}
|
||||
// Second pass: ensure child data is fetched for rows that have children
|
||||
for (int r = 0; r < actualRows; ++r) {
|
||||
const QModelIndex idx0 = m->index(r, 0);
|
||||
if (m->hasChildren(idx0) && m->canFetchMore(idx0))
|
||||
m->fetchMore(idx0);
|
||||
}
|
||||
if (actualRows > 0) {
|
||||
QEventLoop l2b;
|
||||
QTimer::singleShot(500, &l2b, &QEventLoop::quit);
|
||||
l2b.exec();
|
||||
}
|
||||
|
||||
// Top-level properties (simple Q_PROPERTY values like x, y, width, height...)
|
||||
QJsonObject simpleProps;
|
||||
// Nested property groups (anchors, transform, etc.) will have children.
|
||||
QJsonObject groups;
|
||||
for (int r = 0; r < actualRows; ++r) {
|
||||
const QString name = m->data(m->index(r, 0), Qt::DisplayRole).toString();
|
||||
const QString val = m->data(m->index(r, 1), Qt::DisplayRole).toString();
|
||||
const QString ptype = cols > 2 ? m->data(m->index(r, 2), Qt::DisplayRole).toString() : QString();
|
||||
|
||||
QJsonObject prop;
|
||||
prop.insert(QStringLiteral("value"), val);
|
||||
prop.insert(QStringLiteral("type"), ptype);
|
||||
|
||||
const QModelIndex idx0 = m->index(r, 0);
|
||||
if (m->hasChildren(idx0)) {
|
||||
// For grouped properties (anchors, transform, etc.), collect children.
|
||||
// Children also use lazy-fetch, so poll if needed.
|
||||
const int childRows = m->rowCount(idx0);
|
||||
if (childRows > 0) {
|
||||
// Trigger child data fetches first
|
||||
for (int cr = 0; cr < childRows; ++cr) {
|
||||
m->data(m->index(cr, 0, idx0), Qt::DisplayRole);
|
||||
m->data(m->index(cr, 1, idx0), Qt::DisplayRole);
|
||||
}
|
||||
// Poll to let child data arrive
|
||||
{
|
||||
QEventLoop l3;
|
||||
QTimer::singleShot(500, &l3, &QEventLoop::quit);
|
||||
l3.exec();
|
||||
}
|
||||
// Read actual child data
|
||||
QJsonObject children;
|
||||
for (int cr = 0; cr < childRows; ++cr) {
|
||||
const QString cname = m->data(m->index(cr, 0, idx0), Qt::DisplayRole).toString();
|
||||
const QString cval = m->data(m->index(cr, 1, idx0), Qt::DisplayRole).toString();
|
||||
const QString ctype = cols > 2 ? m->data(m->index(cr, 2, idx0), Qt::DisplayRole).toString() : QString();
|
||||
if (cname.isEmpty() || cname == QStringLiteral("Loading..."))
|
||||
continue;
|
||||
QJsonObject child;
|
||||
child.insert(QStringLiteral("value"), cval);
|
||||
child.insert(QStringLiteral("type"), ctype);
|
||||
children.insert(cname, child);
|
||||
}
|
||||
if (!children.isEmpty())
|
||||
prop.insert(QStringLiteral("children"), children);
|
||||
}
|
||||
groups.insert(name, prop);
|
||||
} else {
|
||||
simpleProps.insert(name, prop);
|
||||
}
|
||||
}
|
||||
|
||||
out.insert(QStringLiteral("properties"), simpleProps);
|
||||
if (!groups.isEmpty())
|
||||
out.insert(QStringLiteral("groups"), groups);
|
||||
|
||||
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
|
||||
@@ -968,6 +879,8 @@ QString SceneGraphTools::setRenderMode(const QString &mode) const
|
||||
{
|
||||
if (const QString e = ensureSession(); !e.isEmpty())
|
||||
return e;
|
||||
if (const QString e = ensureQuickInspector(); !e.isEmpty())
|
||||
return QString::fromUtf8(QJsonDocument(errorJson(e)).toJson(QJsonDocument::Compact));
|
||||
|
||||
auto *ep = GammaRay::Endpoint::instance();
|
||||
if (!ep)
|
||||
@@ -1009,6 +922,8 @@ QString SceneGraphTools::setSlowMode(bool enabled) const
|
||||
{
|
||||
if (const QString e = ensureSession(); !e.isEmpty())
|
||||
return e;
|
||||
if (const QString e = ensureQuickInspector(); !e.isEmpty())
|
||||
return QString::fromUtf8(QJsonDocument(errorJson(e)).toJson(QJsonDocument::Compact));
|
||||
|
||||
auto *ep = GammaRay::Endpoint::instance();
|
||||
if (!ep)
|
||||
|
||||
49
bridge/src/widget_inspector_proxy.cpp
Normal file
49
bridge/src/widget_inspector_proxy.cpp
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
widget_inspector_proxy.cpp — Minimal WidgetInspectorInterface client proxy.
|
||||
|
||||
SPDX-FileCopyrightText: 2026 The GammaRay MCP Bridge Authors
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "widget_inspector_proxy.h"
|
||||
|
||||
#include <common/endpoint.h>
|
||||
#include <common/objectbroker.h>
|
||||
#include <common/protocol.h>
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
|
||||
namespace GammaRay {
|
||||
|
||||
WidgetInspectorProxy::WidgetInspectorProxy(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void registerWidgetInspectorProxy()
|
||||
{
|
||||
static bool registered = false;
|
||||
if (registered)
|
||||
return;
|
||||
|
||||
auto *ep = Endpoint::instance();
|
||||
if (!ep)
|
||||
return;
|
||||
|
||||
// Only register if the widget inspector plugin is actually active in the
|
||||
// target process. The QuickInspector plugin activates for any QQuickWindow
|
||||
// app, but WidgetInspector requires QWidget/QApplication. Check via the
|
||||
// WidgetTree model address — if it's valid, the plugin is loaded.
|
||||
if (ep->objectAddress(QStringLiteral("com.kdab.GammaRay.WidgetTree"))
|
||||
== Protocol::InvalidObjectAddress)
|
||||
return; // widget inspector not active — skip registration
|
||||
|
||||
registered = true;
|
||||
|
||||
auto *proxy = new WidgetInspectorProxy();
|
||||
ObjectBroker::registerObject(
|
||||
QStringLiteral("com.kdab.GammaRay.WidgetInspector"),
|
||||
proxy);
|
||||
}
|
||||
|
||||
} // namespace GammaRay
|
||||
33
bridge/src/widget_inspector_proxy.h
Normal file
33
bridge/src/widget_inspector_proxy.h
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
widget_inspector_proxy.h — Minimal WidgetInspectorInterface client proxy.
|
||||
|
||||
SPDX-FileCopyrightText: 2026 The GammaRay MCP Bridge Authors
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
The probe emits WidgetInspectorInterface signals without a matching slot.
|
||||
This proxy registers with the IID so GammaRay's Endpoint can dispatch signals.
|
||||
*/
|
||||
|
||||
#ifndef WIDGET_INSPECTOR_PROXY_H
|
||||
#define WIDGET_INSPECTOR_PROXY_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
namespace GammaRay {
|
||||
|
||||
class WidgetInspectorProxy : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_CLASSINFO("IID", "com.kdab.GammaRay.WidgetInspector")
|
||||
public:
|
||||
explicit WidgetInspectorProxy(QObject *parent = nullptr);
|
||||
|
||||
public slots:
|
||||
void featuresChanged() {}
|
||||
};
|
||||
|
||||
void registerWidgetInspectorProxy();
|
||||
|
||||
} // namespace GammaRay
|
||||
|
||||
#endif // WIDGET_INSPECTOR_PROXY_H
|
||||
364
bridge/src/widget_tools.cpp
Normal file
364
bridge/src/widget_tools.cpp
Normal file
@@ -0,0 +1,364 @@
|
||||
/*
|
||||
widget_tools.cpp — Qt Widget introspection tools via GammaRay.
|
||||
|
||||
SPDX-FileCopyrightText: 2026 The GammaRay MCP Bridge Authors
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "widget_tools.h"
|
||||
#include "gammaray_session.h"
|
||||
#include "property_reader.h"
|
||||
|
||||
#include <endpoint.h>
|
||||
#include <objectbroker.h>
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QCoreApplication>
|
||||
#include <QEventLoop>
|
||||
#include <QItemSelectionModel>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QTimer>
|
||||
|
||||
static const char *kWidgetTreeModel = "com.kdab.GammaRay.WidgetTree";
|
||||
static const char *kWidgetBaseName = "com.kdab.GammaRay.WidgetInspector";
|
||||
|
||||
static constexpr int kWidgetFlagsRole = 261;
|
||||
|
||||
static QJsonObject errorJson(const QString &msg)
|
||||
{
|
||||
return { { QStringLiteral("error"), msg } };
|
||||
}
|
||||
|
||||
static void settle(QEventLoop &loop, int ms)
|
||||
{
|
||||
QTimer::singleShot(ms, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
}
|
||||
|
||||
static void primeTree(const QAbstractItemModel *m, const QModelIndex &parent, int depth)
|
||||
{
|
||||
if (depth <= 0)
|
||||
return;
|
||||
for (int r = 0; r < m->rowCount(parent); ++r) {
|
||||
const QModelIndex idx = m->index(r, 0, parent);
|
||||
m->data(idx, Qt::DisplayRole);
|
||||
m->data(m->index(r, 1, parent), Qt::DisplayRole);
|
||||
if (m->hasChildren(idx))
|
||||
primeTree(m, idx, depth - 1);
|
||||
}
|
||||
}
|
||||
|
||||
static int countLoading(const QAbstractItemModel *m, const QModelIndex &parent, int depth)
|
||||
{
|
||||
if (depth <= 0)
|
||||
return 0;
|
||||
int count = 0;
|
||||
for (int r = 0; r < m->rowCount(parent); ++r) {
|
||||
const QModelIndex idx = m->index(r, 0, parent);
|
||||
if (m->data(idx, Qt::DisplayRole).toString() == QLatin1String("Loading..."))
|
||||
++count;
|
||||
if (m->hasChildren(idx))
|
||||
count += countLoading(m, idx, depth - 1);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static void primeAndWait(const QAbstractItemModel *m, int maxRounds, int settleMs, int quietMs,
|
||||
int finalSettleMs = 2000)
|
||||
{
|
||||
QEventLoop loop;
|
||||
for (int round = 0; round < maxRounds; ++round) {
|
||||
bool signalActivity = false;
|
||||
const auto dcConn = QObject::connect(m, &QAbstractItemModel::dataChanged,
|
||||
[&signalActivity]() { signalActivity = true; });
|
||||
const auto riConn = QObject::connect(m, &QAbstractItemModel::rowsInserted,
|
||||
[&signalActivity]() { signalActivity = true; });
|
||||
|
||||
primeTree(m, QModelIndex(), 16);
|
||||
|
||||
int quiet = 0;
|
||||
while (quiet < quietMs) {
|
||||
signalActivity = false;
|
||||
QTimer::singleShot(settleMs, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
quiet += settleMs;
|
||||
if (signalActivity)
|
||||
quiet = 0;
|
||||
}
|
||||
QObject::disconnect(dcConn);
|
||||
QObject::disconnect(riConn);
|
||||
|
||||
if (countLoading(m, QModelIndex(), 16) == 0)
|
||||
return;
|
||||
}
|
||||
|
||||
if (countLoading(m, QModelIndex(), 16) > 0) {
|
||||
settle(loop, finalSettleMs);
|
||||
primeTree(m, QModelIndex(), 16);
|
||||
settle(loop, finalSettleMs / 2);
|
||||
}
|
||||
}
|
||||
|
||||
static QModelIndex findWidgetInTree(const QAbstractItemModel *m, const QModelIndex &parent,
|
||||
const QString &address, int depth, QEventLoop &loop)
|
||||
{
|
||||
if (depth <= 0)
|
||||
return {};
|
||||
for (int r = 0; r < m->rowCount(parent); ++r) {
|
||||
const QModelIndex idx = m->index(r, 0, parent);
|
||||
const QString addr = m->data(idx, Qt::DisplayRole).toString();
|
||||
if (addr == address)
|
||||
return idx;
|
||||
if (addr == QLatin1String("Loading...")) {
|
||||
settle(loop, 300);
|
||||
if (m->data(idx, Qt::DisplayRole).toString() == address)
|
||||
return idx;
|
||||
}
|
||||
if (m->hasChildren(idx)) {
|
||||
m->rowCount(idx);
|
||||
settle(loop, 200);
|
||||
const auto found = findWidgetInTree(m, idx, address, depth - 1, loop);
|
||||
if (found.isValid())
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
static QJsonArray walkWidgetChildren(const QAbstractItemModel *m, const QModelIndex &parent, int depth)
|
||||
{
|
||||
QJsonArray out;
|
||||
if (depth <= 0)
|
||||
return out;
|
||||
for (int r = 0; r < m->rowCount(parent); ++r) {
|
||||
const QModelIndex idx = m->index(r, 0, parent);
|
||||
const QString name = m->data(idx, Qt::DisplayRole).toString();
|
||||
const QModelIndex typeIdx = m->index(r, 1, parent);
|
||||
const QString type = m->data(typeIdx, Qt::DisplayRole).toString();
|
||||
const int flags = m->data(idx, kWidgetFlagsRole).toInt();
|
||||
QJsonObject node;
|
||||
node.insert(QStringLiteral("name"), name);
|
||||
node.insert(QStringLiteral("type"), type);
|
||||
if (flags & 1)
|
||||
node.insert(QStringLiteral("invisible"), true);
|
||||
if (m->hasChildren(idx))
|
||||
node.insert(QStringLiteral("children"), walkWidgetChildren(m, idx, depth - 1));
|
||||
out.append(node);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// --- WidgetTools ---
|
||||
|
||||
WidgetTools::WidgetTools(GammaRaySession *session, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_session(session)
|
||||
{
|
||||
}
|
||||
|
||||
QString WidgetTools::ensureSession() const
|
||||
{
|
||||
if (!m_session)
|
||||
return QString::fromUtf8(QJsonDocument(errorJson(
|
||||
QStringLiteral("no session"))).toJson(QJsonDocument::Compact));
|
||||
if (m_session->lastUrl().isValid())
|
||||
m_session->ensureConnected(6000);
|
||||
if (m_session->isReady())
|
||||
return {};
|
||||
QJsonObject err = errorJson(
|
||||
m_session->lastUrl().isValid()
|
||||
? QStringLiteral("not connected to probe (last error: %1; call connectProbe to retry)")
|
||||
.arg(m_session->lastError().isEmpty() ? QStringLiteral("timeout") : m_session->lastError())
|
||||
: QStringLiteral("no probe URL configured — call connectProbe(host, port) first"));
|
||||
err.insert(QStringLiteral("state"), m_session->stateString());
|
||||
return QString::fromUtf8(QJsonDocument(err).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
|
||||
void WidgetTools::waitForRows(QAbstractItemModel *m, int timeoutMs) const
|
||||
{
|
||||
if (!m)
|
||||
return;
|
||||
QEventLoop loop;
|
||||
int waited = 0;
|
||||
const int step = 150;
|
||||
while (waited < timeoutMs) {
|
||||
if (m->rowCount() > 0)
|
||||
return;
|
||||
QTimer::singleShot(step, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
waited += step;
|
||||
}
|
||||
}
|
||||
|
||||
QModelIndex WidgetTools::findWidgetByAddress(QAbstractItemModel *m, const QString &address) const
|
||||
{
|
||||
if (!m || address.isEmpty())
|
||||
return {};
|
||||
|
||||
primeAndWait(m, 6, 300, 500);
|
||||
|
||||
QEventLoop loop;
|
||||
const auto idx = findWidgetInTree(m, QModelIndex(), address, 16, loop);
|
||||
if (idx.isValid())
|
||||
return idx;
|
||||
|
||||
for (int round = 0; round < 3; ++round) {
|
||||
primeAndWait(m, 2, 300, 500);
|
||||
const auto idx2 = findWidgetInTree(m, QModelIndex(), address, 16, loop);
|
||||
if (idx2.isValid())
|
||||
return idx2;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QString WidgetTools::ensureWidgetSelected(const QString &address) const
|
||||
{
|
||||
if (const QString e = ensureSession(); !e.isEmpty())
|
||||
return e;
|
||||
|
||||
if (address.isEmpty())
|
||||
return QString::fromUtf8(QJsonDocument(errorJson(
|
||||
QStringLiteral("address is required"))).toJson(QJsonDocument::Compact));
|
||||
|
||||
if (address == m_selectedWidgetAddress)
|
||||
return {};
|
||||
|
||||
auto *treeModel = m_session->model(QString::fromUtf8(kWidgetTreeModel));
|
||||
if (!treeModel)
|
||||
return QString::fromUtf8(QJsonDocument(errorJson(
|
||||
QStringLiteral("WidgetTree model not available (no QWidget app attached?)")))
|
||||
.toJson(QJsonDocument::Compact));
|
||||
|
||||
const QModelIndex idx = findWidgetByAddress(treeModel, address);
|
||||
if (!idx.isValid())
|
||||
return QString::fromUtf8(QJsonDocument(errorJson(
|
||||
QStringLiteral("widget %1 not found in widget tree (still loading?)").arg(address)))
|
||||
.toJson(QJsonDocument::Compact));
|
||||
|
||||
auto *selModel = m_session->selectionModel(treeModel);
|
||||
|
||||
auto *propModel = m_session->model(QStringLiteral("%1.properties").arg(QString::fromUtf8(kWidgetBaseName)));
|
||||
|
||||
QEventLoop loop;
|
||||
settle(loop, 300);
|
||||
|
||||
if (selModel && idx.isValid())
|
||||
selModel->select(idx, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows | QItemSelectionModel::Current);
|
||||
|
||||
settle(loop, 2000);
|
||||
|
||||
m_selectedWidgetAddress = address;
|
||||
|
||||
if (propModel) {
|
||||
propModel->rowCount();
|
||||
settle(loop, 1500);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
QString WidgetTools::listWidgets() const
|
||||
{
|
||||
if (const QString e = ensureSession(); !e.isEmpty())
|
||||
return e;
|
||||
|
||||
auto *m = m_session->model(QString::fromUtf8(kWidgetTreeModel));
|
||||
if (!m)
|
||||
return QString::fromUtf8(QJsonDocument(errorJson(
|
||||
QStringLiteral("WidgetTree model not available (no QWidget app attached?)")))
|
||||
.toJson(QJsonDocument::Compact));
|
||||
|
||||
primeAndWait(m, 6, 300, 500);
|
||||
|
||||
const QJsonArray tree = walkWidgetChildren(m, QModelIndex(), 16);
|
||||
return QString::fromUtf8(QJsonDocument(tree).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
|
||||
QString WidgetTools::selectWidget(const QString &address) const
|
||||
{
|
||||
m_selectedWidgetAddress.clear();
|
||||
const QString e = ensureWidgetSelected(address);
|
||||
if (!e.isEmpty())
|
||||
return e;
|
||||
|
||||
auto *treeModel = m_session->model(QString::fromUtf8(kWidgetTreeModel));
|
||||
QJsonObject out;
|
||||
if (!treeModel) {
|
||||
out.insert(QStringLiteral("error"), QStringLiteral("WidgetTree model gone"));
|
||||
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
const QModelIndex idx = findWidgetByAddress(treeModel, address);
|
||||
if (!idx.isValid()) {
|
||||
out.insert(QStringLiteral("error"), QStringLiteral("address %1 not found after selection").arg(address));
|
||||
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
const QModelIndex typeIdx = treeModel->index(idx.row(), 1, idx.parent());
|
||||
const QString type = treeModel->data(typeIdx, Qt::DisplayRole).toString();
|
||||
int propertyCount = 0;
|
||||
auto *propModel = m_session->model(QStringLiteral("%1.properties").arg(QString::fromUtf8(kWidgetBaseName)));
|
||||
if (propModel)
|
||||
propertyCount = propModel->rowCount();
|
||||
|
||||
out.insert(QStringLiteral("selected"), address);
|
||||
out.insert(QStringLiteral("type"), type);
|
||||
out.insert(QStringLiteral("propertyCount"), propertyCount);
|
||||
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
|
||||
QString WidgetTools::getWidgetProperties(const QString &address) const
|
||||
{
|
||||
const QString e = ensureWidgetSelected(address);
|
||||
if (!e.isEmpty())
|
||||
return e;
|
||||
|
||||
auto *m = m_session->model(QStringLiteral("%1.properties").arg(QString::fromUtf8(kWidgetBaseName)));
|
||||
if (!m)
|
||||
return QString::fromUtf8(QJsonDocument(errorJson(
|
||||
QStringLiteral("widget property model not available"))).toJson(QJsonDocument::Compact));
|
||||
|
||||
QJsonObject out = readAggregatedPropertyModel(m);
|
||||
if (out.isEmpty())
|
||||
return QString::fromUtf8(QJsonDocument(errorJson(
|
||||
QStringLiteral("no properties available (did you select a valid widget?)")))
|
||||
.toJson(QJsonDocument::Compact));
|
||||
|
||||
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
|
||||
QString WidgetTools::getWidgetAttributes(const QString &address) const
|
||||
{
|
||||
const QString e = ensureWidgetSelected(address);
|
||||
if (!e.isEmpty())
|
||||
return e;
|
||||
|
||||
auto *m = m_session->model(QStringLiteral("%1.widgetAttributeModel").arg(QString::fromUtf8(kWidgetBaseName)));
|
||||
if (!m)
|
||||
return QString::fromUtf8(QJsonDocument(errorJson(
|
||||
QStringLiteral("widget attribute model not available"))).toJson(QJsonDocument::Compact));
|
||||
|
||||
waitForRows(m, 3000);
|
||||
|
||||
const int rows = m->rowCount();
|
||||
if (rows == 0)
|
||||
return QString::fromUtf8(QJsonDocument(errorJson(
|
||||
QStringLiteral("no widget attributes (did you select a valid widget?)")))
|
||||
.toJson(QJsonDocument::Compact));
|
||||
|
||||
QJsonObject attrs;
|
||||
for (int r = 0; r < rows; ++r) {
|
||||
const QString name = m->data(m->index(r, 0), Qt::DisplayRole).toString();
|
||||
const bool enabled = m->data(m->index(r, 1), Qt::DisplayRole).toBool();
|
||||
const QString val = m->data(m->index(r, 1), Qt::DisplayRole).toString();
|
||||
attrs.insert(name, QJsonObject{
|
||||
{ QStringLiteral("enabled"), enabled },
|
||||
{ QStringLiteral("value"), val },
|
||||
});
|
||||
}
|
||||
|
||||
QJsonObject out;
|
||||
out.insert(QStringLiteral("attributes"), attrs);
|
||||
out.insert(QStringLiteral("count"), rows);
|
||||
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
42
bridge/src/widget_tools.h
Normal file
42
bridge/src/widget_tools.h
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
widget_tools.h — Q_INVOKABLE methods for Qt Widget introspection via GammaRay.
|
||||
|
||||
SPDX-FileCopyrightText: 2026 The GammaRay MCP Bridge Authors
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef WIDGET_TOOLS_H
|
||||
#define WIDGET_TOOLS_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QModelIndex>
|
||||
|
||||
class GammaRaySession;
|
||||
|
||||
class WidgetTools : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit WidgetTools(GammaRaySession *session, QObject *parent = nullptr);
|
||||
|
||||
// --- Widget tree navigation ---
|
||||
Q_INVOKABLE QString listWidgets() const;
|
||||
// Select a widget by address, triggering property sub-model population.
|
||||
Q_INVOKABLE QString selectWidget(const QString &address) const;
|
||||
// Get all Q_PROPERTY values for a selected widget.
|
||||
Q_INVOKABLE QString getWidgetProperties(const QString &address) const;
|
||||
// Get Qt::WidgetAttribute flags for a selected widget.
|
||||
Q_INVOKABLE QString getWidgetAttributes(const QString &address) const;
|
||||
|
||||
private:
|
||||
GammaRaySession *m_session;
|
||||
mutable QString m_selectedWidgetAddress;
|
||||
|
||||
QString ensureSession() const;
|
||||
QString ensureWidgetSelected(const QString &address) const;
|
||||
QModelIndex findWidgetByAddress(QAbstractItemModel *m, const QString &address) const;
|
||||
void waitForRows(QAbstractItemModel *m, int timeoutMs) const;
|
||||
};
|
||||
|
||||
#endif // WIDGET_TOOLS_H
|
||||
Reference in New Issue
Block a user