Files
gammaray-mcp/bridge/src/scenegraph_tools.cpp

255 lines
9.9 KiB
C++
Raw Normal View History

2026-07-06 20:32:47 +08:00
/*
scenegraph_tools.cpp
SPDX-FileCopyrightText: 2026 The GammaRay MCP Bridge Authors
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "scenegraph_tools.h"
#include "gammaray_session.h"
#include <common/endpoint.h>
#include <QAbstractItemModel>
#include <QCoreApplication>
#include <QEventLoop>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QTimer>
#include <QUrl>
#include <QVariantList>
// Model registration names (from plugins/quickinspector/quickinspector.cpp).
static const char *kQuickWindowModel = "com.kdab.GammaRay.QuickWindowModel";
static const char *kQuickSceneGraphModel = "com.kdab.GammaRay.QuickSceneGraphModel";
// RemoteModel populates asynchronously from the probe and fetches cell data
// lazily: data() returns a "Loading..." placeholder and queues a server fetch;
// the real value arrives later via dataChanged. So we must (a) trigger the
// fetches by calling data(), and (b) wait for the values to settle. We poll in
// a local event loop until the first column display is non-empty AND not the
// "Loading..." placeholder, or until the total budget elapses.
static void waitForData(const QAbstractItemModel *m, int column, int timeoutMs)
{
if (!m)
return;
QEventLoop loop;
int waited = 0;
const int step = 150;
while (waited < timeoutMs) {
if (m->rowCount() > 0) {
// Trigger/refresh the cell fetch for the first row's target column.
const auto v = m->data(m->index(0, column), Qt::DisplayRole).toString();
if (!v.isEmpty() && v != QLatin1String("Loading..."))
return;
}
QTimer::singleShot(step, &loop, &QEventLoop::quit);
loop.exec();
waited += step;
}
}
static QJsonObject errorJson(const QString &msg)
{
return { { QStringLiteral("error"), msg } };
}
// Recursive tree walker for tree-shaped remote models.
static QJsonArray walkChildren(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 address = m->data(idx, Qt::DisplayRole).toString();
const QModelIndex typeIdx = m->index(r, 1, parent);
const QString type = m->data(typeIdx, Qt::DisplayRole).toString();
QJsonObject node;
node.insert(QStringLiteral("address"), address);
node.insert(QStringLiteral("type"), type);
if (m->hasChildren(idx))
node.insert(QStringLiteral("children"), walkChildren(m, idx, depth - 1));
out.append(node);
}
return out;
}
// RemoteModel fetches cell data AND child row counts lazily from the probe:
// data()/rowCount()/hasChildren() trigger server fetches and return a
// placeholder ("Loading..." / 0 / false) until the response arrives. So to get
// a populated tree we must (1) prime: walk the model touching every cell and
// rowCount() to queue all fetches, (2) let the event loop run so responses
// arrive, (3) read. We repeat prime+settle a few rounds so deeper levels
// become reachable as their parents' row counts arrive.
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 void settle(QEventLoop &loop, int ms)
{
QTimer::singleShot(ms, &loop, &QEventLoop::quit);
loop.exec();
}
SceneGraphTools::SceneGraphTools(GammaRaySession *session, QObject *parent)
: QObject(parent)
, m_session(session)
{
}
QString SceneGraphTools::ensureSession() const
{
if (!m_session)
return QString::fromUtf8(QJsonDocument(errorJson(
QStringLiteral("no session"))).toJson(QJsonDocument::Compact));
// First, opportunistically bring up a connection if we have a URL.
if (m_session->lastUrl().isValid()) {
m_session->ensureConnected(6000);
}
if (m_session->isReady())
return {};
// No URL known OR reconnect failed: tell the user to call connectProbe.
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));
}
QString SceneGraphTools::connectProbe(const QString &host, int port) const
{
if (!m_session)
return QString::fromUtf8(QJsonDocument(errorJson(
QStringLiteral("no session"))).toJson(QJsonDocument::Compact));
const QString h = host.isEmpty() ? QStringLiteral("127.0.0.1") : host;
const int p = (port <= 0) ? 11732 : port;
const QUrl url(QStringLiteral("tcp://%1:%2").arg(h).arg(p));
m_session->connectToHost(url);
m_session->waitForReady(6000);
QJsonObject out = {
{ QStringLiteral("connected"), m_session->isReady() },
{ QStringLiteral("state"), m_session->stateString() },
{ QStringLiteral("url"), url.toString() },
};
if (!m_session->isReady())
out.insert(QStringLiteral("error"),
m_session->lastError().isEmpty()
? QStringLiteral("timeout connecting — is the probe listening on %1?").arg(url.toString())
: m_session->lastError());
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
}
QString SceneGraphTools::connectProbeDefault() const
{
return connectProbe(QStringLiteral("127.0.0.1"), 11732);
}
QString SceneGraphTools::disconnectProbe() const
{
if (!m_session)
return QString::fromUtf8(QJsonDocument(errorJson(
QStringLiteral("no session"))).toJson(QJsonDocument::Compact));
m_session->disconnectFromHost();
return probeStatus();
}
QString SceneGraphTools::probeStatus() const
{
if (!m_session)
return QString::fromUtf8(QJsonDocument(errorJson(
QStringLiteral("no session"))).toJson(QJsonDocument::Compact));
QJsonObject out = {
{ QStringLiteral("state"), m_session->stateString() },
{ QStringLiteral("ready"), m_session->isReady() },
{ QStringLiteral("url"), m_session->lastUrl().toString() },
};
if (!m_session->lastError().isEmpty())
out.insert(QStringLiteral("error"), m_session->lastError());
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
}
QString SceneGraphTools::listQuickWindows() const
{
if (const QString e = ensureSession(); !e.isEmpty())
return e;
auto *m = m_session->model(QString::fromUtf8(kQuickWindowModel));
if (!m)
return QString::fromUtf8(QJsonDocument(errorJson(
QStringLiteral("QuickWindowModel not available (no Quick app attached?)"))).toJson(QJsonDocument::Compact));
waitForData(m, 0, 3000);
QJsonArray out;
for (int r = 0; r < m->rowCount(); ++r) {
const QModelIndex a = m->index(r, 0);
const QModelIndex t = m->index(r, 1);
QJsonObject w;
w.insert(QStringLiteral("address"), m->data(a, Qt::DisplayRole).toString());
w.insert(QStringLiteral("type"), m->data(t, Qt::DisplayRole).toString());
out.append(w);
}
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
}
QString SceneGraphTools::selectQuickWindow(int index) const
{
if (const QString e = ensureSession(); !e.isEmpty())
return e;
// The QuickInspectorInterface lives on the probe side. Its header isn't in
// the installed client headers, but Endpoint::invokeObject() can forward a
// method call to a remote object by name+address. The name→address mapping
// is synced during the handshake (ready()), so once connected we can call
// it directly — no client-side stub needed (invokeObject only uses the
// address, not a local object). The object name is the interface IID
// (quickinspectorinterface.cpp:59 registers via the IID), and the method
// name is the bare slot name "selectWindow" (matches QuickInspectorClient).
auto *ep = GammaRay::Endpoint::instance();
if (!ep)
return QString::fromUtf8(QJsonDocument(errorJson(
QStringLiteral("no GammaRay endpoint"))).toJson(QJsonDocument::Compact));
ep->invokeObject(QStringLiteral("com.kdab.GammaRay.QuickInspectorInterface/1.0"),
"selectWindow", QVariantList{index});
return QString::fromUtf8(QJsonDocument(QJsonObject{
{ QStringLiteral("status"), QStringLiteral("selected") },
{ QStringLiteral("index"), index },
}).toJson(QJsonDocument::Compact));
}
QString SceneGraphTools::listScenegraphNodes() const
{
if (const QString e = ensureSession(); !e.isEmpty())
return e;
auto *m = m_session->model(QString::fromUtf8(kQuickSceneGraphModel));
if (!m)
return QString::fromUtf8(QJsonDocument(errorJson(
QStringLiteral("QuickSceneGraphModel not available (no Quick app attached?)"))).toJson(QJsonDocument::Compact));
// Prime + settle across a few rounds so lazily-fetched cell data and child
// row counts have time to arrive before we snapshot the tree.
QEventLoop loop;
for (int round = 0; round < 4; ++round) {
primeTree(m, QModelIndex(), 16);
settle(loop, 600);
}
// Depth cap to keep responses bounded for large scenes.
const QJsonArray tree = walkChildren(m, QModelIndex(), 16);
return QString::fromUtf8(QJsonDocument(tree).toJson(QJsonDocument::Compact));
}