more progress, but some of them are wip

This commit is contained in:
2026-07-07 16:48:44 +08:00
parent 431ca6e7b1
commit 0f484b78bb
10 changed files with 986 additions and 93 deletions

132
PLAN.md
View File

@@ -333,32 +333,89 @@ Map GammaRay client capabilities to MCP tool calls.
All introspection tools below call `session->ensureConnected(timeoutMs)` first; if a URL is known and the session is `Disconnected`/`Failed`, they transparently reconnect. If no URL is known, they return a JSON error pointing the client at `connectProbe()`.
### Navigation tools
- `list_quick_windows` → read `com.kdab.GammaRay.QuickWindowModel`, return window addresses
- `select_quick_window(index)``QuickInspectorInterface::selectWindow(int)`
- `list_quick_items` → traverse `com.kdab.GammaRay.QuickItemModel`, return item tree (id, type, geometry)
- `list_scenegraph_nodes(parentObjectId?)` → traverse `com.kdab.GammaRay.QuickSceneGraphModel`, return node tree (id, type, address)
- `get_scenegraph_node_detail(objectId)` → select node via selection model, read property models
- `listQuickWindows()` → read `com.kdab.GammaRay.QuickWindowModel`, return window addresses
- `selectQuickWindow(index)``QuickInspectorInterface::selectWindow(int)`
- `listQuickItems()` → traverse `com.kdab.GammaRay.QuickItemModel`, return item tree (id, type, geometry)*not yet implemented*
- `listScenegraphNodes()` → traverse `com.kdab.GammaRay.QuickSceneGraphModel`, return node tree (id, type, address). Uses `primeAndWait()` for async fill.
- `selectScenegraphNode(address)` → select node via selection model (currently blocked — see Known issues)
### Geometry tools
- `get_node_vertices(objectId)` → select SG node, read `sgGeometryVertexModel` (vertices: x, y, z, w + render role)
- `get_node_adjacency(objectId)` → read `sgGeometryAdjacencyModel` (drawing mode, vertex indices)
- `getNodeVertices(address)` → select SG node, read `sgGeometryVertexModel` (vertices: x, y, z, w + render role)*blocked*
- `getNodeAdjacency(address)` → read `sgGeometryAdjacencyModel` (drawing mode, vertex indices)*blocked*
### Material/Shader tools
- `get_material_shaders(objectId)` → read `shaderModel` for selected node
- `get_shader_source(row)``MaterialExtensionInterface::getShader(int)` (async: listen for `gotShader` signal)
- `get_material_properties(objectId)` → read `materialPropertyModel`
- `getMaterialShaders(address)` → read `shaderModel` for selected node*blocked*
- `getShaderSource(row)``MaterialExtensionInterface::getShader(int)` (async: listen for `gotShader` signal)*implemented, untested end-to-end (blocked upstream)*
- `getMaterialProperties(address)` → read `materialPropertyModel`*blocked*
### Rendering visualization tools
- `set_render_mode(mode)``QuickInspectorInterface::setCustomRenderMode(mode)` where mode ∈ {Normal, VisualizeClipping, VisualizeOverdraw, VisualizeBatches, VisualizeChanges, VisualizeTraces} (quickinspectorinterface.h:52-66)
- `set_slow_mode(bool)``QuickInspectorInterface::setSlowMode(bool)`
- `setRenderMode(mode)``QuickInspectorInterface::setCustomRenderMode(mode)` where mode ∈ {NormalRendering, VisualizeClipping, VisualizeOverdraw, VisualizeBatches, VisualizeChanges, VisualizeTraces} (quickinspectorinterface.h:52-66). **NOTE**: takes enum NAME string, not int. Values map 1:1 to GammaRay's `RenderMode` enum.
- `setSlowMode(enabled)``QuickInspectorInterface::setSlowMode(bool)`
### Texture tools
- `grab_texture(objectId)``TextureExtension` (async image grab; image arrives via RemoteViewInterface as TransferImage)
- `grabTexture(objectId)``TextureExtension` (async image grab; image arrives via `RemoteViewInterface` as `TransferImage`) — *not yet implemented*
### Implementation status
-`list_quick_windows` (as `listQuickWindows`), `select_quick_window` (as `selectQuickWindow`), `list_scenegraph_nodes` (as `listScenegraphNodes`) — working end-to-end against a live probe.
- ✅ Connection management (`connectProbe`/`connectProbeDefault`/`disconnectProbe`/`probeStatus`) — implemented; bridge no longer hard-requires `--connect`.
- ⏳ Geometry/Material/Shader/Render/Texture tools — pending. They require (a) selecting an SG node via the `QuickSceneGraphModel` selection model so the per-node `sgGeometryVertexModel`/`sgGeometryAdjacencyModel`/`shaderModel`/`materialPropertyModel` populate, and (b) for shader/texture the bridge needs to receive async signals from `MaterialExtensionInterface`/`TextureExtension` (which are NOT in installed headers — will need either a thin custom client stub connected via `Endpoint::registerObject` on the client side, or a signal-forwarding shim built on `Endpoint::invokeObject` + a probe-side callback. To be designed.)
### Implementation status (updated — see "Known issues / Blocked" section for details on the sub-model population blocker)
**Working end-to-end (verified via stdio MCP smoke test against a live probe):**
- `connectProbe(host?, port?)` / `connectProbeDefault()` / `disconnectProbe()` / `probeStatus()` — lazy connect + auto-reconnect state machine (`Disconnected → Connecting → Ready`, with `→ Failed` on persistent error). `disconnectProbe()` clears remembered URL and stops auto-reconnect.
-`listQuickWindows()` — reads `QuickWindowModel`, returns `[{address, type}]`.
-`selectQuickWindow(index)` — forwards via `Endpoint::invokeObject("com.kdab.GammaRay.QuickInspectorInterface/1.0", "selectWindow", {index})`.
-`listScenegraphNodes()` — returns real QSGNode tree (verified: returns 11 GeometryNodes on the test QML). Uses signal-driven `primeAndWait()` (see Implementation findings) — first call may show "Loading..." for deep nodes, second call returns the full tree.
-`setRenderMode(mode)` — accepts render-mode enum **name** string (NOT int): `NormalRendering` / `VisualizeClipping` / `VisualizeOverdraw` / `VisualizeBatches` / `VisualizeChanges` / `VisualizeTraces`. Calls `invokeObject(..., "setCustomRenderMode", {QVariant::fromValue<RenderMode>(mode)})`. Cross-process QVariant serialization works thanks to `quickinspector_types.h` `RenderMode` enum + `Q_DECLARE_METATYPE` with matching fully-qualified name `GammaRay::QuickInspectorInterface::RenderMode`.
-`setSlowMode(enabled)` — calls `invokeObject(..., "setSlowMode", {bool})`. Verified by reading back state.
**Implemented but BLOCKED by selection sync issue — DISABLED (wrapped in `#if 0` with TODO markers; registration removed from `main.cpp`). Re-enable when the bug is fixed:**
-`selectScenegraphNode(address)` (disabled) — finds node in SG tree, selects via `ObjectBroker::selectionModel(sgModel)` + direct `Message` send via `Protocol::fromQModelIndex`. Caches `m_selectedAddress` to avoid redundant reselection. Selection IS sent and IS received by the probe (`sgSelectionChanged()` fires), but the probe-side selection maps to a `QSGTransformNode` (row 0) instead of the target `QSGGeometryNode`. Sub-models stay empty.
-`getNodeVertices(address)` (disabled) — reads `sgGeometryVertexModel` (display + headers + `kIsCoordinateRole=257`). Returns `rowCount=0` because the model is never populated (selection wrong on probe side).
-`getNodeAdjacency(address)` (disabled) — reads `sgGeometryAdjacencyModel` (drawing mode + indices, `kDrawingModeRole=257`, `kRenderRole=258`). Same blocker.
-`getMaterialShaders(address)` (disabled) — reads `shaderModel` (shader stage names). Same blocker.
-`getShaderSource(row)` (disabled) — async path implemented: `MaterialExtensionInterface` proxy (`material_interface.{h,cpp}`) with matching IID `com.kdab.GammaRay.MaterialExtensionInterface`, self-registers via `ObjectBroker::registerObject`, factory registered in `initOnce()`. Calls `invokeObject(..., "getShader", {row})` and waits for `gotShader(QString)` signal. **Untested end-to-end** because the upstream `shaderModel` is never populated.
-`getMaterialProperties(address)` (disabled) — reads `materialPropertyModel`. Same blocker.
**Not yet implemented:**
-`grabTexture(objectId)``TextureExtension` async image grab via `RemoteViewInterface` `TransferImage`. Not started (depends on selection working first to populate the texture list).
### Known issues / Blocked — sub-models not populating after SG node selection
This is the single biggest open issue. Extensive debugging has narrowed it down but not yet fixed it.
**Symptom**: After `selectScenegraphNode(address)` selects a QSGGeometryNode, all four sub-models (`sgGeometryVertexModel` / `sgGeometryAdjacencyModel` / `shaderModel` / `materialPropertyModel`) stay at `rowCount=0` / `columnCount=0`. Tested against all 11 GeometryNodes in the test QML — none populate.
**What's been verified working**:
- All four sub-model addresses are valid on the probe (vertex=161, adjacency=162, shader=154, materialProp=153, selectionModel=142).
- The client-side selection model name is correct: `"com.kdab.GammaRay.QuickSceneGraphModel.selection"` (NOT `".selection"``_diag_dotSelAddr=0` confirms the dotted form is wrong).
- `sourceModelForProxy(sgFilterProxy)` returns `sgFilterProxy` itself (it's registered), so `SelectionModelServer` is created directly for it with the name matching the client's `SelectionModelClient`.
- Client-side selection IS correct: `hasSelection()=true`, `selectedRows()` returns the expected index with the full path matching the target address.
- `Endpoint::isConnected()=true` — selection messages ARE being sent.
- Probe-side `QuickInspector::sgSelectionChanged()` IS being called (confirmed via `qWarning()` debug logging in the journal — added to `/home/blumia/Sources/GammaRay/plugins/quickinspector/quickinspector.cpp:737`).
**Root cause identified (still being narrowed down)**:
The server's `sendSelection()` (triggered by `SelectionModelStateRequest` from `SelectionModelClient`'s 125ms timer, OR by a model-change timer) selects the DEFAULT item (row 0 at root = `QSGTransformNode`) and sends `SelectionModelSelect` back to the client, overriding our selection. Even with: a 2000ms settle before selecting, re-sending the selection twice, manually constructing `SelectionModelSelect` messages via `Protocol::fromQModelIndex`, and turning off slow mode — the sub-models still return `rowCount=0`.
The second `sgSelectionChanged` call shows `parentValid=true parentRow=0` — selection IS reaching a child node, but it's a `QSGTransformNode`, NOT our `QSGGeometryNode`. The path (0,0)→(0,0)→...→(0,0) is either:
(a) being truncated on the wire,
(b) mapping to the wrong node on the server side via `translateSelection()` / `toQModelIndex()`, OR
(c) the server's default-selection response is arriving AFTER our selection and overriding it.
**Things tried that did NOT fix it**:
- Waiting longer (2s settle before + after selection, re-sending selection twice).
- Disabling slow mode before selection.
- Manually constructing `SelectionModelSelect` messages via `Protocol::fromQModelIndex` (bypassing `SelectionModelClient`).
- Turning off slow mode.
**Things NOT yet tried** (see Next steps):
- Add diagnostic logging to the probe-side `NetworkSelectionModel::translateSelection()` and `toQModelIndex()` to trace exactly what path arrives and how it's walked.
- Check whether the server's `sendSelection()` response is arriving AFTER our selection and overriding it (could disable the `SelectionModelClient`'s `requestSelection` 125ms one-shot timer, or call `sendSelection()` on the server after our selection to force-sync).
- Patch `QuickInspector::selectSGNode()` (line 474, currently NOT Q_INVOKABLE) to make it Q_INVOKABLE so the bridge can call it directly via `invokeObject`, bypassing the whole selection-sync mechanism.
- Run the GammaRay GUI client (`xvfb-run` or `QT_QPA_PLATFORM=offscreen`) against the same probe to verify the selection mechanism works at all with this probe setup — if the GUI also fails to populate sub-models, the bug is in the probe plugin, not the bridge.
### Half-finished artifacts to clean up once the blocker is resolved
- **Debug `qWarning()` logging in probe plugin**: `/home/blumia/Sources/GammaRay/plugins/quickinspector/quickinspector.cpp:737` (`SGDBG:` lines). Must rebuild+install the probe plugin after any change: `cmake --build /home/blumia/Sources/GammaRay/build -j --target gammaray_quickinspector && cmake --install /home/blumia/Sources/GammaRay/build --prefix $(pwd)/install-prefix`.
- **`m_lastSelectPath` diagnostic field in `SceneGraphTools`**: added to `scenegraph_tools.{h,cpp}` to log the serialized selection path. Pure diagnostic — should be removed (or kept behind a `#ifndef QT_NO_DEBUG`) once the issue is fixed.
- **Disabled tool registrations in `main.cpp` + `scenegraph_tools.{h,cpp}`**: 6 broken tools (`selectScenegraphNode` / `getNodeVertices` / `getNodeAdjacency` / `getMaterialShaders` / `getShaderSource` / `getMaterialProperties`) wrapped in `#if 0` or commented out with `TODO` markers. Remove the `#if 0` / uncomment when the blocker is fixed.
- `listScenegraphNodes` first-call fill stability — first call still shows "Loading..." for deep nodes; second call returns the full tree. The `primeAndWait()` loop already handles this, but a single-call fix would be nicer.
## Step 7: Launch and run
@@ -450,24 +507,34 @@ When in doubt, these files have the ground truth:
-**Step 1 done**: GammaRay 3.4.0 built + installed to `install-prefix/`. Protocol version 38.
-**Minimal client built & run**: `minimal-client/` links `gammaray_client` + `gammaray_common`, connects to a live probe, receives `ready()`, fetches `QuickSceneGraphModel` / `QuickWindowModel` via `ObjectBroker` — clean exit. Proves the client ABI + protocol path is viable for the bridge.
-**qtmcp FetchContent viable**: `Qt6::McpServer`/`Qt6::McpCommon` build & link from a `FetchContent_MakeAvailable()` call. No separate clone/install.
-**Bridge scaffold built & working end-to-end** (`bridge/`): `qml-sg-mcp-bridge` links GammaRay client + qtmcp, runs as a stdio MCP server, and against a live probe returns REAL data:
-**Bridge scaffold built & working end-to-end** (`bridge/`): `qml-sg-mcp-bridge` links GammaRay client + qtmcp, runs as a stdio MCP server. Smoke test (`/tmp/opencode/smoke_test.py`) against a live probe confirms:
- `initialize` → serverInfo + capabilities + protocolVersion `2024-11-05`
- `tools/list``connectProbe`, `connectProbeDefault`, `disconnectProbe`, `probeStatus`, `listQuickWindows`, `selectQuickWindow`, `listScenegraphNodes`
- `connectProbe(host, port)` → connects + waits for handshake, returns `{connected, state, url, error?}`
- `probeStatus()``{state, ready, url, error?}` (verified: returns `disconnected` with empty url when no probe configured; returns `ready` after `connectProbe`)
- `listQuickWindows``[{"address":"0x... (QQuickWindowQmlImpl)"}]`
- `selectQuickWindow(index)` → forwards via `Endpoint::invokeObject("com.kdab.GammaRay.QuickInspectorInterface/1.0", "selectWindow", {index})`
- `listScenegraphNodes` → nested QSGNode tree (Root Node → Transform Node → ...)
-**Connection model revised**: bridge no longer hard-requires `--connect` at startup. Lazy connect + `connectProbe` MCP tool + auto-reconnect on dropped session. See "Connection model (decided)" in Step 7. State machine (`Disconnected → Connecting → Ready`, with `→ Failed` on persistent error) exposed via `probeStatus()`. Verified via standalone stdio test: with no `--connect`, `probeStatus` returns `disconnected`/empty URL; `listQuickWindows` returns a JSON error hinting at `connectProbe`; after `connectProbe(127.0.0.1, 11732)` against a live probe, tools return real data.
- `tools/list` 15 tools registered: `connectProbe`, `connectProbeDefault`, `disconnectProbe`, `getMaterialProperties`, `getMaterialShaders`, `getNodeAdjacency`, `getNodeVertices`, `getShaderSource`, `listQuickWindows`, `listScenegraphNodes`, `probeStatus`, `selectQuickWindow`, `selectScenegraphNode`, `setRenderMode`, `setSlowMode`
- `probeStatus` (before connect) → `{"ready":false,"state":"disconnected","url":""}`
- `connectProbeDefault()``{"connected":true,"state":"ready","url":"tcp://127.0.0.1:11732"}`
- `probeStatus` (after connect)`{"ready":true,"state":"ready","url":"tcp://127.0.0.1:11732"}`
- `listQuickWindows``[{"address":"0x... (ApplicationWindow_QMLTYPE_0)","type":""}]`
- `selectQuickWindow(0)``{"index":0,"status":"selected"}`
- `setSlowMode(true)``{"slowMode":true}`
- `setRenderMode("VisualizeOverdraw")``{"renderMode":"VisualizeOverdraw"}` ✅ (NOTE: takes enum name string, NOT int — see "Known issues")
- `setRenderMode("NormalRendering")``{"renderMode":"NormalRendering"}`
- `disconnectProbe()``{"ready":false,"state":"disconnected","url":""}`
- `listScenegraphNodes` (round 2, after 1s sleep) → full QSGNode tree with 11 GeometryNodes ✅
-**Connection model revised**: bridge no longer hard-requires `--connect` at startup. Lazy connect + `connectProbe` MCP tool + auto-reconnect on dropped session. See "Connection model (decided)" in Step 7. State machine (`Disconnected → Connecting → Ready`, with `→ Failed` on persistent error) exposed via `probeStatus()`.
### Key implementation findings (new, verified during scaffold work)
- **`Endpoint::invokeObject()` needs the object name == interface IID, NOT the bare name**. `QuickInspectorInterface` is registered via `ObjectBroker::registerObject<QuickInspectorInterface*>(this)` (quickinspectorinterface.cpp:59), which derives the name from `qobject_interface_iid` = `"com.kdab.GammaRay.QuickInspectorInterface/1.0"`. The bare `"com.kdab.GammaRay.QuickInspectorInterface"` is NOT in the client's name map → `Q_ASSERT(obj)` crash in debug builds. Also the method arg is the bare slot name `"selectWindow"` (no `(int)` signature), matching `QuickInspectorClient::selectWindow`.
- **No client-side stub object needed for `invokeObject`**: it only uses the name→address mapping (synced at handshake `ready()`), not a local object. So `selectQuickWindow` works without registering any factory/stub — `Endpoint::invokeObject` is public in the installed `endpoint.h`. (This resolves open question #1's "approach (b)" more cleanly than expected.)
- **RemoteModel is lazily fetched** (client/remotemodel.cpp): `data()`, `rowCount()`, `hasChildren()` return placeholders ("Loading..." / 0 / false) and queue server fetches whose results arrive async via `dataChanged`/`rowsInserted`. So a tool that reads a model right after `ready()` gets stale/empty data. The scaffold handles this with: `waitForReady()` (block until handshake), `waitForData()` (poll column 0 until non-"Loading..."), and for the SG tree a `primeTree()` + `settle()` round-robin (walk touching every cell + rowCount to queue fetches, run the event loop ~600ms, repeat 4x) before snapshotting with `walkChildren()`.
- **RemoteModel is lazily fetched** (client/remotemodel.cpp): `data()`, `rowCount()`, `hasChildren()` return placeholders ("Loading..." / 0 / false) and queue server fetches whose results arrive async via `dataChanged`/`rowsInserted`. So a tool that reads a model right after `ready()` gets stale/empty data. The scaffold handles this with: `waitForReady()` (block until handshake), `waitForData()` (poll column 0 until non-"Loading..."), and for the SG tree a `primeAndWait()` (see below) before snapshotting with `walkChildren()`.
- **`primeAndWait()` — signal-driven tree loading** (new): replaces the old fixed-time `primeTree()`+`settle()` loop. Tracks `dataChanged`/`rowsInserted` signals on the model, settles until quiet, then a final long settle. Signature `primeAndWait(m, rounds, settleMs, quietMs, finalSettleMs)` — 6 rounds of prime+settle (300ms settle, 500ms quiet), then final 2s settle + re-prime. Fixes "Loading..." for deep nodes: second call to `listScenegraphNodes` returns the full tree (verified: 11 GeometryNodes on test QML). First call may still show "Loading..." for deep nodes because RemoteModel's `requestDataAndFlags()` sets a cell to "Loading" state and won't re-request until the response arrives — the second call is the workaround.
- **`QApplication` required** (already noted): the headless bridge runs `QApplication` + `QT_QPA_PLATFORM=offscreen`.
- **FetchContent + qtmcp CMake scope gotcha**: a top-level `find_package(Qt6)`/`find_package(GammaRay)` BEFORE `FetchContent_MakeAvailable(qtmcp)` creates `Threads::Threads` at top scope; qtmcp's subdirectory then fails to promote it to global ("not built in this directory"). Fix: run `FetchContent_MakeAvailable(qtmcp)` FIRST (so qtmcp's own `find_package(Qt6)` creates+promotes 3rd-party targets inside its subdirectory), THEN `find_package(GammaRay)`.
- **qtmcp tool return types**: only `void`/`bool`/`QString`/`QStringList`/`QImage` (sync) + `QFuture<QList<QMcpCallToolResultContent>>` (async). `QJsonObject` hits `qFatal`. Structured results → JSON-serialized `QString` (see Step 2 tool section).
- **Cross-process QVariant enum serialization** (new, verified): `setRenderMode` needs to send a `RenderMode` enum value through `Endpoint::invokeObject()``QDataStream` → probe. The bridge defines a minimal `RenderMode` enum in `quickinspector_types.h` with `Q_DECLARE_METATYPE(GammaRay::QuickInspectorInterface::RenderMode)` so the enum's fully-qualified name matches GammaRay's. Values match exactly: `NormalRendering=0`, `Overdraw=1`, `Batches=2`, `Changes=3` (and `VisualizeClipping`/`VisualizeTraces` from the interface). Without the matching `Q_DECLARE_METATYPE`, the enum fails to serialize and the probe silently ignores the call.
- **`MaterialExtensionInterface` proxy pattern** (new, implemented): `MaterialExtensionInterface` and `MaterialExtensionClient` are NOT in installed headers — they're plugin-private. To receive the `gotShader(QString)` signal from the probe, the bridge defines a minimal `MaterialExtensionInterface` proxy class with the SAME IID (`com.kdab.GammaRay.MaterialExtensionInterface`), a `gotShader(QString)` signal, and a `getShader(int row)` slot that forwards via `Endpoint::invokeObject("com.kdab.GammaRay.QuickSceneGraph.material", "getShader", {row})`. The concrete `MaterialExtensionProxy` self-registers via `ObjectBroker::registerObject(name, this)` and the factory is registered via `registerClientObjectFactoryCallback<MaterialExtensionInterface*>()` in `initOnce()`. The `material_interface.moc` include is required (it's a header-only Q_OBJECT class — `#include "material_interface.moc"` at the bottom of the .cpp).
- **Direct `Message` construction for selection** (new, for the blocked selection work): `bridge/CMakeLists.txt` adds `/home/blumia/Sources/GammaRay/common` to `target_include_directories` so the bridge can include `<message.h>` and `<protocol.h>` directly from the GammaRay source tree. This lets the bridge manually construct a `SelectionModelSelect` message via `Protocol::fromQModelIndex(idx)` and send it with `Endpoint::send(msg)`, bypassing the `SelectionModelClient`'s state machine. `Protocol::fromQModelIndex()` serializes the QModelIndex as a path of (row, column) pairs — NOT internal pointers — so this works across processes. (See "Known issues / Blocked" — this bypass did NOT fix the sub-model population issue, but is kept for future debugging.)
- **Selection mechanism chain** (verified by reading source): `ObjectBroker::selectionModel(sgModel)` creates a `SelectionModelClient` (name = `model->objectName() + ".selection"`), which syncs to the probe's `SelectionModelServer`. The probe's `QuickInspector::sgSelectionChanged()` reads `ObjectModel::ObjectRole``QSGNode*`, then calls `m_sgPropertyController->setObject(node, className)`, which calls `setObject()` on all extensions (SGGeometryExtension, MaterialExtension, etc.) which populate the sub-models. `sourceModelForProxy()` stops at the first REGISTERED model (in `ObjectBroker`'s model map), not at the root source model — for `sgFilterProxy` (which IS registered), it returns `sgFilterProxy` itself, so `SelectionModelServer` is created directly for it.
### Running the bridge
@@ -485,4 +552,15 @@ cmake --build build
```
`run.sh` sets `LD_LIBRARY_PATH` (GammaRay + qtmcp libs) and `QT_PLUGIN_PATH` (qtmcp `mcpserverbackend/libqmcpserverstdio.so`) and `QT_QPA_PLATFORM=offscreen`.
-**Next**: implement the remaining Step 6 tools (`get_node_vertices`/`get_node_adjacency``sgGeometryVertexModel`/`sgGeometryAdjacencyModel`, `get_material_shaders`/`get_shader_source``shaderModel` + `MaterialExtensionInterface::getShader` async, `get_material_properties`, `set_render_mode`/`set_slow_mode` via `QuickInspectorInterface` invokeObject, `grab_texture`). The geometry/material models require selecting a SG node first (via a selection model on `QuickSceneGraphModel`), which is the next pattern to wire up.
## Next steps
1. **Unblock sub-model population** — the single highest-priority issue. Try (in order of likelihood):
1. **Run the GammaRay GUI client** (`xvfb-run install-prefix/bin/gammaray-client` or `QT_QPA_PLATFORM=offscreen`) against the same probe and click a GeometryNode. If the GUI ALSO fails to populate the vertex/adjacency/shader/material-property sub-models, the bug is in the probe plugin (or the offscreen platform), not the bridge — re-investigate `SGGeometryExtension::setObject()` and the `typeName == "QSGGeometryNode"` check in `sggeometryextension.cpp`. If the GUI WORKS, the bug is in our selection-sync path.
2. **Add diagnostic logging to probe-side `NetworkSelectionModel::translateSelection()` and `toQModelIndex()`** (in `/home/blumia/Sources/GammaRay/common/networkselectionmodel.cpp` and `common/protocol.cpp`) to trace exactly what path arrives on the wire and how it's walked back into a QModelIndex. The current `SGDBG:` logging only shows the FINAL node, not the path translation.
3. **Check if `sendSelection()` response arrives AFTER our selection and overrides it** — disable the `SelectionModelClient`'s `requestSelection` 125ms one-shot timer (`client/selectionmodelclient.cpp:21`), OR call `sendSelection()` on the server AFTER our selection to force-sync the client's state to OUR selection instead of the server's default.
4. **Patch `QuickInspector::selectSGNode()` to be Q_INVOKABLE** (`/home/blumia/Sources/GammaRay/plugins/quickinspector/quickinspector.cpp:474`). This bypasses the entire selection-sync mechanism — the bridge calls `invokeObject("com.kdab.GammaRay.QuickInspectorInterface/1.0", "selectSGNode", {QVariant::fromValue<void*>(nodePtr)})` directly. Requires the bridge to send the raw QSGNode pointer (which it has from the `QuickSceneGraphModel`'s `ObjectRole`). This is the most invasive option (modifies GammaRay source) but the most likely to work.
2. **Clean up debug artifacts** once the blocker is resolved — remove the `SGDBG:` `qWarning()` logging in `quickinspector.cpp:737`, remove the `m_lastSelectPath` diagnostic field in `scenegraph_tools.{h,cpp}`.
3. **Test `getShaderSource` end-to-end** once `shaderModel` populates — verify the `MaterialExtensionInterface` proxy + `invokeObject("getShader", {row})` + `gotShader(QString)` signal flow works.
4. **Implement `grabTexture(objectId)`**`TextureExtension` async image grab via `RemoteViewInterface` `TransferImage`. Follows the same `MaterialExtensionInterface` proxy pattern: a thin `TextureExtensionInterface` proxy with matching IID. Depends on selection working first to populate the texture list.
5. **Fix `listScenegraphNodes` first-call fill stability** — currently the first call shows "Loading..." for deep nodes and the second call returns the full tree. Either: (a) make `primeAndWait()` smarter (detect when a "Loading..." cell has been requested but not yet answered and wait for the `dataChanged`), or (b) just call `primeAndWait()` twice internally within a single `listScenegraphNodes` call.
6. **Pin qtmcp `GIT_TAG`** to a specific commit/tag in `bridge/CMakeLists.txt` for reproducibility (currently tracks `main`).

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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);

View 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"

View 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

View 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

View File

@@ -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));
}

View File

@@ -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