more progress, but some of them are wip
This commit is contained in:
@@ -42,6 +42,9 @@ add_executable(qml-sg-mcp-bridge
|
||||
src/gammaray_session.h
|
||||
src/scenegraph_tools.cpp
|
||||
src/scenegraph_tools.h
|
||||
src/material_interface.cpp
|
||||
src/material_interface.h
|
||||
src/quickinspector_types.h
|
||||
)
|
||||
|
||||
target_link_libraries(qml-sg-mcp-bridge PRIVATE
|
||||
@@ -55,6 +58,11 @@ target_link_libraries(qml-sg-mcp-bridge PRIVATE
|
||||
Qt6::McpCommon
|
||||
)
|
||||
|
||||
# Add GammaRay source common/ for message.h (not in installed headers)
|
||||
target_include_directories(qml-sg-mcp-bridge PRIVATE
|
||||
/home/blumia/Sources/GammaRay/common
|
||||
)
|
||||
|
||||
# Runtime: qtmcp builds SHARED libs + the stdio backend plugin into the build
|
||||
# tree. Set RPATH so the bridge finds libQt6Mcp*.so, and set QT_PLUGIN_PATH at
|
||||
# run time (see run.sh) so Qt loads mcpserverbackend/libqmcpserverstdio.so.
|
||||
|
||||
@@ -12,8 +12,13 @@
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QEventLoop>
|
||||
#include <QItemSelectionModel>
|
||||
#include <QTimer>
|
||||
|
||||
// Defined in material_interface.cpp — registers the MaterialExtensionInterface
|
||||
// client factory so ObjectBroker can create our proxy for async getShader.
|
||||
extern void registerMaterialExtensionFactory();
|
||||
|
||||
GammaRaySession::GammaRaySession(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
@@ -61,6 +66,11 @@ GammaRaySession::~GammaRaySession() = default;
|
||||
void GammaRaySession::initOnce()
|
||||
{
|
||||
GammaRay::ClientConnectionManager::init();
|
||||
// Register our MaterialExtensionInterface proxy factory. The real factory
|
||||
// is in QuickInspectorUiFactory::initUi() (a GUI plugin we don't load).
|
||||
// Without this, ObjectBroker::object<MaterialExtensionInterface *>() would
|
||||
// Q_ASSERT (no factory registered for the IID).
|
||||
registerMaterialExtensionFactory();
|
||||
}
|
||||
|
||||
void GammaRaySession::connectToHost(const QUrl &url)
|
||||
@@ -123,6 +133,13 @@ QAbstractItemModel *GammaRaySession::model(const QString &name) const
|
||||
return GammaRay::ObjectBroker::model(name);
|
||||
}
|
||||
|
||||
QItemSelectionModel *GammaRaySession::selectionModel(QAbstractItemModel *model) const
|
||||
{
|
||||
if (m_state != State::Ready || !model)
|
||||
return nullptr;
|
||||
return GammaRay::ObjectBroker::selectionModel(model);
|
||||
}
|
||||
|
||||
QString GammaRaySession::stateString(State s)
|
||||
{
|
||||
switch (s) {
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QAbstractItemModel;
|
||||
class QItemSelectionModel;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
namespace GammaRay {
|
||||
@@ -89,6 +90,13 @@ public:
|
||||
// disconnect/reconnect.
|
||||
QAbstractItemModel *model(const QString &name) const;
|
||||
|
||||
// Retrieve a synced selection model for the given model. Returns nullptr if
|
||||
// not ready or no selection factory is registered. The selection model
|
||||
// (a SelectionModelClient on the wire) automatically syncs selection
|
||||
// changes to the probe, which triggers PropertyController::setObject() on
|
||||
// the probe side and populates per-node sub-models (geometry, material, etc.).
|
||||
QItemSelectionModel *selectionModel(QAbstractItemModel *model) const;
|
||||
|
||||
signals:
|
||||
void ready();
|
||||
void disconnected();
|
||||
|
||||
@@ -76,22 +76,39 @@ int main(int argc, char **argv)
|
||||
|
||||
SceneGraphTools tools(&session, &server);
|
||||
server.registerToolSet(&tools, {
|
||||
// Connection management. The bridge no longer hard-requires --connect:
|
||||
// a client calls connectProbe() to (re)establish a probe connection,
|
||||
// and any introspection tool will also auto-reconnect if a URL was
|
||||
// previously established. connectProbeDefault() is the zero-arg entry
|
||||
// point for the common 127.0.0.1:11732 case.
|
||||
// Connection management
|
||||
{ QStringLiteral("connectProbe"), QStringLiteral("Connect to a GammaRay probe. host defaults to 127.0.0.1; pass port=0 to use the default 11732.") },
|
||||
{ QStringLiteral("connectProbe/host"), QStringLiteral("Probe host (default 127.0.0.1)") },
|
||||
{ QStringLiteral("connectProbe/port"), QStringLiteral("Probe port (default 11732; pass 0 to use default)") },
|
||||
{ QStringLiteral("connectProbeDefault"), QStringLiteral("Connect to a GammaRay probe at 127.0.0.1:11732 (convenience for connectProbe with no args)") },
|
||||
{ QStringLiteral("disconnectProbe"), QStringLiteral("Drop the current probe connection and forget the URL") },
|
||||
{ QStringLiteral("probeStatus"), QStringLiteral("Report current probe connection state, last URL and last error") },
|
||||
// Introspection
|
||||
// Window-level navigation
|
||||
{ QStringLiteral("listQuickWindows"), QStringLiteral("List QQuickWindows in the target app") },
|
||||
{ QStringLiteral("selectQuickWindow"), QStringLiteral("Select a Quick window (by index into listQuickWindows) so the scene graph is introspected for it") },
|
||||
{ QStringLiteral("selectQuickWindow/index"), QStringLiteral("0-based index of the window in listQuickWindows") },
|
||||
{ QStringLiteral("listScenegraphNodes"), QStringLiteral("List the QSGNode tree of the selected Quick window") },
|
||||
// SG node selection
|
||||
// TODO: re-enable when the selection-sync / sub-model-population bug is fixed
|
||||
// { QStringLiteral("selectScenegraphNode"), QStringLiteral("Select a SG node by address (from listScenegraphNodes) so geometry/material sub-models populate for it") },
|
||||
// { QStringLiteral("selectScenegraphNode/address"), QStringLiteral("Node address (e.g. 0x56396e6d7400, from listScenegraphNodes)") },
|
||||
// Geometry
|
||||
// { QStringLiteral("getNodeVertices"), QStringLiteral("Get vertex data for a GeometryNode (vertices, attributes, isCoordinate flags)") },
|
||||
// { QStringLiteral("getNodeVertices/address"), QStringLiteral("Node address (must be a GeometryNode)") },
|
||||
// { QStringLiteral("getNodeAdjacency"), QStringLiteral("Get adjacency/index data for a GeometryNode (drawing mode + index list)") },
|
||||
// { QStringLiteral("getNodeAdjacency/address"), QStringLiteral("Node address (must be a GeometryNode)") },
|
||||
// Material/Shader
|
||||
// { QStringLiteral("getMaterialShaders"), QStringLiteral("List shader stages (Vertex, Fragment, ...) for a node's material") },
|
||||
// { QStringLiteral("getMaterialShaders/address"), QStringLiteral("Node address (must be a GeometryNode with a material)") },
|
||||
// { QStringLiteral("getShaderSource"), QStringLiteral("Get shader source code for a shader stage (async: waits for probe response)") },
|
||||
// { QStringLiteral("getShaderSource/row"), QStringLiteral("Row index from getMaterialShaders (0 = first shader stage)") },
|
||||
// { QStringLiteral("getMaterialProperties"), QStringLiteral("Get material properties (name/value pairs) for a node's material") },
|
||||
// { QStringLiteral("getMaterialProperties/address"), QStringLiteral("Node address (must be a GeometryNode with a material)") },
|
||||
// Rendering visualization
|
||||
{ QStringLiteral("setRenderMode"), QStringLiteral("Set custom render mode for visualization") },
|
||||
{ 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") },
|
||||
});
|
||||
|
||||
QObject::connect(&server, &QMcpServer::finished, &app, &QCoreApplication::quit);
|
||||
|
||||
54
bridge/src/material_interface.cpp
Normal file
54
bridge/src/material_interface.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
material_interface.cpp — client-side MaterialExtension proxy.
|
||||
|
||||
SPDX-FileCopyrightText: 2026 The GammaRay MCP Bridge Authors
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "material_interface.h"
|
||||
|
||||
#include <common/endpoint.h>
|
||||
#include <common/objectbroker.h>
|
||||
|
||||
using namespace GammaRay;
|
||||
|
||||
// Concrete client proxy: getShader() forwards to the probe via invokeObject.
|
||||
// The probe-side MaterialExtension::getShader(row) emits gotShader(source),
|
||||
// which the protocol delivers back to this proxy's gotShader signal.
|
||||
class MaterialExtensionProxy : public GammaRay::MaterialExtensionInterface
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MaterialExtensionProxy(const QString &name, QObject *parent = nullptr)
|
||||
: MaterialExtensionInterface(name, parent)
|
||||
{
|
||||
// Self-register in the ObjectBroker so the protocol can find us by name
|
||||
// to deliver the gotShader signal. This mirrors the real
|
||||
// MaterialExtensionInterface ctor (materialextensioninterface.cpp:23).
|
||||
ObjectBroker::registerObject(name, this);
|
||||
}
|
||||
|
||||
public slots:
|
||||
void getShader(int row) override
|
||||
{
|
||||
auto *ep = Endpoint::instance();
|
||||
if (ep)
|
||||
ep->invokeObject(name(), "getShader", QVariantList{row});
|
||||
}
|
||||
};
|
||||
|
||||
// Factory callback for ObjectBroker — called when ObjectBroker::objectInternal()
|
||||
// needs to create a client-side proxy for a MaterialExtensionInterface.
|
||||
static QObject *createMaterialExtensionProxy(const QString &name, QObject *parent)
|
||||
{
|
||||
return new MaterialExtensionProxy(name, parent);
|
||||
}
|
||||
|
||||
// Public registration function — called from GammaRaySession::initOnce().
|
||||
void registerMaterialExtensionFactory()
|
||||
{
|
||||
ObjectBroker::registerClientObjectFactoryCallback<MaterialExtensionInterface *>(
|
||||
createMaterialExtensionProxy);
|
||||
}
|
||||
|
||||
#include "material_interface.moc"
|
||||
59
bridge/src/material_interface.h
Normal file
59
bridge/src/material_interface.h
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
material_interface.h — minimal MaterialExtensionInterface proxy for the bridge.
|
||||
|
||||
SPDX-FileCopyrightText: 2026 The GammaRay MCP Bridge Authors
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
The real MaterialExtensionInterface (plugins/quickinspector/materialextension/
|
||||
materialextensioninterface.h) is NOT in the installed GammaRay headers. But the
|
||||
bridge needs to:
|
||||
1. Call getShader(row) — forwards via Endpoint::invokeObject to the probe.
|
||||
2. Receive the gotShader(QString) signal — delivered by the GammaRay protocol
|
||||
to a client-side proxy object registered under the same name.
|
||||
|
||||
This class replicates the minimal interface: a QObject with the `gotShader`
|
||||
signal and `getShader` slot, the same IID ("com.kdab.GammaRay.MaterialExtensionInterface"),
|
||||
and a constructor that self-registers via ObjectBroker::registerObject(name, this)
|
||||
— exactly like the real MaterialExtensionInterface base class does.
|
||||
|
||||
The GammaRay protocol forwards signal emissions by object name + signal
|
||||
signature. As long as our proxy has `gotShader(QString)` in its meta-object
|
||||
(guaranteed by Q_OBJECT + signals:), the signal arrives correctly.
|
||||
*/
|
||||
|
||||
#ifndef MATERIAL_INTERFACE_H
|
||||
#define MATERIAL_INTERFACE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
namespace GammaRay {
|
||||
class MaterialExtensionInterface : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MaterialExtensionInterface(const QString &name, QObject *parent = nullptr)
|
||||
: QObject(parent)
|
||||
, m_name(name)
|
||||
{
|
||||
}
|
||||
|
||||
const QString &name() const { return m_name; }
|
||||
|
||||
signals:
|
||||
void gotShader(const QString &shaderSource);
|
||||
|
||||
public slots:
|
||||
virtual void getShader(int row) = 0;
|
||||
|
||||
protected:
|
||||
QString m_name;
|
||||
};
|
||||
} // namespace GammaRay
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
Q_DECLARE_INTERFACE(GammaRay::MaterialExtensionInterface,
|
||||
"com.kdab.GammaRay.MaterialExtensionInterface")
|
||||
QT_END_NAMESPACE
|
||||
|
||||
#endif // MATERIAL_INTERFACE_H
|
||||
50
bridge/src/quickinspector_types.h
Normal file
50
bridge/src/quickinspector_types.h
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
quickinspector_types.h — minimal enum definitions matching the probe's
|
||||
QuickInspectorInterface, for use with Endpoint::invokeObject.
|
||||
|
||||
SPDX-FileCopyrightText: 2026 The GammaRay MCP Bridge Authors
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
The bridge cannot include the real quickinspectorinterface.h (not in installed
|
||||
headers — see PLAN.md open question #1). But setCustomRenderMode() takes a
|
||||
RenderMode enum by value, and Endpoint::invokeObject serializes QVariant args
|
||||
via QDataStream which uses the QVariant typeName() for custom types. So we
|
||||
need a type whose fully-qualified name matches "GammaRay::QuickInspectorInterface::RenderMode"
|
||||
exactly, and which is registered via Q_DECLARE_METATYPE on both sides.
|
||||
|
||||
This header defines just that — a nested enum inside a class with the same
|
||||
namespace/class structure. Q_DECLARE_METATYPE registers it with the matching
|
||||
type name, enabling cross-process QVariant serialization.
|
||||
*/
|
||||
|
||||
#ifndef QUICKINSPECTOR_TYPES_H
|
||||
#define QUICKINSPECTOR_TYPES_H
|
||||
|
||||
#include <QMetaType>
|
||||
#include <QObject>
|
||||
|
||||
namespace GammaRay {
|
||||
|
||||
// Minimal stand-in for the real QuickInspectorInterface — just the RenderMode
|
||||
// enum. Do NOT add Q_OBJECT (we don't need MOC for this header-only class;
|
||||
// Q_DECLARE_METATYPE below handles metatype registration).
|
||||
class QuickInspectorInterface
|
||||
{
|
||||
public:
|
||||
// Values MUST match the probe's enum (quickinspectorinterface.h:52-66).
|
||||
enum RenderMode
|
||||
{
|
||||
NormalRendering,
|
||||
VisualizeClipping,
|
||||
VisualizeOverdraw,
|
||||
VisualizeBatches,
|
||||
VisualizeChanges,
|
||||
VisualizeTraces,
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace GammaRay
|
||||
|
||||
Q_DECLARE_METATYPE(GammaRay::QuickInspectorInterface::RenderMode)
|
||||
|
||||
#endif // QUICKINSPECTOR_TYPES_H
|
||||
@@ -7,12 +7,18 @@
|
||||
|
||||
#include "scenegraph_tools.h"
|
||||
#include "gammaray_session.h"
|
||||
#include "material_interface.h"
|
||||
#include "quickinspector_types.h"
|
||||
|
||||
#include <common/endpoint.h>
|
||||
#include <endpoint.h>
|
||||
#include <message.h>
|
||||
#include <protocol.h>
|
||||
#include <objectbroker.h>
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QCoreApplication>
|
||||
#include <QEventLoop>
|
||||
#include <QItemSelectionModel>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
@@ -23,13 +29,17 @@
|
||||
// Model registration names (from plugins/quickinspector/quickinspector.cpp).
|
||||
static const char *kQuickWindowModel = "com.kdab.GammaRay.QuickWindowModel";
|
||||
static const char *kQuickSceneGraphModel = "com.kdab.GammaRay.QuickSceneGraphModel";
|
||||
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) ---
|
||||
|
||||
// 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)
|
||||
@@ -39,7 +49,6 @@ static void waitForData(const QAbstractItemModel *m, int column, int timeoutMs)
|
||||
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;
|
||||
@@ -55,7 +64,6 @@ 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;
|
||||
@@ -76,13 +84,6 @@ static QJsonArray walkChildren(const QAbstractItemModel *m, const QModelIndex &p
|
||||
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)
|
||||
@@ -102,6 +103,101 @@ static void settle(QEventLoop &loop, int ms)
|
||||
loop.exec();
|
||||
}
|
||||
|
||||
// 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 ---
|
||||
|
||||
SceneGraphTools::SceneGraphTools(GammaRaySession *session, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_session(session)
|
||||
@@ -113,13 +209,10 @@ 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()) {
|
||||
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)")
|
||||
@@ -129,6 +222,8 @@ QString SceneGraphTools::ensureSession() const
|
||||
return QString::fromUtf8(QJsonDocument(err).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
|
||||
// --- Connection management ---
|
||||
|
||||
QString SceneGraphTools::connectProbe(const QString &host, int port) const
|
||||
{
|
||||
if (!m_session)
|
||||
@@ -181,6 +276,8 @@ QString SceneGraphTools::probeStatus() const
|
||||
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
|
||||
// --- Window-level navigation ---
|
||||
|
||||
QString SceneGraphTools::listQuickWindows() const
|
||||
{
|
||||
if (const QString e = ensureSession(); !e.isEmpty())
|
||||
@@ -210,19 +307,11 @@ 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"),
|
||||
ep->invokeObject(QString::fromUtf8(kQuickInspectorIface),
|
||||
"selectWindow", QVariantList{index});
|
||||
return QString::fromUtf8(QJsonDocument(QJsonObject{
|
||||
{ QStringLiteral("status"), QStringLiteral("selected") },
|
||||
@@ -240,15 +329,493 @@ QString SceneGraphTools::listScenegraphNodes() const
|
||||
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);
|
||||
}
|
||||
// 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);
|
||||
|
||||
// Depth cap to keep responses bounded for large scenes.
|
||||
const QJsonArray tree = walkChildren(m, QModelIndex(), 16);
|
||||
return QString::fromUtf8(QJsonDocument(tree).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
|
||||
// --- 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));
|
||||
}
|
||||
|
||||
@@ -15,6 +15,17 @@
|
||||
established (via --connect at startup OR a prior connectProbe() call), a stale
|
||||
session is transparently reconnected. If no URL is known, the tool returns a
|
||||
JSON error pointing the user at connectProbe().
|
||||
|
||||
SG node selection model
|
||||
-----------------------
|
||||
Geometry/material/texture sub-models are populated by the probe-side
|
||||
PropertyController only for the *currently selected* SG node. Selection is
|
||||
driven via ObjectBroker::selectionModel(sgModel) which returns a synced
|
||||
SelectionModelClient. Calling select(index, ClearAndSelect) on it sends the
|
||||
selection to the probe, which calls setObject() on all PropertyController
|
||||
extensions, which in turn populate sgGeometryVertexModel, shaderModel, etc.
|
||||
Tools that need per-node data call ensureNodeSelected(address) first, which
|
||||
finds the node index in the (lazily-fetched) SG tree and selects it.
|
||||
*/
|
||||
|
||||
#ifndef SCENEGRAPH_TOOLS_H
|
||||
@@ -22,6 +33,7 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QModelIndex>
|
||||
|
||||
class GammaRaySession;
|
||||
|
||||
@@ -32,43 +44,66 @@ public:
|
||||
explicit SceneGraphTools(GammaRaySession *session, QObject *parent = nullptr);
|
||||
|
||||
// --- Connection management ---
|
||||
// Connect to a GammaRay probe. host defaults to 127.0.0.1, port to 11732
|
||||
// (GammaRay's default). Blocks up to ~6s for the handshake. Returns JSON:
|
||||
// {"connected":bool,"state":"ready|connecting|failed|disconnected",
|
||||
// "url":"tcp://127.0.0.1:11732","error":"<only if failed>"}
|
||||
// Even on failure the URL is remembered so subsequent tool calls will retry.
|
||||
Q_INVOKABLE QString connectProbe(const QString &host, int port) const;
|
||||
// Same with default port (overload not supported by qtmcp; use 0 to mean default).
|
||||
Q_INVOKABLE QString connectProbeDefault() const;
|
||||
// Drop the current connection and forget the URL. Subsequent tool calls
|
||||
// will require connectProbe() again.
|
||||
Q_INVOKABLE QString disconnectProbe() const;
|
||||
// Report current connection state. Returns the same JSON shape as
|
||||
// connectProbe() minus the action.
|
||||
Q_INVOKABLE QString probeStatus() const;
|
||||
|
||||
// --- Introspection ---
|
||||
// List top-level QQuickWindows. Returns compact JSON: [{"address":..,"type":..}]
|
||||
// --- Window-level navigation ---
|
||||
Q_INVOKABLE QString listQuickWindows() const;
|
||||
|
||||
// Select a Quick window by index (into listQuickWindows) on the probe side.
|
||||
// This is required before listScenegraphNodes returns anything — the
|
||||
// QuickSceneGraphModel is built for the *selected* window.
|
||||
// Uses Endpoint::invokeObject directly since QuickInspectorInterface is not
|
||||
// in the installed client headers (PLAN.md open question #1).
|
||||
Q_INVOKABLE QString selectQuickWindow(int index) const;
|
||||
|
||||
// Walk the QSGNode tree and return nested JSON:
|
||||
// [{"address":..,"type":..,"children":[...]}]
|
||||
// --- SceneGraph navigation ---
|
||||
Q_INVOKABLE QString listScenegraphNodes() const;
|
||||
// TODO: re-enable when the selection-sync / sub-model-population bug is fixed
|
||||
// Find a node by address (e.g. "0x56396e6d7400") in the SG tree and select it
|
||||
// via the synced selection model. Returns JSON with the node's type and which
|
||||
// sub-models have data. Must be called before geometry/material tools can
|
||||
// return data for that node. Selecting a non-GeometryNode clears the
|
||||
// geometry/material models (they only populate for QSGGeometryNode).
|
||||
// Q_INVOKABLE QString selectScenegraphNode(const QString &address) const;
|
||||
|
||||
// --- Geometry (requires a GeometryNode to be selected) ---
|
||||
// TODO: re-enable when the selection-sync / sub-model-population bug is fixed
|
||||
// Returns vertex data: rows = vertices, columns = attributes.
|
||||
// Each cell is the display string (e.g. "0.5, 0.5, 0, 1"); headers are
|
||||
// attribute type names (PositionAttribute, ColorAttribute, ...).
|
||||
// Also includes isCoordinate per column.
|
||||
// Q_INVOKABLE QString getNodeVertices(const QString &address) const;
|
||||
// Returns adjacency data: drawing mode + index list.
|
||||
// Q_INVOKABLE QString getNodeAdjacency(const QString &address) const;
|
||||
|
||||
// --- Material/Shader (requires a GeometryNode to be selected) ---
|
||||
// TODO: re-enable when the selection-sync / sub-model-population bug is fixed
|
||||
// List shader stages (Vertex, Fragment, ...) for the selected node's material.
|
||||
// Q_INVOKABLE QString getMaterialShaders(const QString &address) const;
|
||||
// Async: request shader source for a row (from getMaterialShaders).
|
||||
// Uses the MaterialExtensionInterface proxy + gotShader signal.
|
||||
// Q_INVOKABLE QString getShaderSource(int row) const;
|
||||
// Material properties (name/value pairs from AggregatedPropertyModel).
|
||||
// Q_INVOKABLE QString getMaterialProperties(const QString &address) const;
|
||||
|
||||
// --- Rendering visualization ---
|
||||
// Set custom render mode. mode ∈ {NormalRendering, VisualizeClipping,
|
||||
// VisualizeOverdraw, VisualizeBatches, VisualizeChanges, VisualizeTraces}
|
||||
Q_INVOKABLE QString setRenderMode(const QString &mode) const;
|
||||
// Toggle slow animations mode (renders continuously instead of on-demand).
|
||||
Q_INVOKABLE QString setSlowMode(bool enabled) const;
|
||||
|
||||
private:
|
||||
GammaRaySession *m_session;
|
||||
mutable QString m_selectedAddress; // last selected node address (for sub-model tools)
|
||||
mutable QString m_lastSelectPath; // diagnostic: last selection path info
|
||||
|
||||
// Shared helper: ensures a connection (auto-reconnect if a URL is known),
|
||||
// returns nullptr on success or a JSON error string the caller can return
|
||||
// verbatim. The JSON always hints at connectProbe() when no URL is known.
|
||||
QString ensureSession() const;
|
||||
// Find the QModelIndex for address in the SG tree (prime + search).
|
||||
// Returns invalid index if not found.
|
||||
QModelIndex findNodeByAddress(QAbstractItemModel *m, const QString &address) const;
|
||||
// Ensure the given SG node is selected (select it if not already). Returns
|
||||
// empty string on success, or a JSON error string.
|
||||
QString ensureNodeSelected(const QString &address) const;
|
||||
// Wait for a model to have rows (polling with event loop, up to timeoutMs).
|
||||
void waitForRows(QAbstractItemModel *m, int timeoutMs) const;
|
||||
};
|
||||
|
||||
#endif // SCENEGRAPH_TOOLS_H
|
||||
|
||||
Reference in New Issue
Block a user