tools for qml item properties

This commit is contained in:
2026-07-07 20:31:55 +08:00
parent 47da444d74
commit 316d52bfe5
3 changed files with 174 additions and 6 deletions

View File

@@ -93,6 +93,11 @@ int main(int argc, char **argv)
// SG node selection
{ 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)") },
// QML item selection + properties
{ QStringLiteral("selectQuickItem"), QStringLiteral("Select a QML item by address (from listQuickItems) so item properties populate for it") },
{ QStringLiteral("selectQuickItem/address"), QStringLiteral("Item address (e.g. 0x55e451db7200, from listQuickItems)") },
{ QStringLiteral("getItemProperties"), QStringLiteral("Get all Q_PROPERTY values (x, y, width, height, opacity, visible, z, anchors, etc.) for a QML item") },
{ QStringLiteral("getItemProperties/address"), QStringLiteral("Item address (from listQuickItems)") },
// Geometry
{ QStringLiteral("getNodeVertices"), QStringLiteral("Get vertex data for a GeometryNode (vertices, attributes, isCoordinate flags)") },
{ QStringLiteral("getNodeVertices/address"), QStringLiteral("Node address (must be a GeometryNode)") },

View File

@@ -31,6 +31,7 @@ static const char *kQuickWindowModel = "com.kdab.GammaRay.QuickWindowModel";
static const char *kQuickItemModel = "com.kdab.GammaRay.QuickItemModel";
static const char *kQuickSceneGraphModel = "com.kdab.GammaRay.QuickSceneGraphModel";
static const char *kBaseName = "com.kdab.GammaRay.QuickSceneGraph";
static const char *kItemBaseName = "com.kdab.GammaRay.QuickItem";
static const char *kMaterialIface = "com.kdab.GammaRay.MaterialExtensionInterface";
static const char *kQuickInspectorIface = "com.kdab.GammaRay.QuickInspectorInterface/1.0";
@@ -732,7 +733,159 @@ QString SceneGraphTools::getMaterialProperties(const QString &address) const
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
}
// --- Rendering visualization ---
// --- QML Item selection + properties ---
QString SceneGraphTools::ensureItemSelected(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_selectedItemAddress)
return {};
auto *itemModel = m_session->model(QString::fromUtf8(kQuickItemModel));
if (!itemModel)
return QString::fromUtf8(QJsonDocument(errorJson(
QStringLiteral("QuickItemModel not available"))).toJson(QJsonDocument::Compact));
const QModelIndex idx = findNodeByAddress(itemModel, address);
if (!idx.isValid())
return QString::fromUtf8(QJsonDocument(errorJson(
QStringLiteral("item %1 not found in QML item tree (still loading?)").arg(address)))
.toJson(QJsonDocument::Compact));
// Create the selection model client BEFORE selecting.
auto *selModel = m_session->selectionModel(itemModel);
// CRITICAL: Create the item properties RemoteModel BEFORE sending the selection,
// so the server is already monitoring when the property model resets/populates.
auto *propModel = m_session->model(QStringLiteral("%1.properties").arg(QString::fromUtf8(kItemBaseName)));
// Wait for the SelectionModelClient's 125ms default-selection timer to fire
// and settle before our override.
QEventLoop loop;
settle(loop, 300);
// Select the item via client-side selection model.
if (selModel && idx.isValid())
selModel->select(idx, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows | QItemSelectionModel::Current);
// Wait for the selection to be processed and property data to arrive.
settle(loop, 2000);
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); }
return {};
}
QString SceneGraphTools::selectQuickItem(const QString &address) const
{
m_selectedItemAddress.clear(); // force reselect
const QString e = ensureItemSelected(address);
if (!e.isEmpty())
return e;
// 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();
}
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 },
};
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
}
QString SceneGraphTools::getItemProperties(const QString &address) const
{
const QString e = ensureItemSelected(address);
if (!e.isEmpty())
return e;
auto *m = m_session->model(QStringLiteral("%1.properties").arg(QString::fromUtf8(kItemBaseName)));
if (!m)
return QString::fromUtf8(QJsonDocument(errorJson(
QStringLiteral("item property model not available"))).toJson(QJsonDocument::Compact));
// Item property properties model has 4 columns:
// 0 = property name, 1 = value (display string), 2 = type name, 3 = class name
waitForRows(m, 3000);
const int rows = m->rowCount();
if (rows == 0)
return QString::fromUtf8(QJsonDocument(errorJson(
QStringLiteral("no properties available (did you select a valid QML item?)")))
.toJson(QJsonDocument::Compact));
const int cols = m->columnCount();
QJsonObject out;
QStringList headers;
for (int c = 0; c < cols; ++c)
headers.append(m->headerData(c, Qt::Horizontal, Qt::DisplayRole).toString());
// 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) {
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))) {
// 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);
}
if (!children.isEmpty())
prop.insert(QStringLiteral("children"), children);
groups.insert(name, prop);
} else {
simpleProps.insert(name, prop);
}
}
out.insert(QStringLiteral("properties"), simpleProps);
if (!groups.isEmpty())
out.insert(QStringLiteral("groups"), groups);
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
}
QString SceneGraphTools::setRenderMode(const QString &mode) const
{

View File

@@ -58,12 +58,18 @@ public:
// --- SceneGraph navigation ---
Q_INVOKABLE QString listScenegraphNodes() const;
// Find a node by address (e.g. "0x56396e6d7400") in the SG tree and select it
// via the probe-side Q_INVOKABLE method. 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).
// via the selection model client path. Returns JSON with the node's type and
// which sub-models have data.
Q_INVOKABLE QString selectScenegraphNode(const QString &address) const;
// --- QML Item selection + properties ---
// Select a QQuickItem by address (from listQuickItems output).
// Triggers the item property controller, populating the item properties model.
Q_INVOKABLE QString selectQuickItem(const QString &address) const;
// Get all Q_PROPERTY values (x, y, width, height, opacity, visible, z, etc.)
// for a QML item. Selects the item first if not already selected.
Q_INVOKABLE QString getItemProperties(const QString &address) const;
// --- Geometry (requires a GeometryNode to be selected) ---
// Returns vertex data: rows = vertices, columns = attributes.
// Each cell is the display string (e.g. "0.5, 0.5, 0, 1"); headers are
@@ -91,7 +97,8 @@ public:
private:
GammaRaySession *m_session;
mutable QString m_selectedAddress; // last selected node address (for sub-model tools)
mutable QString m_selectedAddress; // last selected SG node address (for sub-model tools)
mutable QString m_selectedItemAddress; // last selected QuickItem address (for property tools)
QString ensureSession() const;
@@ -101,6 +108,9 @@ private:
// 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;
// Ensure the given QuickItem is selected (select it if not already). Returns
// empty string on success, or a JSON error string.
QString ensureItemSelected(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;
};