364 lines
12 KiB
C++
364 lines
12 KiB
C++
/*
|
|
widget_tools.cpp — Qt Widget introspection tools via GammaRay.
|
|
|
|
SPDX-FileCopyrightText: 2026 The GammaRay MCP Bridge Authors
|
|
SPDX-License-Identifier: GPL-2.0-or-later
|
|
*/
|
|
|
|
#include "widget_tools.h"
|
|
#include "gammaray_session.h"
|
|
#include "property_reader.h"
|
|
|
|
#include <endpoint.h>
|
|
#include <objectbroker.h>
|
|
|
|
#include <QAbstractItemModel>
|
|
#include <QCoreApplication>
|
|
#include <QEventLoop>
|
|
#include <QItemSelectionModel>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QTimer>
|
|
|
|
static const char *kWidgetTreeModel = "com.kdab.GammaRay.WidgetTree";
|
|
static const char *kWidgetBaseName = "com.kdab.GammaRay.WidgetInspector";
|
|
|
|
static constexpr int kWidgetFlagsRole = 261;
|
|
|
|
static QJsonObject errorJson(const QString &msg)
|
|
{
|
|
return { { QStringLiteral("error"), msg } };
|
|
}
|
|
|
|
static void settle(QEventLoop &loop, int ms)
|
|
{
|
|
QTimer::singleShot(ms, &loop, &QEventLoop::quit);
|
|
loop.exec();
|
|
}
|
|
|
|
static void primeTree(const QAbstractItemModel *m, const QModelIndex &parent, int depth)
|
|
{
|
|
if (depth <= 0)
|
|
return;
|
|
for (int r = 0; r < m->rowCount(parent); ++r) {
|
|
const QModelIndex idx = m->index(r, 0, parent);
|
|
m->data(idx, Qt::DisplayRole);
|
|
m->data(m->index(r, 1, parent), Qt::DisplayRole);
|
|
if (m->hasChildren(idx))
|
|
primeTree(m, idx, depth - 1);
|
|
}
|
|
}
|
|
|
|
static 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);
|
|
if (m->data(idx, Qt::DisplayRole).toString() == QLatin1String("Loading..."))
|
|
++count;
|
|
if (m->hasChildren(idx))
|
|
count += countLoading(m, idx, depth - 1);
|
|
}
|
|
return count;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (countLoading(m, QModelIndex(), 16) > 0) {
|
|
settle(loop, finalSettleMs);
|
|
primeTree(m, QModelIndex(), 16);
|
|
settle(loop, finalSettleMs / 2);
|
|
}
|
|
}
|
|
|
|
static QModelIndex findWidgetInTree(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 (addr == QLatin1String("Loading...")) {
|
|
settle(loop, 300);
|
|
if (m->data(idx, Qt::DisplayRole).toString() == address)
|
|
return idx;
|
|
}
|
|
if (m->hasChildren(idx)) {
|
|
m->rowCount(idx);
|
|
settle(loop, 200);
|
|
const auto found = findWidgetInTree(m, idx, address, depth - 1, loop);
|
|
if (found.isValid())
|
|
return found;
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
static QJsonArray walkWidgetChildren(const QAbstractItemModel *m, const QModelIndex &parent, int depth)
|
|
{
|
|
QJsonArray out;
|
|
if (depth <= 0)
|
|
return out;
|
|
for (int r = 0; r < m->rowCount(parent); ++r) {
|
|
const QModelIndex idx = m->index(r, 0, parent);
|
|
const QString name = m->data(idx, Qt::DisplayRole).toString();
|
|
const QModelIndex typeIdx = m->index(r, 1, parent);
|
|
const QString type = m->data(typeIdx, Qt::DisplayRole).toString();
|
|
const int flags = m->data(idx, kWidgetFlagsRole).toInt();
|
|
QJsonObject node;
|
|
node.insert(QStringLiteral("name"), name);
|
|
node.insert(QStringLiteral("type"), type);
|
|
if (flags & 1)
|
|
node.insert(QStringLiteral("invisible"), true);
|
|
if (m->hasChildren(idx))
|
|
node.insert(QStringLiteral("children"), walkWidgetChildren(m, idx, depth - 1));
|
|
out.append(node);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// --- WidgetTools ---
|
|
|
|
WidgetTools::WidgetTools(GammaRaySession *session, QObject *parent)
|
|
: QObject(parent)
|
|
, m_session(session)
|
|
{
|
|
}
|
|
|
|
QString WidgetTools::ensureSession() const
|
|
{
|
|
if (!m_session)
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
QStringLiteral("no session"))).toJson(QJsonDocument::Compact));
|
|
if (m_session->lastUrl().isValid())
|
|
m_session->ensureConnected(6000);
|
|
if (m_session->isReady())
|
|
return {};
|
|
QJsonObject err = errorJson(
|
|
m_session->lastUrl().isValid()
|
|
? QStringLiteral("not connected to probe (last error: %1; call connectProbe to retry)")
|
|
.arg(m_session->lastError().isEmpty() ? QStringLiteral("timeout") : m_session->lastError())
|
|
: QStringLiteral("no probe URL configured — call connectProbe(host, port) first"));
|
|
err.insert(QStringLiteral("state"), m_session->stateString());
|
|
return QString::fromUtf8(QJsonDocument(err).toJson(QJsonDocument::Compact));
|
|
}
|
|
|
|
void WidgetTools::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;
|
|
}
|
|
}
|
|
|
|
QModelIndex WidgetTools::findWidgetByAddress(QAbstractItemModel *m, const QString &address) const
|
|
{
|
|
if (!m || address.isEmpty())
|
|
return {};
|
|
|
|
primeAndWait(m, 6, 300, 500);
|
|
|
|
QEventLoop loop;
|
|
const auto idx = findWidgetInTree(m, QModelIndex(), address, 16, loop);
|
|
if (idx.isValid())
|
|
return idx;
|
|
|
|
for (int round = 0; round < 3; ++round) {
|
|
primeAndWait(m, 2, 300, 500);
|
|
const auto idx2 = findWidgetInTree(m, QModelIndex(), address, 16, loop);
|
|
if (idx2.isValid())
|
|
return idx2;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
QString WidgetTools::ensureWidgetSelected(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 (address == m_selectedWidgetAddress)
|
|
return {};
|
|
|
|
auto *treeModel = m_session->model(QString::fromUtf8(kWidgetTreeModel));
|
|
if (!treeModel)
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
QStringLiteral("WidgetTree model not available (no QWidget app attached?)")))
|
|
.toJson(QJsonDocument::Compact));
|
|
|
|
const QModelIndex idx = findWidgetByAddress(treeModel, address);
|
|
if (!idx.isValid())
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
QStringLiteral("widget %1 not found in widget tree (still loading?)").arg(address)))
|
|
.toJson(QJsonDocument::Compact));
|
|
|
|
auto *selModel = m_session->selectionModel(treeModel);
|
|
|
|
auto *propModel = m_session->model(QStringLiteral("%1.properties").arg(QString::fromUtf8(kWidgetBaseName)));
|
|
|
|
QEventLoop loop;
|
|
settle(loop, 300);
|
|
|
|
if (selModel && idx.isValid())
|
|
selModel->select(idx, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows | QItemSelectionModel::Current);
|
|
|
|
settle(loop, 2000);
|
|
|
|
m_selectedWidgetAddress = address;
|
|
|
|
if (propModel) {
|
|
propModel->rowCount();
|
|
settle(loop, 1500);
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
QString WidgetTools::listWidgets() const
|
|
{
|
|
if (const QString e = ensureSession(); !e.isEmpty())
|
|
return e;
|
|
|
|
auto *m = m_session->model(QString::fromUtf8(kWidgetTreeModel));
|
|
if (!m)
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
QStringLiteral("WidgetTree model not available (no QWidget app attached?)")))
|
|
.toJson(QJsonDocument::Compact));
|
|
|
|
primeAndWait(m, 6, 300, 500);
|
|
|
|
const QJsonArray tree = walkWidgetChildren(m, QModelIndex(), 16);
|
|
return QString::fromUtf8(QJsonDocument(tree).toJson(QJsonDocument::Compact));
|
|
}
|
|
|
|
QString WidgetTools::selectWidget(const QString &address) const
|
|
{
|
|
m_selectedWidgetAddress.clear();
|
|
const QString e = ensureWidgetSelected(address);
|
|
if (!e.isEmpty())
|
|
return e;
|
|
|
|
auto *treeModel = m_session->model(QString::fromUtf8(kWidgetTreeModel));
|
|
QJsonObject out;
|
|
if (!treeModel) {
|
|
out.insert(QStringLiteral("error"), QStringLiteral("WidgetTree model gone"));
|
|
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
|
|
}
|
|
const QModelIndex idx = findWidgetByAddress(treeModel, 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 = treeModel->index(idx.row(), 1, idx.parent());
|
|
const QString type = treeModel->data(typeIdx, Qt::DisplayRole).toString();
|
|
int propertyCount = 0;
|
|
auto *propModel = m_session->model(QStringLiteral("%1.properties").arg(QString::fromUtf8(kWidgetBaseName)));
|
|
if (propModel)
|
|
propertyCount = propModel->rowCount();
|
|
|
|
out.insert(QStringLiteral("selected"), address);
|
|
out.insert(QStringLiteral("type"), type);
|
|
out.insert(QStringLiteral("propertyCount"), propertyCount);
|
|
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
|
|
}
|
|
|
|
QString WidgetTools::getWidgetProperties(const QString &address) const
|
|
{
|
|
const QString e = ensureWidgetSelected(address);
|
|
if (!e.isEmpty())
|
|
return e;
|
|
|
|
auto *m = m_session->model(QStringLiteral("%1.properties").arg(QString::fromUtf8(kWidgetBaseName)));
|
|
if (!m)
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
QStringLiteral("widget property model not available"))).toJson(QJsonDocument::Compact));
|
|
|
|
QJsonObject out = readAggregatedPropertyModel(m);
|
|
if (out.isEmpty())
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
QStringLiteral("no properties available (did you select a valid widget?)")))
|
|
.toJson(QJsonDocument::Compact));
|
|
|
|
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
|
|
}
|
|
|
|
QString WidgetTools::getWidgetAttributes(const QString &address) const
|
|
{
|
|
const QString e = ensureWidgetSelected(address);
|
|
if (!e.isEmpty())
|
|
return e;
|
|
|
|
auto *m = m_session->model(QStringLiteral("%1.widgetAttributeModel").arg(QString::fromUtf8(kWidgetBaseName)));
|
|
if (!m)
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
QStringLiteral("widget attribute model not available"))).toJson(QJsonDocument::Compact));
|
|
|
|
waitForRows(m, 3000);
|
|
|
|
const int rows = m->rowCount();
|
|
if (rows == 0)
|
|
return QString::fromUtf8(QJsonDocument(errorJson(
|
|
QStringLiteral("no widget attributes (did you select a valid widget?)")))
|
|
.toJson(QJsonDocument::Compact));
|
|
|
|
QJsonObject attrs;
|
|
for (int r = 0; r < rows; ++r) {
|
|
const QString name = m->data(m->index(r, 0), Qt::DisplayRole).toString();
|
|
const bool enabled = m->data(m->index(r, 1), Qt::DisplayRole).toBool();
|
|
const QString val = m->data(m->index(r, 1), Qt::DisplayRole).toString();
|
|
attrs.insert(name, QJsonObject{
|
|
{ QStringLiteral("enabled"), enabled },
|
|
{ QStringLiteral("value"), val },
|
|
});
|
|
}
|
|
|
|
QJsonObject out;
|
|
out.insert(QStringLiteral("attributes"), attrs);
|
|
out.insert(QStringLiteral("count"), rows);
|
|
return QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Compact));
|
|
} |