Files
logos-view-module-runtime/include/LogosQmlBridge.h

157 lines
7.0 KiB
C++

#pragma once
#include <QObject>
#include <QString>
#include <QVariant>
#include <QVariantList>
#include <QJSValue>
#include <QMap>
class LogosAPI;
class QRemoteObjectNode;
class QAbstractItemModelReplica;
class LogosViewReplicaFactory;
class QPluginLoader;
// ── LogosQmlBridge ───────────────────────────────────────────────────────────
//
// Single entry point for QML to talk to modules.
//
// Non-view (backend) modules — go through LogosAPI IPC:
// logos.callModule(name, method, [args]) // sync, returns JSON
// logos.callModuleAsync(name, method, [args], cb, ms) // async, JSON payload
//
// View modules — each view module ships a typed replica factory plugin
// (generated by logos_module() from a .rep file). The host loads it and
// exposes the typed replica directly to QML, so this API is dead simple:
//
// property var backend: logos.module("my_view_module")
// Text { text: backend.someProperty }
// Button { onClicked: backend.someSlot() }
// Connections { target: backend; function onSomeSignal() { ... } }
//
// // Enums declared in the .rep are available under the QML URI the
// // factory registered (typically "Logos.<ModuleName>"):
// import Logos.MyViewModule 1.0
// Text { text: backend.status === MyViewModule.Active ? "on" : "off" }
//
// // QAbstractItemModel* Q_PROPERTYs are still remoted separately:
// ListView { model: logos.model("my_view_module", "items") }
//
class LogosQmlBridge : public QObject {
Q_OBJECT
public:
explicit LogosQmlBridge(LogosAPI* api, QObject* parent = nullptr);
// ── Backend (non-view) module calls via LogosAPI IPC ────────────────
Q_INVOKABLE QString callModule(const QString& module,
const QString& method,
const QVariantList& args = QVariantList());
// timeoutMs: if > 0, the callback is invoked with an error payload when
// no reply arrives within that many milliseconds. Default 30000 (30s);
// pass 0 to disable.
Q_INVOKABLE void callModuleAsync(const QString& module,
const QString& method,
const QVariantList& args,
QJSValue callback,
int timeoutMs = 30000);
// ── View module API ─────────────────────────────────────────────────
// Returns the typed replica for a view module. Safe to call before the
// replica is Valid — QML will see default-constructed values until then
// and property NOTIFYs fire once the source meta arrives.
Q_INVOKABLE QObject* module(const QString& moduleName);
// Returns a QAbstractItemModelReplica* for a QAbstractItemModel*
// Q_PROPERTY on the view module. ui-host auto-remotes every such
// property as "<module>/<propertyName>".
//
// prefetch=true acquires with QtRemoteObjects::PrefetchData, so the
// replica caches all roles for the initial rowCount before it reports
// populated — kills the "row exists but roles are still null" flash
// that FetchRootSize (the default) shows. Only safe for bounded /
// paged models; a raw source of 100k rows would fetch 100k *
// roles up front.
Q_INVOKABLE QObject* model(const QString& moduleName,
const QString& modelName,
bool prefetch = false);
// True once the view module's replica is Valid (source meta received).
Q_INVOKABLE bool isViewModuleReady(const QString& moduleName) const;
// Watch a QRemoteObjectPendingCall returned by a replica slot call and
// invoke callbacks with the result. Replaces QtRemoteObjects.watch() so
// QML plugins don't need to import QtRemoteObjects (keeping the sandbox
// locked down).
//
// Usage:
// logos.watch(backend.add(1, 2),
// function(value) { display.text = value },
// function(error) { console.log(error) })
Q_INVOKABLE void watch(const QVariant& pendingCall,
QJSValue onSuccess,
QJSValue onError = QJSValue());
// Called by the host application during setup.
void setViewModuleSocket(const QString& moduleName, const QString& socketName);
void setViewReplicaPlugin(const QString& moduleName, const QString& pluginPath);
// Called when the view module's child process exits unexpectedly.
void notifyViewModuleCrashed(const QString& moduleName);
// ── Test-friendly helpers ───────────────────────────────────────────
//
// Stateless serializer for a QVariant result into the JSON string
// shape QML consumers expect. Exposed as a public static method so
// unit tests (and callers who want the same format without going
// through callModule()) can exercise it directly.
static QString serializeResultForTesting(const QVariant& result);
// Introspection helpers used by unit tests to assert routing state
// without needing a real QRemoteObjectNode.
bool hasViewModuleSocket(const QString& moduleName) const;
QString viewModuleSocket(const QString& moduleName) const;
QString viewReplicaPluginPath(const QString& moduleName) const;
// Subscribe to events emitted by core (non-view) modules. When the
// module emits an event, the moduleEventReceived signal fires.
//
// Usage:
// Component.onCompleted: logos.onModuleEvent("calc_module", "resultReady")
// Connections {
// target: logos
// function onModuleEventReceived(moduleName, eventName, data) {
// if (eventName === "resultReady")
// console.log("Got result:", JSON.stringify(data))
// }
// }
//
// Returns true if the subscription succeeded.
Q_INVOKABLE bool onModuleEvent(const QString& moduleName, const QString& eventName);
signals:
void viewModuleReadyChanged(const QString& moduleName, bool ready);
void viewModuleCrashed(const QString& moduleName);
void moduleEventReceived(const QString& moduleName,
const QString& eventName,
const QVariantList& data);
private:
void dropViewModuleCaches(const QString& moduleName);
QRemoteObjectNode* getOrCreateNode(const QString& moduleName);
LogosViewReplicaFactory* loadFactory(const QString& moduleName);
LogosAPI* m_logosAPI;
// Per-view-module state
QMap<QString, QString> m_viewModuleSockets;
QMap<QString, QString> m_replicaPluginPaths;
QMap<QString, QRemoteObjectNode*> m_replicaNodes;
QMap<QString, QPluginLoader*> m_factoryLoaders;
QMap<QString, LogosViewReplicaFactory*> m_factories;
QMap<QString, QObject*> m_replicas;
QMap<QString, QAbstractItemModelReplica*> m_modelReplicas;
};