Files
logos-app/src/MainShellView.h
T
Dario Gabriel Lipicar 9b8cf6e47e feat(shell): ship main_ui as a plugin that links no logos runtime
Measured on the built artefact: main_ui.dylib DEFINES zero and IMPORTS zero of
TokenManager, StoreRegistry, LogosAPI, LogosAPIClient and logos_core_*, out of
3410 symbols read. It links Qt and nothing else from this workspace, which
nix/symbol-gate.nix enforces across the in-process image set.

Getting there needed the last non-Qt types off the boundary: the model
properties cross as QAbstractItemModel*, catalogInstallStageChanged carries an
int rather than InstallStage::Value, and the two prebuilt AppsFilterProxy
instances are declared in QML instead of owned by MainUIBackend. That last one
removes a real inversion -- PackageCoordinator called setRequiredPackages() on
a proxy the host held a pointer to; it now emits requiredPackagesResolved() and
QML binds to the republished property.

Window resolves the plugin, qobject_casts it to IShellView, checks
hostAbiVersion() against IShellHost_abi, and calls createShell(IShellHost*).
No error-label fallback widget: that degraded to something that looked like a
working app with an empty window.

Filter proxies read role constants off host-side models and InstallEnums is
used by nine host files, so app/interfaces/ gains the contract headers both
sides compile against -- the models inherit the role structs, leaving every
AppsModel::NameRole call site unchanged. The plugin's include path is
app/interfaces only, so including a host header does not compile.

Four things only running it finds:
  * Logos::DesignSystem may be linked by exactly ONE image -- both linked it
    and the app aborted with "Cannot add multiple registrations for
    Logos.Icons"; QML module registration is process-global
  * qmltyperegistrar emits no #include for a SOURCES header given as an
    absolute path outside the project
  * each qt_add_qml_module is its own target and inherits no include dirs
  * AUTOMOC pairs header<->cpp by same-basename-same-DIRECTORY, which the
    split breaks

tst_AppManagerView.qml grows five tests for the QML binding, checked with a
negative control: breaking one assertion fails qml-tests, so they run.
2026-08-22 16:42:13 -03:00

40 lines
1.8 KiB
C++

#pragma once
#include "IShellView.h"
#include <QObject>
#include <QPointer>
class MainContainer;
// ─────────────────────────────────────────────────────────────────────────────
// MainShellView — the IShellView implementation for Basecamp's own UI shell.
//
// Q_INTERFACES + Q_PLUGIN_METADATA: QPluginLoader instantiates this class and
// Window reaches it with `qobject_cast<IShellView*>`. A real cast, not
// QMetaObject::invokeMethod by name — a signature change that way still
// compiles on both sides and misses only at runtime.
//
// Holds no state beyond the shell it built; the rest arrives via IShellHost*.
// ─────────────────────────────────────────────────────────────────────────────
class MainShellView : public QObject, public IShellView {
Q_OBJECT
Q_INTERFACES(IShellView)
Q_PLUGIN_METADATA(IID IShellView_iid FILE "metadata.json")
public:
explicit MainShellView(QObject* parent = nullptr);
~MainShellView() override;
// ── IShellView ──────────────────────────────────────────────────────────
QWidget* createShell(IShellHost* host) override;
void destroyShell(QWidget* widget) override;
int hostAbiVersion() const override;
private:
// QPointer auto-nulls when the widget is destroyed by its Qt parent (Window
// owns it as the central widget), so a later destroyShell() is a no-op
// rather than a double delete.
QPointer<MainContainer> m_shell;
};