init commit
This commit is contained in:
254
bridge/src/scenegraph_tools.cpp
Normal file
254
bridge/src/scenegraph_tools.cpp
Normal file
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
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));
|
||||
}
|
||||
Reference in New Issue
Block a user