use uv to run test, more fixes
This commit is contained in:
@@ -21,8 +21,9 @@ include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
qtmcp
|
||||
GIT_REPOSITORY https://github.com/signal-slot/qtmcp.git
|
||||
GIT_TAG main # TODO: pin to a released tag/commit for reproducibility
|
||||
GIT_TAG 2706345c44e282aba15cf020608af2283e9fb5f9
|
||||
GIT_SHALLOW TRUE
|
||||
GIT_REMOTE_UPDATE_STRATEGY CHECKOUT
|
||||
)
|
||||
# Qt-prefixed vars (NOT BUILD_EXAMPLES) — verified to suppress example/test build.
|
||||
set(QT_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||
@@ -44,6 +45,8 @@ add_executable(qml-sg-mcp-bridge
|
||||
src/scenegraph_tools.h
|
||||
src/material_interface.cpp
|
||||
src/material_interface.h
|
||||
src/quickinspector_proxy.cpp
|
||||
src/quickinspector_proxy.h
|
||||
src/quickinspector_types.h
|
||||
)
|
||||
|
||||
|
||||
@@ -7,12 +7,15 @@
|
||||
|
||||
#include "gammaray_session.h"
|
||||
|
||||
#include "quickinspector_proxy.h"
|
||||
|
||||
#include <client/clientconnectionmanager.h>
|
||||
#include <common/objectbroker.h>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QEventLoop>
|
||||
#include <QItemSelectionModel>
|
||||
#include <QMargins>
|
||||
#include <QTimer>
|
||||
|
||||
// Defined in material_interface.cpp — registers the MaterialExtensionInterface
|
||||
@@ -29,6 +32,13 @@ GammaRaySession::GammaRaySession(QObject *parent)
|
||||
QObject::connect(m_conMan, &GammaRay::ClientConnectionManager::ready, [this]() {
|
||||
m_lastError.clear();
|
||||
setState(State::Ready);
|
||||
|
||||
// Register a minimal QuickInspectorInterface proxy now that
|
||||
// Endpoint::instance() is available. The probe emits features() during
|
||||
// selectWindow — without a client-side object with the right IID,
|
||||
// GammaRay's Endpoint cannot dispatch the signal.
|
||||
GammaRay::registerQuickInspectorProxy();
|
||||
|
||||
// Pre-fetch the remote models we expose as tools. ObjectBroker::model()
|
||||
// lazily creates a RemoteModel which then syncs asynchronously from the
|
||||
// probe (~1-2s for the first rows). Requesting them now at ready() means
|
||||
@@ -66,6 +76,13 @@ GammaRaySession::~GammaRaySession() = default;
|
||||
void GammaRaySession::initOnce()
|
||||
{
|
||||
GammaRay::ClientConnectionManager::init();
|
||||
|
||||
// Register Qt types that GammaRay's QDataStream may serialize/deserialize.
|
||||
// Without these, QVariant::load() fails with "unknown user type" and the
|
||||
// GammaRay::Message stream enters an error state, crashing the bridge.
|
||||
qRegisterMetaType<QMargins>("QMargins");
|
||||
qRegisterMetaType<QMarginsF>("QMarginsF");
|
||||
|
||||
// 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
|
||||
@@ -85,11 +102,23 @@ void GammaRaySession::connectToHost(const QUrl &url)
|
||||
void GammaRaySession::disconnectFromHost()
|
||||
{
|
||||
m_reconnectScheduled = false;
|
||||
// Clear the URL so auto-reconnect won't fire after an explicit disconnect.
|
||||
// (lastUrl() returns empty afterwards — connectProbe() is required to
|
||||
// re-establish, which matches user intent for "I want this bridge off".)
|
||||
m_serverUrl = QUrl();
|
||||
m_conMan->disconnectFromHost();
|
||||
|
||||
// Wait for the async disconnect to complete. QTcpSocket::disconnectFromHost()
|
||||
// is asynchronous — Endpoint::connectionClosed() (which clears m_socket)
|
||||
// fires on a later event loop iteration. Without this wait, the next
|
||||
// connectToHost() in the same process would trigger
|
||||
// socketConnected() → Endpoint::setDevice()
|
||||
// which asserts Q_ASSERT(!m_socket) because the old socket is still set.
|
||||
{
|
||||
QEventLoop loop;
|
||||
QObject::connect(m_conMan, &GammaRay::ClientConnectionManager::disconnected,
|
||||
&loop, &QEventLoop::quit);
|
||||
QTimer::singleShot(5000, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
}
|
||||
|
||||
setState(State::Disconnected);
|
||||
}
|
||||
|
||||
@@ -98,7 +127,7 @@ bool GammaRaySession::ensureConnected(int timeoutMs)
|
||||
if (m_state == State::Ready)
|
||||
return true;
|
||||
if (!m_serverUrl.isValid())
|
||||
return false; // caller must invoke connectProbe() first
|
||||
return false;
|
||||
if (m_state == State::Disconnected || m_state == State::Failed)
|
||||
connectToHost(m_serverUrl); // kick a fresh attempt
|
||||
|
||||
|
||||
43
bridge/src/quickinspector_proxy.cpp
Normal file
43
bridge/src/quickinspector_proxy.cpp
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
quickinspector_proxy.cpp — Minimal QuickInspectorInterface client proxy.
|
||||
|
||||
SPDX-FileCopyrightText: 2026 The GammaRay MCP Bridge Authors
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "quickinspector_proxy.h"
|
||||
|
||||
#include <common/objectbroker.h>
|
||||
|
||||
#include <QMetaType>
|
||||
|
||||
namespace GammaRay {
|
||||
|
||||
QuickInspectorProxy::QuickInspectorProxy(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
// Register the metatype string for QFlags<QuickInspectorInterface::Feature>
|
||||
// as a typedef for int. The features() signal packs this type into QVariant,
|
||||
// and QVariant::load() fails with "unknown user type" without registration.
|
||||
if (QMetaType::fromName("QFlags<GammaRay::QuickInspectorInterface::Feature>").id() == QMetaType::UnknownType) {
|
||||
QMetaType::registerNormalizedTypedef(
|
||||
"QFlags<GammaRay::QuickInspectorInterface::Feature>",
|
||||
QMetaType::fromType<int>());
|
||||
}
|
||||
}
|
||||
|
||||
void registerQuickInspectorProxy()
|
||||
{
|
||||
// Guard: only register once (Endpoint::registerObject asserts on duplicate).
|
||||
static bool registered = false;
|
||||
if (registered)
|
||||
return;
|
||||
registered = true;
|
||||
|
||||
auto *proxy = new QuickInspectorProxy();
|
||||
ObjectBroker::registerObject(
|
||||
QStringLiteral("com.kdab.GammaRay.QuickInspectorInterface/1.0"),
|
||||
proxy);
|
||||
}
|
||||
|
||||
} // namespace GammaRay
|
||||
38
bridge/src/quickinspector_proxy.h
Normal file
38
bridge/src/quickinspector_proxy.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
quickinspector_proxy.h — Minimal QuickInspectorInterface client proxy.
|
||||
|
||||
SPDX-FileCopyrightText: 2026 The GammaRay MCP Bridge Authors
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
The probe emits QuickInspectorInterface signals (features, slowModeChanged,
|
||||
etc.) without a matching slot registered via ObjectBroker. This proxy
|
||||
registers with the IID as object name so GammaRay's Endpoint can dispatch
|
||||
signals. No-op stub slots prevent "cannot call method features on unknown
|
||||
object" errors.
|
||||
*/
|
||||
|
||||
#ifndef QUICKINSPECTOR_PROXY_H
|
||||
#define QUICKINSPECTOR_PROXY_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
namespace GammaRay {
|
||||
|
||||
class QuickInspectorProxy : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_CLASSINFO("IID", "com.kdab.GammaRay.QuickInspectorInterface/1.0")
|
||||
public:
|
||||
explicit QuickInspectorProxy(QObject *parent = nullptr);
|
||||
|
||||
public slots:
|
||||
void features(int) {}
|
||||
void serverSideDecorationChanged(bool) {}
|
||||
void slowModeChanged(bool) {}
|
||||
};
|
||||
|
||||
void registerQuickInspectorProxy();
|
||||
|
||||
} // namespace GammaRay
|
||||
|
||||
#endif // QUICKINSPECTOR_PROXY_H
|
||||
@@ -780,8 +780,17 @@ QString SceneGraphTools::ensureItemSelected(const QString &address) const
|
||||
|
||||
m_selectedItemAddress = address;
|
||||
|
||||
// Prime the property model to fetch its row count and cell data.
|
||||
if (propModel) { propModel->rowCount(); primeAndWait(propModel, 4, 300, 500, 1500); }
|
||||
// Wait for the property model to populate. The RemoteModel's lazy-fetch
|
||||
// mechanism requests data from the probe on the first rowCount() call.
|
||||
// After the wait, the data should have arrived.
|
||||
if (propModel) {
|
||||
propModel->rowCount();
|
||||
// Give the model time to settle. Use a longer wait since the
|
||||
// AggregatedPropertyModel has nested groups that need multiple
|
||||
// fetch rounds. primeAndWait() is avoided here because it triggers
|
||||
// a GammaRay RemoteModel use-after-free in primeTree().
|
||||
settle(loop, 1500);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
@@ -795,26 +804,28 @@ QString SceneGraphTools::selectQuickItem(const QString &address) const
|
||||
|
||||
// Report what we know about the selected item.
|
||||
auto *itemModel = m_session->model(QString::fromUtf8(kQuickItemModel));
|
||||
const QModelIndex idx = findNodeByAddress(itemModel, address);
|
||||
QString type;
|
||||
int flags = 0;
|
||||
int propertyCount = 0;
|
||||
if (idx.isValid()) {
|
||||
const QModelIndex typeIdx = itemModel->index(idx.row(), 1, idx.parent());
|
||||
type = itemModel->data(typeIdx, Qt::DisplayRole).toString();
|
||||
flags = itemModel->data(idx, kItemFlagsRole).toInt();
|
||||
QJsonObject out;
|
||||
if (!itemModel) {
|
||||
out.insert(QStringLiteral("error"), QStringLiteral("QuickItemModel gone"));
|
||||
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
|
||||
const QModelIndex idx = findNodeByAddress(itemModel, address);
|
||||
if (!idx.isValid()) {
|
||||
out.insert(QStringLiteral("error"), QStringLiteral("address %1 not found after selection").arg(address));
|
||||
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
const QModelIndex typeIdx = itemModel->index(idx.row(), 1, idx.parent());
|
||||
const QString type = itemModel->data(typeIdx, Qt::DisplayRole).toString();
|
||||
const int flags = itemModel->data(idx, kItemFlagsRole).toInt();
|
||||
int propertyCount = 0;
|
||||
auto *propModel = m_session->model(QStringLiteral("%1.properties").arg(QString::fromUtf8(kItemBaseName)));
|
||||
if (propModel)
|
||||
propertyCount = propModel->rowCount();
|
||||
|
||||
QJsonObject out = {
|
||||
{ QStringLiteral("address"), address },
|
||||
{ QStringLiteral("type"), type },
|
||||
{ QStringLiteral("flags"), flagsToJson(flags) },
|
||||
{ QStringLiteral("propertyCount"), propertyCount },
|
||||
};
|
||||
out.insert(QStringLiteral("selected"), address);
|
||||
out.insert(QStringLiteral("type"), type);
|
||||
out.insert(QStringLiteral("flags"), flags);
|
||||
out.insert(QStringLiteral("propertyCount"), propertyCount);
|
||||
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
|
||||
@@ -839,6 +850,30 @@ QString SceneGraphTools::getItemProperties(const QString &address) const
|
||||
QStringLiteral("no properties available (did you select a valid QML item?)")))
|
||||
.toJson(QJsonDocument::Compact));
|
||||
|
||||
// The RemoteModel uses lazy-fetch: data() triggers a fetch and returns
|
||||
// "Loading..." until the fetch response arrives on the event loop.
|
||||
// Poll until column 0 data arrives for at least one row.
|
||||
{
|
||||
QEventLoop loop;
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
bool gotData = false;
|
||||
while (std::chrono::steady_clock::now() < deadline && !gotData) {
|
||||
for (int r = 0; r < rows; ++r) {
|
||||
const QString val = m->data(m->index(r, 0), Qt::DisplayRole).toString();
|
||||
if (!val.isEmpty() && val != QStringLiteral("Loading...")) {
|
||||
gotData = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!gotData) {
|
||||
QTimer::singleShot(200, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-read rowCount after polling (more rows may have arrived)
|
||||
const int actualRows = m->rowCount();
|
||||
const int cols = m->columnCount();
|
||||
|
||||
QJsonObject out;
|
||||
@@ -846,34 +881,76 @@ QString SceneGraphTools::getItemProperties(const QString &address) const
|
||||
for (int c = 0; c < cols; ++c)
|
||||
headers.append(m->headerData(c, Qt::Horizontal, Qt::DisplayRole).toString());
|
||||
|
||||
// Pre-fetch children for grouped properties. RemoteModel uses lazy-fetch:
|
||||
// rowCount(parent) triggers an async request and returns 0 until the reply
|
||||
// arrives on the event loop. After polling, hasChildren() works correctly.
|
||||
for (int r = 0; r < actualRows; ++r) {
|
||||
m->rowCount(m->index(r, 0));
|
||||
}
|
||||
// Poll to let row count responses arrive
|
||||
if (actualRows > 0) {
|
||||
QEventLoop l2;
|
||||
QTimer::singleShot(500, &l2, &QEventLoop::quit);
|
||||
l2.exec();
|
||||
}
|
||||
// Second pass: ensure child data is fetched for rows that have children
|
||||
for (int r = 0; r < actualRows; ++r) {
|
||||
const QModelIndex idx0 = m->index(r, 0);
|
||||
if (m->hasChildren(idx0) && m->canFetchMore(idx0))
|
||||
m->fetchMore(idx0);
|
||||
}
|
||||
if (actualRows > 0) {
|
||||
QEventLoop l2b;
|
||||
QTimer::singleShot(500, &l2b, &QEventLoop::quit);
|
||||
l2b.exec();
|
||||
}
|
||||
|
||||
// Top-level properties (simple Q_PROPERTY values like x, y, width, height...)
|
||||
QJsonObject simpleProps;
|
||||
// Nested property groups (anchors, transform, etc.) will have children.
|
||||
QJsonObject groups;
|
||||
for (int r = 0; r < rows; ++r) {
|
||||
for (int r = 0; r < actualRows; ++r) {
|
||||
const QString name = m->data(m->index(r, 0), Qt::DisplayRole).toString();
|
||||
const QString val = m->data(m->index(r, 1), Qt::DisplayRole).toString();
|
||||
const QString ptype = cols > 2 ? m->data(m->index(r, 2), Qt::DisplayRole).toString() : QString();
|
||||
|
||||
// Check if this property has children (nested sub-properties).
|
||||
QJsonObject prop;
|
||||
prop.insert(QStringLiteral("value"), val);
|
||||
prop.insert(QStringLiteral("type"), ptype);
|
||||
|
||||
if (m->hasChildren(m->index(r, 0))) {
|
||||
const QModelIndex idx0 = m->index(r, 0);
|
||||
if (m->hasChildren(idx0)) {
|
||||
// For grouped properties (anchors, transform, etc.), collect children.
|
||||
QJsonObject children;
|
||||
for (int cr = 0; cr < m->rowCount(m->index(r, 0)); ++cr) {
|
||||
const QString cname = m->data(m->index(cr, 0, m->index(r, 0)), Qt::DisplayRole).toString();
|
||||
const QString cval = m->data(m->index(cr, 1, m->index(r, 0)), Qt::DisplayRole).toString();
|
||||
const QString ctype = cols > 2 ? m->data(m->index(cr, 2, m->index(r, 0)), Qt::DisplayRole).toString() : QString();
|
||||
QJsonObject child;
|
||||
child.insert(QStringLiteral("value"), cval);
|
||||
child.insert(QStringLiteral("type"), ctype);
|
||||
children.insert(cname, child);
|
||||
// Children also use lazy-fetch, so poll if needed.
|
||||
const int childRows = m->rowCount(idx0);
|
||||
if (childRows > 0) {
|
||||
// Trigger child data fetches first
|
||||
for (int cr = 0; cr < childRows; ++cr) {
|
||||
m->data(m->index(cr, 0, idx0), Qt::DisplayRole);
|
||||
m->data(m->index(cr, 1, idx0), Qt::DisplayRole);
|
||||
}
|
||||
// Poll to let child data arrive
|
||||
{
|
||||
QEventLoop l3;
|
||||
QTimer::singleShot(500, &l3, &QEventLoop::quit);
|
||||
l3.exec();
|
||||
}
|
||||
// Read actual child data
|
||||
QJsonObject children;
|
||||
for (int cr = 0; cr < childRows; ++cr) {
|
||||
const QString cname = m->data(m->index(cr, 0, idx0), Qt::DisplayRole).toString();
|
||||
const QString cval = m->data(m->index(cr, 1, idx0), Qt::DisplayRole).toString();
|
||||
const QString ctype = cols > 2 ? m->data(m->index(cr, 2, idx0), Qt::DisplayRole).toString() : QString();
|
||||
if (cname.isEmpty() || cname == QStringLiteral("Loading..."))
|
||||
continue;
|
||||
QJsonObject child;
|
||||
child.insert(QStringLiteral("value"), cval);
|
||||
child.insert(QStringLiteral("type"), ctype);
|
||||
children.insert(cname, child);
|
||||
}
|
||||
if (!children.isEmpty())
|
||||
prop.insert(QStringLiteral("children"), children);
|
||||
}
|
||||
if (!children.isEmpty())
|
||||
prop.insert(QStringLiteral("children"), children);
|
||||
groups.insert(name, prop);
|
||||
} else {
|
||||
simpleProps.insert(name, prop);
|
||||
|
||||
Reference in New Issue
Block a user