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"
|
2026-07-07 16:48:44 +08:00
|
|
|
#include "material_interface.h"
|
|
|
|
|
#include "quickinspector_types.h"
|
2026-07-06 20:32:47 +08:00
|
|
|
|
2026-07-07 16:48:44 +08:00
|
|
|
#include <endpoint.h>
|
|
|
|
|
#include <message.h>
|
|
|
|
|
#include <protocol.h>
|
|
|
|
|
#include <objectbroker.h>
|
2026-07-06 20:32:47 +08:00
|
|
|
|
|
|
|
|
#include <QAbstractItemModel>
|
|
|
|
|
#include <QCoreApplication>
|
|
|
|
|
#include <QEventLoop>
|
2026-07-07 16:48:44 +08:00
|
|
|
#include <QItemSelectionModel>
|
2026-07-06 20:32:47 +08:00
|
|
|
#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";
|
2026-07-07 16:48:44 +08:00
|
|
|
static const char *kBaseName = "com.kdab.GammaRay.QuickSceneGraph";
|
|
|
|
|
static const char *kMaterialIface = "com.kdab.GammaRay.MaterialExtensionInterface";
|
|
|
|
|
static const char *kQuickInspectorIface = "com.kdab.GammaRay.QuickInspectorInterface/1.0";
|
|
|
|
|
|
|
|
|
|
// Custom roles from sggeometrymodel.h / materialshadermodel.h
|
|
|
|
|
static constexpr int kIsCoordinateRole = 257;
|
|
|
|
|
static constexpr int kDrawingModeRole = 257;
|
|
|
|
|
static constexpr int kRenderRole = 258;
|
|
|
|
|
|
|
|
|
|
// --- async helpers (from earlier scaffold) ---
|
2026-07-06 20:32:47 +08:00
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
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 } };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-07 16:48:44 +08:00
|
|
|
// Count how many cells in the tree are still "Loading..." (column 0).
|
|
|
|
|
// Used to decide whether more prime+settle rounds are needed.
|
|
|
|
|
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);
|
|
|
|
|
const QString v = m->data(idx, Qt::DisplayRole).toString();
|
|
|
|
|
if (v == QLatin1String("Loading..."))
|
|
|
|
|
++count;
|
|
|
|
|
if (m->hasChildren(idx))
|
|
|
|
|
count += countLoading(m, idx, depth - 1);
|
|
|
|
|
}
|
|
|
|
|
return count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Signal-driven tree loading: prime the tree, then settle until no
|
|
|
|
|
// dataChanged/rowsInserted signals arrive for quietMs. Repeat up to maxRounds
|
|
|
|
|
// or until no cells are "Loading...". After the main rounds, do one final
|
|
|
|
|
// long settle (finalSettleMs) to let any in-flight fetch responses arrive,
|
|
|
|
|
// then re-check. This handles the common case where the deepest cells' fetch
|
|
|
|
|
// responses arrive just after the quiet window expires.
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Final long settle: let any in-flight fetch responses arrive, then re-prime.
|
|
|
|
|
if (countLoading(m, QModelIndex(), 16) > 0) {
|
|
|
|
|
settle(loop, finalSettleMs);
|
|
|
|
|
primeTree(m, QModelIndex(), 16);
|
|
|
|
|
settle(loop, finalSettleMs / 2);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Recursively search the SG tree for a node whose column-0 display string
|
|
|
|
|
// matches the given address. Primes the tree as it goes so deeper levels
|
|
|
|
|
// become available. Returns the matching index (column 0) or invalid.
|
|
|
|
|
static QModelIndex findNodeInTree(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 this node is still "Loading...", settle and retry
|
|
|
|
|
if (addr == QLatin1String("Loading...")) {
|
|
|
|
|
settle(loop, 300);
|
|
|
|
|
const QString addr2 = m->data(idx, Qt::DisplayRole).toString();
|
|
|
|
|
if (addr2 == address)
|
|
|
|
|
return idx;
|
|
|
|
|
}
|
|
|
|
|
if (m->hasChildren(idx)) {
|
|
|
|
|
// Prime children and recurse
|
|
|
|
|
m->rowCount(idx); // trigger child count fetch
|
|
|
|
|
settle(loop, 200);
|
|
|
|
|
const auto found = findNodeInTree(m, idx, address, depth - 1, loop);
|
|
|
|
|
if (found.isValid())
|
|
|
|
|
return found;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- SceneGraphTools ---
|
|
|
|
|
|
2026-07-06 20:32:47 +08:00
|
|
|
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));
|
2026-07-07 16:48:44 +08:00
|
|
|
if (m_session->lastUrl().isValid())
|
2026-07-06 20:32:47 +08:00
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-07 16:48:44 +08:00
|
|
|
// --- Connection management ---
|
|
|
|
|
|
2026-07-06 20:32:47 +08:00
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-07 16:48:44 +08:00
|
|
|
// --- Window-level navigation ---
|
|
|
|
|
|
2026-07-06 20:32:47 +08:00
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
auto *ep = GammaRay::Endpoint::instance();
|
|
|
|
|
if (!ep)
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
|
|
|
QStringLiteral("no GammaRay endpoint"))).toJson(QJsonDocument::Compact));
|
2026-07-07 16:48:44 +08:00
|
|
|
ep->invokeObject(QString::fromUtf8(kQuickInspectorIface),
|
2026-07-06 20:32:47 +08:00
|
|
|
"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));
|
|
|
|
|
|
2026-07-07 16:48:44 +08:00
|
|
|
// Signal-driven tree loading: prime + settle until the model goes quiet
|
|
|
|
|
// and no cells are "Loading...". More rounds + longer settle for reliability.
|
|
|
|
|
primeAndWait(m, 6, 300, 500);
|
2026-07-06 20:32:47 +08:00
|
|
|
|
|
|
|
|
const QJsonArray tree = walkChildren(m, QModelIndex(), 16);
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(tree).toJson(QJsonDocument::Compact));
|
|
|
|
|
}
|
2026-07-07 16:48:44 +08:00
|
|
|
|
|
|
|
|
// --- SG node selection ---
|
|
|
|
|
// TODO: all code below is disabled until the selection-sync / sub-model-population
|
|
|
|
|
// bug is fixed. See PLAN.md "Known issues / Blocked" section.
|
|
|
|
|
#if 0
|
|
|
|
|
|
|
|
|
|
QModelIndex SceneGraphTools::findNodeByAddress(QAbstractItemModel *m, const QString &address) const
|
|
|
|
|
{
|
|
|
|
|
if (!m || address.isEmpty())
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
// Signal-driven tree loading to maximize the chance of finding the node.
|
|
|
|
|
primeAndWait(m, 6, 300, 500);
|
|
|
|
|
|
|
|
|
|
QEventLoop loop;
|
|
|
|
|
// Now search for the address.
|
|
|
|
|
const auto idx = findNodeInTree(m, QModelIndex(), address, 16, loop);
|
|
|
|
|
if (idx.isValid())
|
|
|
|
|
return idx;
|
|
|
|
|
|
|
|
|
|
// Maybe the tree was still loading — try a couple more rounds.
|
|
|
|
|
for (int round = 0; round < 3; ++round) {
|
|
|
|
|
primeAndWait(m, 2, 300, 500);
|
|
|
|
|
const auto idx2 = findNodeInTree(m, QModelIndex(), address, 16, loop);
|
|
|
|
|
if (idx2.isValid())
|
|
|
|
|
return idx2;
|
|
|
|
|
}
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SceneGraphTools::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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString SceneGraphTools::ensureNodeSelected(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 already selected, nothing to do.
|
|
|
|
|
if (address == m_selectedAddress)
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
auto *sgModel = m_session->model(QString::fromUtf8(kQuickSceneGraphModel));
|
|
|
|
|
if (!sgModel)
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
|
|
|
QStringLiteral("QuickSceneGraphModel not available"))).toJson(QJsonDocument::Compact));
|
|
|
|
|
|
|
|
|
|
const QModelIndex idx = findNodeByAddress(sgModel, address);
|
|
|
|
|
if (!idx.isValid())
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
|
|
|
QStringLiteral("node %1 not found in scene graph tree (still loading?)").arg(address)))
|
|
|
|
|
.toJson(QJsonDocument::Compact));
|
|
|
|
|
|
|
|
|
|
auto *selModel = m_session->selectionModel(sgModel);
|
|
|
|
|
if (!selModel)
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
|
|
|
QStringLiteral("selection model not available"))).toJson(QJsonDocument::Compact));
|
|
|
|
|
|
|
|
|
|
// CRITICAL: Create sub-model RemoteModels BEFORE sending the selection.
|
|
|
|
|
// The probe populates and resets sub-models (vertex, adjacency, shader,
|
|
|
|
|
// materialProperty) when it receives the selection. The RemoteModelServer
|
|
|
|
|
// only forwards modelReset/rowsInserted to clients that are monitoring.
|
|
|
|
|
// By creating the RemoteModels first, their ObjectMonitored messages reach
|
|
|
|
|
// the server before the SelectionModelSelect (FIFO on same TCP socket),
|
|
|
|
|
// so the server is already monitoring when the reset fires.
|
|
|
|
|
const QString base = QString::fromUtf8(kBaseName);
|
|
|
|
|
auto *vModel = m_session->model(base + ".sgGeometryVertexModel");
|
|
|
|
|
auto *aModel = m_session->model(base + ".sgGeometryAdjacencyModel");
|
|
|
|
|
auto *sModel = m_session->model(base + ".shaderModel");
|
|
|
|
|
auto *mModel = m_session->model(base + ".materialPropertyModel");
|
|
|
|
|
|
|
|
|
|
// Turn off slow mode before selecting to prevent SG tree updates from
|
|
|
|
|
// interfering with the selection (continuous rendering causes model resets
|
|
|
|
|
// that can clear the selection).
|
|
|
|
|
GammaRay::Endpoint::instance()->invokeObject(
|
|
|
|
|
QString::fromUtf8(kQuickInspectorIface),
|
|
|
|
|
"setSlowMode", QVariantList{ false });
|
|
|
|
|
|
|
|
|
|
// Wait long enough for:
|
|
|
|
|
// 1. ObjectMonitored messages to reach the server and start monitoring
|
|
|
|
|
// 2. SelectionModelStateRequest (125ms timer from SelectionModelClient ctor)
|
|
|
|
|
// to be sent and the server's response (default selection) to arrive
|
|
|
|
|
// 3. Any SG model updates from setSlowMode(false) to settle
|
|
|
|
|
QEventLoop loop;
|
|
|
|
|
settle(loop, 2000);
|
|
|
|
|
|
|
|
|
|
// Now send the selection. The server should now be in a stable state with
|
|
|
|
|
// the default selection already processed. Our selection will override it.
|
|
|
|
|
selModel->select(idx, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows | QItemSelectionModel::Current);
|
|
|
|
|
m_selectedAddress = address;
|
|
|
|
|
|
|
|
|
|
// Also manually construct and send a SelectionModelSelect message directly
|
|
|
|
|
// to the server's selection model address, bypassing SelectionModelClient.
|
|
|
|
|
// This eliminates any issues with SelectionModelClient state.
|
|
|
|
|
{
|
|
|
|
|
const auto selAddr = GammaRay::Endpoint::instance()->objectAddress(
|
|
|
|
|
QStringLiteral("%1.selection").arg(QString::fromUtf8(kQuickSceneGraphModel)));
|
|
|
|
|
if (selAddr != GammaRay::Protocol::InvalidObjectAddress) {
|
|
|
|
|
// Check the path length
|
|
|
|
|
const auto mi = GammaRay::Protocol::fromQModelIndex(idx);
|
|
|
|
|
int pathLen = mi.size();
|
|
|
|
|
// Log the path for debugging
|
|
|
|
|
QString pathStr;
|
|
|
|
|
for (int i = 0; i < mi.size(); ++i)
|
|
|
|
|
pathStr += QStringLiteral("(%1,%2) ").arg(mi[i].row).arg(mi[i].column);
|
|
|
|
|
|
|
|
|
|
GammaRay::Message msg(selAddr, GammaRay::Protocol::SelectionModelSelect);
|
|
|
|
|
msg << qint32(1); // 1 range
|
|
|
|
|
msg << mi << mi; // topLeft, bottomRight
|
|
|
|
|
msg << (QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows | QItemSelectionModel::Current);
|
|
|
|
|
GammaRay::Endpoint::send(msg);
|
|
|
|
|
|
|
|
|
|
// Store path info for diagnostics
|
|
|
|
|
m_lastSelectPath = QStringLiteral("len=%1 %2").arg(pathLen).arg(pathStr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wait for the selection to be processed and sub-model data to arrive.
|
|
|
|
|
settle(loop, 2000);
|
|
|
|
|
|
|
|
|
|
// Re-send via direct message to ensure it sticks.
|
|
|
|
|
{
|
|
|
|
|
const auto selAddr = GammaRay::Endpoint::instance()->objectAddress(
|
|
|
|
|
QStringLiteral("%1.selection").arg(QString::fromUtf8(kQuickSceneGraphModel)));
|
|
|
|
|
if (selAddr != GammaRay::Protocol::InvalidObjectAddress) {
|
|
|
|
|
GammaRay::Message msg(selAddr, GammaRay::Protocol::SelectionModelSelect);
|
|
|
|
|
const auto mi = GammaRay::Protocol::fromQModelIndex(idx);
|
|
|
|
|
msg << qint32(1);
|
|
|
|
|
msg << mi << mi;
|
|
|
|
|
msg << (QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows | QItemSelectionModel::Current);
|
|
|
|
|
GammaRay::Endpoint::send(msg);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
settle(loop, 2000);
|
|
|
|
|
|
|
|
|
|
// Prime sub-models to fetch their row/column counts and cell data.
|
|
|
|
|
if (vModel) { vModel->rowCount(); primeAndWait(vModel, 4, 300, 500, 1500); }
|
|
|
|
|
if (aModel) { aModel->rowCount(); primeAndWait(aModel, 4, 300, 500, 1500); }
|
|
|
|
|
if (sModel) { sModel->rowCount(); primeAndWait(sModel, 4, 300, 500, 1500); }
|
|
|
|
|
if (mModel) { mModel->rowCount(); primeAndWait(mModel, 4, 300, 500, 1500); }
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString SceneGraphTools::selectScenegraphNode(const QString &address) const
|
|
|
|
|
{
|
|
|
|
|
m_selectedAddress.clear(); // force reselect
|
|
|
|
|
const QString e = ensureNodeSelected(address);
|
|
|
|
|
if (!e.isEmpty())
|
|
|
|
|
return e;
|
|
|
|
|
|
|
|
|
|
// Report what we know about the selected node.
|
|
|
|
|
auto *sgModel = m_session->model(QString::fromUtf8(kQuickSceneGraphModel));
|
|
|
|
|
const QModelIndex idx = findNodeByAddress(sgModel, address);
|
|
|
|
|
QString type;
|
|
|
|
|
if (idx.isValid()) {
|
|
|
|
|
const QModelIndex typeIdx = sgModel->index(idx.row(), 1, idx.parent());
|
|
|
|
|
type = sgModel->data(typeIdx, Qt::DisplayRole).toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check selection state and sub-model row counts
|
|
|
|
|
auto *sgModel2 = m_session->model(QString::fromUtf8(kQuickSceneGraphModel));
|
|
|
|
|
auto *selModel = m_session->selectionModel(sgModel2);
|
|
|
|
|
auto *vModel = m_session->model(QStringLiteral("%1.sgGeometryVertexModel").arg(QString::fromUtf8(kBaseName)));
|
|
|
|
|
auto *aModel = m_session->model(QStringLiteral("%1.sgGeometryAdjacencyModel").arg(QString::fromUtf8(kBaseName)));
|
|
|
|
|
auto *sModel = m_session->model(QStringLiteral("%1.shaderModel").arg(QString::fromUtf8(kBaseName)));
|
|
|
|
|
auto *mModel = m_session->model(QStringLiteral("%1.materialPropertyModel").arg(QString::fromUtf8(kBaseName)));
|
|
|
|
|
|
|
|
|
|
// Diagnostic: check if models are registered on the probe (have valid addresses)
|
|
|
|
|
auto *ep = GammaRay::Endpoint::instance();
|
|
|
|
|
const auto vAddr = ep ? ep->objectAddress(QStringLiteral("%1.sgGeometryVertexModel").arg(QString::fromUtf8(kBaseName))) : 0;
|
|
|
|
|
const auto aAddr = ep ? ep->objectAddress(QStringLiteral("%1.sgGeometryAdjacencyModel").arg(QString::fromUtf8(kBaseName))) : 0;
|
|
|
|
|
const auto sAddr = ep ? ep->objectAddress(QStringLiteral("%1.shaderModel").arg(QString::fromUtf8(kBaseName))) : 0;
|
|
|
|
|
const auto mAddr = ep ? ep->objectAddress(QStringLiteral("%1.materialPropertyModel").arg(QString::fromUtf8(kBaseName))) : 0;
|
|
|
|
|
const auto selAddr = ep ? ep->objectAddress(QStringLiteral("%1.selection").arg(QString::fromUtf8(kQuickSceneGraphModel))) : 0;
|
|
|
|
|
// Also check the wrong name (in case source model name mismatch)
|
|
|
|
|
const auto dotSelAddr = ep ? ep->objectAddress(QStringLiteral(".selection")) : 0;
|
|
|
|
|
|
|
|
|
|
// Check what the selected index looks like
|
|
|
|
|
QString selIdxInfo;
|
|
|
|
|
if (selModel && selModel->hasSelection()) {
|
|
|
|
|
auto rows = selModel->selectedRows();
|
|
|
|
|
if (rows.isEmpty()) {
|
|
|
|
|
selIdxInfo = QStringLiteral("hasSelection=true but selectedRows() empty");
|
|
|
|
|
} else {
|
|
|
|
|
// Walk the parent chain to get the full path
|
|
|
|
|
QStringList pathParts;
|
|
|
|
|
QModelIndex cur = rows.first();
|
|
|
|
|
while (cur.isValid()) {
|
|
|
|
|
QString addr = cur.data(Qt::DisplayRole).toString();
|
|
|
|
|
pathParts.prepend(QStringLiteral("r%1:%2").arg(cur.row()).arg(addr));
|
|
|
|
|
cur = cur.parent();
|
|
|
|
|
}
|
|
|
|
|
selIdxInfo = pathParts.join(" -> ");
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
selIdxInfo = QStringLiteral("no selection");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QJsonObject out = {
|
|
|
|
|
{ QStringLiteral("address"), address },
|
|
|
|
|
{ QStringLiteral("type"), type },
|
|
|
|
|
{ QStringLiteral("hasGeometry"), vModel && vModel->rowCount() > 0 },
|
|
|
|
|
{ QStringLiteral("hasMaterial"), sModel && sModel->rowCount() > 0 },
|
|
|
|
|
{ QStringLiteral("_diag_epConnected"), ep ? ep->isConnected() : false },
|
|
|
|
|
{ QStringLiteral("_diag_selModelAddr"), qint64(selAddr) },
|
|
|
|
|
{ QStringLiteral("_diag_selHasSelection"), selModel ? selModel->hasSelection() : false },
|
|
|
|
|
{ QStringLiteral("_diag_selIdxInfo"), selIdxInfo },
|
|
|
|
|
{ QStringLiteral("_diag_vertexModelAddr"), qint64(vAddr) },
|
|
|
|
|
{ QStringLiteral("_diag_adjacencyModelAddr"), qint64(aAddr) },
|
|
|
|
|
{ QStringLiteral("_diag_shaderModelAddr"), qint64(sAddr) },
|
|
|
|
|
{ QStringLiteral("_diag_materialPropModelAddr"), qint64(mAddr) },
|
|
|
|
|
{ QStringLiteral("_diag_vertexRowCount"), vModel ? vModel->rowCount() : -1 },
|
|
|
|
|
{ QStringLiteral("_diag_adjacencyRowCount"), aModel ? aModel->rowCount() : -1 },
|
|
|
|
|
{ QStringLiteral("_diag_shaderRowCount"), sModel ? sModel->rowCount() : -1 },
|
|
|
|
|
{ QStringLiteral("_diag_materialPropRowCount"), mModel ? mModel->rowCount() : -1 },
|
|
|
|
|
{ QStringLiteral("_diag_vertexColCount"), vModel ? vModel->columnCount() : -1 },
|
|
|
|
|
{ QStringLiteral("_diag_vertexData00"), vModel && vModel->rowCount() > 0 && vModel->columnCount() > 0 ? vModel->data(vModel->index(0, 0)).toString() : QString() },
|
|
|
|
|
};
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Geometry ---
|
|
|
|
|
|
|
|
|
|
QString SceneGraphTools::getNodeVertices(const QString &address) const
|
|
|
|
|
{
|
|
|
|
|
const QString e = ensureNodeSelected(address);
|
|
|
|
|
if (!e.isEmpty())
|
|
|
|
|
return e;
|
|
|
|
|
|
|
|
|
|
auto *m = m_session->model(QStringLiteral("%1.sgGeometryVertexModel").arg(QString::fromUtf8(kBaseName)));
|
|
|
|
|
if (!m)
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
|
|
|
QStringLiteral("sgGeometryVertexModel not available"))).toJson(QJsonDocument::Compact));
|
|
|
|
|
|
|
|
|
|
waitForRows(m, 3000);
|
|
|
|
|
|
|
|
|
|
const int rows = m->rowCount();
|
|
|
|
|
const int cols = m->columnCount();
|
|
|
|
|
if (rows == 0)
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
|
|
|
QStringLiteral("no vertex data (selected node may not be a GeometryNode)")))
|
|
|
|
|
.toJson(QJsonDocument::Compact));
|
|
|
|
|
|
|
|
|
|
QJsonArray headers;
|
|
|
|
|
QJsonArray isCoordinate;
|
|
|
|
|
for (int c = 0; c < cols; ++c) {
|
|
|
|
|
headers.append(m->headerData(c, Qt::Horizontal, Qt::DisplayRole).toString());
|
|
|
|
|
isCoordinate.append(m->data(m->index(0, c), kIsCoordinateRole).toBool());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QJsonArray vertices;
|
|
|
|
|
for (int r = 0; r < rows; ++r) {
|
|
|
|
|
QJsonArray row;
|
|
|
|
|
for (int c = 0; c < cols; ++c)
|
|
|
|
|
row.append(m->data(m->index(r, c), Qt::DisplayRole).toString());
|
|
|
|
|
vertices.append(row);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QJsonObject out = {
|
|
|
|
|
{ QStringLiteral("vertexCount"), rows },
|
|
|
|
|
{ QStringLiteral("attributes"), headers },
|
|
|
|
|
{ QStringLiteral("isCoordinate"), isCoordinate },
|
|
|
|
|
{ QStringLiteral("vertices"), vertices },
|
|
|
|
|
};
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString SceneGraphTools::getNodeAdjacency(const QString &address) const
|
|
|
|
|
{
|
|
|
|
|
const QString e = ensureNodeSelected(address);
|
|
|
|
|
if (!e.isEmpty())
|
|
|
|
|
return e;
|
|
|
|
|
|
|
|
|
|
auto *m = m_session->model(QStringLiteral("%1.sgGeometryAdjacencyModel").arg(QString::fromUtf8(kBaseName)));
|
|
|
|
|
if (!m)
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
|
|
|
QStringLiteral("sgGeometryAdjacencyModel not available"))).toJson(QJsonDocument::Compact));
|
|
|
|
|
|
|
|
|
|
waitForRows(m, 3000);
|
|
|
|
|
|
|
|
|
|
const int rows = m->rowCount();
|
|
|
|
|
if (rows == 0)
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
|
|
|
QStringLiteral("no adjacency data (selected node may not be a GeometryNode)")))
|
|
|
|
|
.toJson(QJsonDocument::Compact));
|
|
|
|
|
|
|
|
|
|
// DrawingModeRole is the same for all rows — read from row 0.
|
|
|
|
|
int drawingMode = m->data(m->index(0, 0), kDrawingModeRole).toInt();
|
|
|
|
|
QJsonArray indices;
|
|
|
|
|
for (int r = 0; r < rows; ++r)
|
|
|
|
|
indices.append(m->data(m->index(r, 0), kRenderRole).toInt());
|
|
|
|
|
|
|
|
|
|
QJsonObject out = {
|
|
|
|
|
{ QStringLiteral("drawingMode"), drawingMode },
|
|
|
|
|
{ QStringLiteral("indexCount"), rows },
|
|
|
|
|
{ QStringLiteral("indices"), indices },
|
|
|
|
|
};
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Material/Shader ---
|
|
|
|
|
|
|
|
|
|
QString SceneGraphTools::getMaterialShaders(const QString &address) const
|
|
|
|
|
{
|
|
|
|
|
const QString e = ensureNodeSelected(address);
|
|
|
|
|
if (!e.isEmpty())
|
|
|
|
|
return e;
|
|
|
|
|
|
|
|
|
|
auto *m = m_session->model(QStringLiteral("%1.shaderModel").arg(QString::fromUtf8(kBaseName)));
|
|
|
|
|
if (!m)
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
|
|
|
QStringLiteral("shaderModel not available"))).toJson(QJsonDocument::Compact));
|
|
|
|
|
|
|
|
|
|
waitForRows(m, 3000);
|
|
|
|
|
|
|
|
|
|
const int rows = m->rowCount();
|
|
|
|
|
if (rows == 0)
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
|
|
|
QStringLiteral("no shaders (selected node may not have a material)")))
|
|
|
|
|
.toJson(QJsonDocument::Compact));
|
|
|
|
|
|
|
|
|
|
QJsonArray out;
|
|
|
|
|
for (int r = 0; r < rows; ++r) {
|
|
|
|
|
const QString stage = m->data(m->index(r, 0), Qt::DisplayRole).toString();
|
|
|
|
|
out.append(QJsonObject{
|
|
|
|
|
{ QStringLiteral("row"), r },
|
|
|
|
|
{ QStringLiteral("stage"), stage },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString SceneGraphTools::getShaderSource(int row) const
|
|
|
|
|
{
|
|
|
|
|
if (const QString e = ensureSession(); !e.isEmpty())
|
|
|
|
|
return e;
|
|
|
|
|
|
|
|
|
|
// Get or create the MaterialExtensionInterface proxy. The factory (registered
|
|
|
|
|
// in initOnce) creates a MaterialExtensionProxy that self-registers via
|
|
|
|
|
// ObjectBroker::registerObject. The probe forwards gotShader emissions to it.
|
|
|
|
|
auto *proxy = qobject_cast<GammaRay::MaterialExtensionInterface *>(
|
|
|
|
|
GammaRay::ObjectBroker::objectInternal(
|
|
|
|
|
QStringLiteral("%1.material").arg(QString::fromUtf8(kBaseName)),
|
|
|
|
|
QByteArray(kMaterialIface)));
|
|
|
|
|
if (!proxy)
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
|
|
|
QStringLiteral("MaterialExtensionInterface proxy not available"))).toJson(QJsonDocument::Compact));
|
|
|
|
|
|
|
|
|
|
QString result;
|
|
|
|
|
QEventLoop loop;
|
|
|
|
|
const auto conn = QObject::connect(proxy, &GammaRay::MaterialExtensionInterface::gotShader,
|
|
|
|
|
&loop, [&](const QString &src) {
|
|
|
|
|
result = src;
|
|
|
|
|
loop.quit();
|
|
|
|
|
});
|
|
|
|
|
QTimer::singleShot(10000, &loop, &QEventLoop::quit);
|
|
|
|
|
proxy->getShader(row); // forwards via invokeObject to the probe
|
|
|
|
|
loop.exec();
|
|
|
|
|
QObject::disconnect(conn);
|
|
|
|
|
|
|
|
|
|
if (result.isEmpty())
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
|
|
|
QStringLiteral("timeout waiting for shader source (row %1)").arg(row)))
|
|
|
|
|
.toJson(QJsonDocument::Compact));
|
|
|
|
|
|
|
|
|
|
QJsonObject out = {
|
|
|
|
|
{ QStringLiteral("row"), row },
|
|
|
|
|
{ QStringLiteral("source"), result },
|
|
|
|
|
};
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString SceneGraphTools::getMaterialProperties(const QString &address) const
|
|
|
|
|
{
|
|
|
|
|
const QString e = ensureNodeSelected(address);
|
|
|
|
|
if (!e.isEmpty())
|
|
|
|
|
return e;
|
|
|
|
|
|
|
|
|
|
auto *m = m_session->model(QStringLiteral("%1.materialPropertyModel").arg(QString::fromUtf8(kBaseName)));
|
|
|
|
|
if (!m)
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
|
|
|
QStringLiteral("materialPropertyModel not available"))).toJson(QJsonDocument::Compact));
|
|
|
|
|
|
|
|
|
|
waitForRows(m, 3000);
|
|
|
|
|
|
|
|
|
|
const int rows = m->rowCount();
|
|
|
|
|
const int cols = m->columnCount();
|
|
|
|
|
if (rows == 0)
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
|
|
|
QStringLiteral("no material properties (selected node may not have a material)")))
|
|
|
|
|
.toJson(QJsonDocument::Compact));
|
|
|
|
|
|
|
|
|
|
QJsonArray out;
|
|
|
|
|
for (int r = 0; r < rows; ++r) {
|
|
|
|
|
QJsonObject prop;
|
|
|
|
|
for (int c = 0; c < cols; ++c) {
|
|
|
|
|
const QString header = m->headerData(c, Qt::Horizontal, Qt::DisplayRole).toString();
|
|
|
|
|
const QString val = m->data(m->index(r, c), Qt::DisplayRole).toString();
|
|
|
|
|
prop.insert(header.isEmpty() ? QString::number(c) : header, val);
|
|
|
|
|
}
|
|
|
|
|
out.append(prop);
|
|
|
|
|
}
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endif // disabled until selection-sync bug is fixed
|
|
|
|
|
|
|
|
|
|
// --- Rendering visualization ---
|
|
|
|
|
|
|
|
|
|
QString SceneGraphTools::setRenderMode(const QString &mode) const
|
|
|
|
|
{
|
|
|
|
|
if (const QString e = ensureSession(); !e.isEmpty())
|
|
|
|
|
return e;
|
|
|
|
|
|
|
|
|
|
auto *ep = GammaRay::Endpoint::instance();
|
|
|
|
|
if (!ep)
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
|
|
|
QStringLiteral("no GammaRay endpoint"))).toJson(QJsonDocument::Compact));
|
|
|
|
|
|
|
|
|
|
// Parse mode string → enum. Accept full enum name or short alias.
|
|
|
|
|
GammaRay::QuickInspectorInterface::RenderMode rm = GammaRay::QuickInspectorInterface::NormalRendering;
|
|
|
|
|
static const QHash<QString, GammaRay::QuickInspectorInterface::RenderMode> map = {
|
|
|
|
|
{ QStringLiteral("NormalRendering"), GammaRay::QuickInspectorInterface::NormalRendering },
|
|
|
|
|
{ QStringLiteral("normal"), GammaRay::QuickInspectorInterface::NormalRendering },
|
|
|
|
|
{ QStringLiteral("VisualizeClipping"), GammaRay::QuickInspectorInterface::VisualizeClipping },
|
|
|
|
|
{ QStringLiteral("clipping"), GammaRay::QuickInspectorInterface::VisualizeClipping },
|
|
|
|
|
{ QStringLiteral("VisualizeOverdraw"), GammaRay::QuickInspectorInterface::VisualizeOverdraw },
|
|
|
|
|
{ QStringLiteral("overdraw"), GammaRay::QuickInspectorInterface::VisualizeOverdraw },
|
|
|
|
|
{ QStringLiteral("VisualizeBatches"), GammaRay::QuickInspectorInterface::VisualizeBatches },
|
|
|
|
|
{ QStringLiteral("batches"), GammaRay::QuickInspectorInterface::VisualizeBatches },
|
|
|
|
|
{ QStringLiteral("VisualizeChanges"), GammaRay::QuickInspectorInterface::VisualizeChanges },
|
|
|
|
|
{ QStringLiteral("changes"), GammaRay::QuickInspectorInterface::VisualizeChanges },
|
|
|
|
|
{ QStringLiteral("VisualizeTraces"), GammaRay::QuickInspectorInterface::VisualizeTraces },
|
|
|
|
|
{ QStringLiteral("traces"), GammaRay::QuickInspectorInterface::VisualizeTraces },
|
|
|
|
|
};
|
|
|
|
|
if (map.contains(mode))
|
|
|
|
|
rm = map.value(mode);
|
|
|
|
|
else
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
|
|
|
QStringLiteral("unknown render mode '%1' — valid: NormalRendering, VisualizeClipping, VisualizeOverdraw, VisualizeBatches, VisualizeChanges, VisualizeTraces").arg(mode)))
|
|
|
|
|
.toJson(QJsonDocument::Compact));
|
|
|
|
|
|
|
|
|
|
ep->invokeObject(QString::fromUtf8(kQuickInspectorIface),
|
|
|
|
|
"setCustomRenderMode",
|
|
|
|
|
QVariantList{QVariant::fromValue(rm)});
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(QJsonObject{
|
|
|
|
|
{ QStringLiteral("renderMode"), mode },
|
|
|
|
|
}).toJson(QJsonDocument::Compact));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString SceneGraphTools::setSlowMode(bool enabled) const
|
|
|
|
|
{
|
|
|
|
|
if (const QString e = ensureSession(); !e.isEmpty())
|
|
|
|
|
return e;
|
|
|
|
|
|
|
|
|
|
auto *ep = GammaRay::Endpoint::instance();
|
|
|
|
|
if (!ep)
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
|
|
|
QStringLiteral("no GammaRay endpoint"))).toJson(QJsonDocument::Compact));
|
|
|
|
|
|
|
|
|
|
ep->invokeObject(QString::fromUtf8(kQuickInspectorIface),
|
|
|
|
|
"setSlowMode", QVariantList{enabled});
|
|
|
|
|
return QString::fromUtf8(QJsonDocument(QJsonObject{
|
|
|
|
|
{ QStringLiteral("slowMode"), enabled },
|
|
|
|
|
}).toJson(QJsonDocument::Compact));
|
|
|
|
|
}
|