mirror of
https://github.com/logos-co/logos-app.git
synced 2026-08-27 17:31:13 +00:00
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.
370 lines
19 KiB
C++
370 lines
19 KiB
C++
#include "MainUIBackend.h"
|
|
#include "AppsModel.h"
|
|
#include "CoreModuleManager.h"
|
|
#include "ModuleInstanceModel.h"
|
|
#include "UIPluginManager.h"
|
|
#include "PackageCoordinator.h"
|
|
#include "BuildInfo.h"
|
|
|
|
#include <QDebug>
|
|
#include <QTimer>
|
|
|
|
MainUIBackend::MainUIBackend(LogosAPI* logosAPI, logos::qt::QtLogosCore* core, QObject* parent)
|
|
: QObject(parent)
|
|
, m_currentActiveSectionIndex(0)
|
|
, m_logosAPI(logosAPI)
|
|
, m_core(core)
|
|
, m_ownsLogosAPI(false)
|
|
, m_coreModuleManager(nullptr)
|
|
, m_uiPluginManager(nullptr)
|
|
, m_packageCoordinator(nullptr)
|
|
, m_uiModulesModel(new ModuleInstanceModel(this))
|
|
, m_coreModulesModel(new ModuleInstanceModel(this))
|
|
{
|
|
if (!m_logosAPI) {
|
|
m_logosAPI = new LogosAPI("core", this);
|
|
m_ownsLogosAPI = true;
|
|
}
|
|
|
|
// Order matters: CoreModuleManager must exist before UIPluginManager so
|
|
// the latter's ctor can receive a valid pointer; UIPluginManager must
|
|
// exist before PackageCoordinator for the same reason. Qt tears children
|
|
// down in reverse order at destruction, so PackageCoordinator dies first
|
|
// (stops talking to the module), then UIPluginManager (tears down
|
|
// widgets while CoreModuleManager's C API handle is still valid).
|
|
|
|
|
|
m_appsModel = new AppsModel(this);
|
|
|
|
m_coreModuleManager = new CoreModuleManager(m_logosAPI, m_core, this);
|
|
m_uiPluginManager = new UIPluginManager(m_logosAPI, m_coreModuleManager, this);
|
|
m_packageCoordinator = new PackageCoordinator(m_logosAPI, m_coreModuleManager, m_uiPluginManager, m_appsModel, this);
|
|
m_appsModel->setInstallRegistry(m_packageCoordinator->installRegistry());
|
|
|
|
// Setter-injection closes the cycle — UIPluginManager queries
|
|
// PackageCoordinator for installType / missing-deps when building its
|
|
// uiModules() list, and consumes uiPluginsFetched for its UI-specific
|
|
// metadata cache. See UIPluginManager::setPackageCoordinator for the signal
|
|
// connections it sets up internally.
|
|
m_uiPluginManager->setPackageCoordinator(m_packageCoordinator);
|
|
|
|
// Forward manager signals into our own signals of the same name. QML
|
|
// binds to these; by funneling through the facade we keep a stable
|
|
// surface regardless of which manager emitted them.
|
|
//
|
|
// UIPluginManager drives uiModulesChanged/launcherAppsChanged on load/
|
|
// unload events; PackageCoordinator's own uiModulesChanged/launcherAppsChanged
|
|
// already flow through UIPluginManager (wired in setPackageCoordinator) so
|
|
// we only need to listen to UIPluginManager here. The refresh slot
|
|
// repopulates m_uiModulesModel in one shot; QML consumers listen to the
|
|
// model's own dataChanged/modelReset signals rather than any signal here.
|
|
connect(m_uiPluginManager, &UIPluginManager::uiModulesChanged,
|
|
this, &MainUIBackend::refreshUiModulesModel);
|
|
// Clear the Modules Reload overlay once the user-initiated UI refresh
|
|
// lands its first uiModulesChanged (list is already updated by then).
|
|
connect(m_uiPluginManager, &UIPluginManager::uiModulesChanged,
|
|
this, [this]() {
|
|
if (!m_pendingUiModulesRefresh) return;
|
|
m_pendingUiModulesRefresh = false;
|
|
endModulesLoading();
|
|
});
|
|
connect(m_uiPluginManager, &UIPluginManager::launcherAppsChanged,
|
|
this, &MainUIBackend::launcherAppsChanged);
|
|
connect(m_uiPluginManager, &UIPluginManager::loadingModulesChanged,
|
|
this, &MainUIBackend::loadingModulesChanged);
|
|
connect(m_uiPluginManager, &UIPluginManager::currentVisibleAppChanged,
|
|
this, &MainUIBackend::currentVisibleAppChanged);
|
|
connect(m_uiPluginManager, &UIPluginManager::navigateToApps,
|
|
this, &MainUIBackend::navigateToApps);
|
|
connect(m_uiPluginManager, &UIPluginManager::navigateToRepositoriesRequested,
|
|
this, &MainUIBackend::navigateToRepositoriesRequested);
|
|
connect(m_uiPluginManager, &UIPluginManager::packageInstallFailedNotice,
|
|
this, &MainUIBackend::installFailureNoticeRequested);
|
|
connect(m_uiPluginManager, &UIPluginManager::missingDepsPopupRequested,
|
|
this, &MainUIBackend::missingDepsPopupRequested);
|
|
connect(m_uiPluginManager, &UIPluginManager::unloadCascadeConfirmationRequested,
|
|
this, &MainUIBackend::unloadCascadeConfirmationRequested);
|
|
connect(m_uiPluginManager, &UIPluginManager::pluginWindowRequested,
|
|
this, &MainUIBackend::pluginWindowRequested);
|
|
connect(m_uiPluginManager, &UIPluginManager::pluginWindowRemoveRequested,
|
|
this, &MainUIBackend::pluginWindowRemoveRequested);
|
|
connect(m_uiPluginManager, &UIPluginManager::pluginWindowActivateRequested,
|
|
this, &MainUIBackend::pluginWindowActivateRequested);
|
|
|
|
// Forward PackageCoordinator dialog signals. One uninstallPlanRequested
|
|
// serves all four initiators — see buildPlanPayload.
|
|
connect(m_packageCoordinator, &PackageCoordinator::uninstallPlanRequested,
|
|
this, &MainUIBackend::uninstallPlanRequested);
|
|
connect(m_packageCoordinator, &PackageCoordinator::dependencyDataReadyChanged,
|
|
this, &MainUIBackend::dependencyDataReadyChanged);
|
|
// Distinct upgrade/downgrade/reinstall cascade signal — the dialog
|
|
// shape is the same as the uninstall variant, but the title + body
|
|
// need the target releaseTag + UpgradeMode (so a downgrade doesn't
|
|
// look like a bare uninstall). PackageCoordinator emits this from
|
|
// onBeforeUpgrade; OverlayDialogs.qml renders it via the
|
|
// "upgradeCascade" mode of ConfirmationDialog.
|
|
connect(m_packageCoordinator, &PackageCoordinator::upgradeCascadeConfirmationRequested,
|
|
this, &MainUIBackend::upgradeCascadeConfirmationRequested);
|
|
connect(m_packageCoordinator, &PackageCoordinator::installGateConfirmationRequested,
|
|
this, &MainUIBackend::installGateConfirmationRequested);
|
|
connect(m_packageCoordinator, &PackageCoordinator::requestOpenAddApplicationDialog,
|
|
this, &MainUIBackend::requestOpenAddApplicationDialog);
|
|
connect(m_packageCoordinator, &PackageCoordinator::addApplicationDataUpdated,
|
|
this, &MainUIBackend::addApplicationDataUpdated);
|
|
connect(m_packageCoordinator, &PackageCoordinator::launchAppRequested,
|
|
this, &MainUIBackend::launchAppRequested);
|
|
// The resolver's required packages are cached here and published as a
|
|
// property; QML binds a shell-declared AppsFilterProxy to it. Nothing on
|
|
// this side holds a pointer to that proxy.
|
|
connect(m_packageCoordinator, &PackageCoordinator::requiredPackagesResolved,
|
|
this, [this](const QVariantList& entries) {
|
|
if (m_requiredPackages == entries) return;
|
|
m_requiredPackages = entries;
|
|
emit requiredPackagesChanged();
|
|
});
|
|
|
|
// Widened to int at this hop rather than connected signal-to-signal, so
|
|
// the conversion is written down instead of relying on the implicit
|
|
// enum-to-int the connect check would otherwise permit silently.
|
|
connect(m_packageCoordinator, &PackageCoordinator::catalogInstallStageChanged,
|
|
this, [this](const QString& name, InstallStage::Value stage) {
|
|
emit catalogInstallStageChanged(name, static_cast<int>(stage));
|
|
});
|
|
connect(m_packageCoordinator, &PackageCoordinator::catalogInstallFinished,
|
|
this, &MainUIBackend::catalogInstallFinished);
|
|
connect(m_packageCoordinator, &PackageCoordinator::catalogInstallFailed,
|
|
this, &MainUIBackend::catalogInstallFailed);
|
|
|
|
// Package repositories — pure re-emits so QML binding to backend.*
|
|
connect(m_packageCoordinator, &PackageCoordinator::repositoriesChanged,
|
|
this, &MainUIBackend::repositoriesChanged);
|
|
connect(m_packageCoordinator, &PackageCoordinator::repositoriesLoadingChanged,
|
|
this, &MainUIBackend::repositoriesLoadingChanged);
|
|
connect(m_packageCoordinator, &PackageCoordinator::appsLoadingChanged,
|
|
this, &MainUIBackend::appsLoadingChanged);
|
|
connect(m_packageCoordinator, &PackageCoordinator::repositoryOperationCompleted,
|
|
this, &MainUIBackend::repositoryOperationCompleted);
|
|
|
|
// Any of the three managers can trigger coreModulesChanged:
|
|
// * CoreModuleManager on stats-tick / refresh
|
|
// * UIPluginManager on cascade-induced state changes (re-emits
|
|
// PackageCoordinator's coreModulesChanged as part of that wiring)
|
|
// Both fan into the same refresh slot. Qt coalesces redundant dataChanged
|
|
// notifies within a frame, so the two-connect layout doesn't cause
|
|
// visible flicker.
|
|
connect(m_uiPluginManager, &UIPluginManager::coreModulesChanged,
|
|
this, &MainUIBackend::refreshCoreModulesModel);
|
|
connect(m_coreModuleManager, &CoreModuleManager::coreModulesChanged,
|
|
this, &MainUIBackend::refreshCoreModulesModel);
|
|
|
|
// Kick the first catalog scan now that all wiring is in place. We do
|
|
// this AFTER setPackageCoordinator (and its signal connections) so the
|
|
// resulting uiPluginsFetched / uiModulesChanged land on live slots.
|
|
QTimer::singleShot(0, this, [this]() {
|
|
m_packageCoordinator->refresh();
|
|
});
|
|
|
|
qDebug() << "MainUIBackend created";
|
|
}
|
|
|
|
MainUIBackend::~MainUIBackend() = default;
|
|
|
|
QAbstractItemModel* MainUIBackend::appsModel() const
|
|
{
|
|
return m_appsModel;
|
|
}
|
|
|
|
void MainUIBackend::beginShutdown()
|
|
{
|
|
if (m_uiPluginManager) {
|
|
m_uiPluginManager->shutdown();
|
|
}
|
|
}
|
|
|
|
int MainUIBackend::currentActiveSectionIndex() const
|
|
{
|
|
return m_currentActiveSectionIndex;
|
|
}
|
|
|
|
void MainUIBackend::setCurrentActiveSectionIndex(int index)
|
|
{
|
|
// Section list is owned by QML (SidebarPanel). The upper bound is
|
|
// self-policed there; we only guard against negative indices.
|
|
// Per-section side effects (e.g., the Modules-view auto-refresh) live
|
|
// in the QML view that becomes visible, not here.
|
|
if (m_currentActiveSectionIndex != index && index >= 0) {
|
|
m_currentActiveSectionIndex = index;
|
|
emit currentActiveSectionIndexChanged();
|
|
}
|
|
}
|
|
|
|
// --- coreModules() composer ------------------------------------------------
|
|
//
|
|
// coreModules is the one QML-visible property that spans multiple managers.
|
|
// Known + loaded + stats come from CoreModuleManager (raw liblogos state);
|
|
// installType comes from PackageCoordinator (populated during its dep-info
|
|
// refresh). We compose here so neither manager has to know about the other's
|
|
// schema.
|
|
QVariantList MainUIBackend::buildCoreModulesSnapshot() const
|
|
{
|
|
QVariantList modules;
|
|
if (!m_coreModuleManager) return modules;
|
|
|
|
const QStringList known = m_coreModuleManager->knownModules();
|
|
const QStringList loaded = m_coreModuleManager->loadedModules();
|
|
|
|
for (const QString& name : known) {
|
|
QVariantMap module;
|
|
module["name"] = name;
|
|
module["displayName"] = m_packageCoordinator ? m_packageCoordinator->displayNameFor(name) : name;
|
|
module["isLoaded"] = loaded.contains(name);
|
|
// installType populated lazily by refreshDependencyInfo's full-scan
|
|
// pass on PackageCoordinator. Empty means "not known yet" — QML treats
|
|
// that as a non-user module and hides Uninstall, which is the safe
|
|
// default.
|
|
module["installType"] = m_packageCoordinator ? m_packageCoordinator->installType(name) : QString();
|
|
|
|
const QVariantMap stats = m_coreModuleManager->moduleStats(name);
|
|
if (!stats.isEmpty()) {
|
|
module["cpu"] = stats["cpu"];
|
|
module["memory"] = stats["memory"];
|
|
} else {
|
|
module["cpu"] = "0.0";
|
|
module["memory"] = "0.0";
|
|
}
|
|
|
|
modules.append(module);
|
|
}
|
|
|
|
return modules;
|
|
}
|
|
|
|
// --- Manager delegations ---------------------------------------------------
|
|
//
|
|
// Each slot is a one-liner routing to the right manager. These stay on
|
|
// MainUIBackend so the QML `backend.foo(...)` contract is untouched by the
|
|
// refactor (QML still sees one receiver).
|
|
|
|
QVariantList MainUIBackend::buildUiModulesSnapshot() const { return m_uiPluginManager->uiModules(); }
|
|
|
|
// Refresh slots — fold "compute snapshot + push into model" into one call
|
|
// so the ctor can wire signal→slot directly rather than routing through a
|
|
// lambda for each source of change.
|
|
void MainUIBackend::refreshUiModulesModel()
|
|
{
|
|
m_uiModulesModel->replaceRows(buildUiModulesSnapshot());
|
|
}
|
|
|
|
void MainUIBackend::refreshCoreModulesModel()
|
|
{
|
|
m_coreModulesModel->replaceRows(buildCoreModulesSnapshot());
|
|
}
|
|
|
|
QVariantList MainUIBackend::launcherApps() const { return m_uiPluginManager->launcherApps(); }
|
|
QString MainUIBackend::currentVisibleApp() const{ return m_uiPluginManager->currentVisibleApp(); }
|
|
QStringList MainUIBackend::loadingModules() const { return m_uiPluginManager->loadingModules(); }
|
|
|
|
// UIPluginManager — UI plugin widget lifecycle + local unload cascade.
|
|
void MainUIBackend::loadUiModule(const QString& n) { m_uiPluginManager->loadUiModule(n); }
|
|
void MainUIBackend::unloadUiModule(const QString& n) { m_uiPluginManager->unloadUiModule(n); }
|
|
void MainUIBackend::activateApp(const QString& n) { m_uiPluginManager->activateApp(n); }
|
|
void MainUIBackend::confirmUnloadCascade(const QString& n) { m_uiPluginManager->confirmUnloadCascade(n); }
|
|
void MainUIBackend::loadCoreModule(const QString& n) { m_uiPluginManager->loadCoreModule(n); }
|
|
void MainUIBackend::unloadCoreModule(const QString& n) { m_uiPluginManager->unloadCoreModule(n); }
|
|
void MainUIBackend::refreshUiModules()
|
|
{
|
|
if (!m_pendingUiModulesRefresh) {
|
|
beginModulesLoading();
|
|
m_pendingUiModulesRefresh = true;
|
|
}
|
|
m_uiPluginManager->refreshUiModules();
|
|
}
|
|
void MainUIBackend::onAppLauncherClicked(const QString& n) { m_uiPluginManager->onAppLauncherClicked(n); }
|
|
void MainUIBackend::setCurrentVisibleApp(const QString& n) { m_uiPluginManager->setCurrentVisibleApp(n); }
|
|
|
|
// PackageCoordinator — package_manager IPC and package-lifecycle cascade.
|
|
QString MainUIBackend::displayNameFor(const QString& n) const {
|
|
return m_packageCoordinator ? m_packageCoordinator->displayNameFor(n) : n;
|
|
}
|
|
void MainUIBackend::uninstallUiModule(const QString& n) { m_packageCoordinator->uninstallUiModule(n); }
|
|
void MainUIBackend::uninstallApp(const QString& n, const QString& repositoryUrl)
|
|
{ m_packageCoordinator->uninstallApp(n, repositoryUrl); }
|
|
void MainUIBackend::uninstallCoreModule(const QString& n) { m_packageCoordinator->uninstallCoreModule(n); }
|
|
void MainUIBackend::confirmUninstallCascade(const QString& n) { m_packageCoordinator->confirmUninstallCascade(n); }
|
|
void MainUIBackend::confirmUninstallMultiCascade(const QStringList& names) { m_packageCoordinator->confirmUninstallMultiCascade(names); }
|
|
void MainUIBackend::cancelMultiUninstall(const QStringList& names) { m_packageCoordinator->cancelMultiUninstall(names); }
|
|
void MainUIBackend::cancelPendingUninstallApp(const QString& name) { m_packageCoordinator->cancelPendingUninstallApp(name); }
|
|
void MainUIBackend::confirmInstallGate(const QString& n) { m_packageCoordinator->confirmInstallGate(n); }
|
|
void MainUIBackend::cancelInstallGate(const QString& n) { m_packageCoordinator->cancelInstallGate(n); }
|
|
void MainUIBackend::openApp(const QString& name, const QString& repositoryUrl, const QVariantMap& versionPins, bool allowFastLaunch)
|
|
{ m_packageCoordinator->openApp(name, repositoryUrl, versionPins, allowFastLaunch); }
|
|
void MainUIBackend::confirmCatalogInstall(const QString& name, const QString& repositoryUrl, const QVariantMap& versionPins)
|
|
{ m_packageCoordinator->confirmCatalogInstall(name, repositoryUrl, versionPins); }
|
|
void MainUIBackend::notifyAddApplicationDialogClosed()
|
|
{ m_packageCoordinator->notifyAddApplicationDialogClosed(); }
|
|
|
|
// cancelPendingAction is the one slot that doesn't route to a single manager:
|
|
// a pending action lives on either UIPluginManager (local unload cascade) or
|
|
// PackageCoordinator (uninstall/upgrade cascade) but not both. Fan out to both —
|
|
// the un-involved manager no-ops on name-mismatch. This preserves the QML
|
|
// contract (single `backend.cancelPendingAction(name)` call for either dialog).
|
|
void MainUIBackend::cancelPendingAction(const QString& n) {
|
|
m_uiPluginManager->cancelUnloadCascade(n);
|
|
m_packageCoordinator->cancelPendingAction(n);
|
|
}
|
|
|
|
// Package repositories — delegations + cache pass-through.
|
|
QVariantList MainUIBackend::repositories() const { return m_packageCoordinator->repositories(); }
|
|
bool MainUIBackend::repositoriesLoading() const { return m_packageCoordinator->repositoriesLoading(); }
|
|
bool MainUIBackend::appsLoading() const
|
|
{ return !m_packageCoordinator || m_packageCoordinator->appsLoading(); }
|
|
|
|
bool MainUIBackend::dependencyDataReady() const
|
|
{ return m_packageCoordinator && m_packageCoordinator->dependencyDataReady(); }
|
|
|
|
bool MainUIBackend::modulesLoading() const
|
|
{ return m_modulesLoadingCount > 0; }
|
|
|
|
void MainUIBackend::beginModulesLoading()
|
|
{
|
|
if (++m_modulesLoadingCount == 1)
|
|
emit modulesLoadingChanged();
|
|
}
|
|
|
|
void MainUIBackend::endModulesLoading()
|
|
{
|
|
if (m_modulesLoadingCount <= 0) return;
|
|
if (--m_modulesLoadingCount == 0)
|
|
emit modulesLoadingChanged();
|
|
}
|
|
|
|
void MainUIBackend::refreshRepositories() { m_packageCoordinator->refreshRepositories(); }
|
|
void MainUIBackend::refreshAppCatalog() { m_packageCoordinator->remoteRefresh(); }
|
|
void MainUIBackend::addRepository(const QString& url) { m_packageCoordinator->addRepository(url); }
|
|
void MainUIBackend::removeRepository(const QString& url) { m_packageCoordinator->removeRepository(url); }
|
|
void MainUIBackend::setRepositoryEnabled(const QString& url, bool enabled) { m_packageCoordinator->setRepositoryEnabled(url, enabled); }
|
|
|
|
// --- CoreModuleManager delegations ----------------------------------------
|
|
|
|
void MainUIBackend::refreshCoreModules()
|
|
{
|
|
beginModulesLoading();
|
|
m_coreModuleManager->refresh();
|
|
QTimer::singleShot(0, this, [this]() { endModulesLoading(); });
|
|
}
|
|
QString MainUIBackend::getCoreModuleMethods(const QString& n) { return m_coreModuleManager->getMethods(n); }
|
|
QString MainUIBackend::getCoreModuleEvents(const QString& n) { return m_coreModuleManager->getEvents(n); }
|
|
QString MainUIBackend::callCoreModuleMethod(const QString& n,
|
|
const QString& m,
|
|
const QString& a) { return m_coreModuleManager->callMethod(n, m, a); }
|
|
|
|
// --- Build info -----------------------------------------------------------
|
|
//
|
|
// Thin QML-facing wrappers over the shared LogosBasecampBuildInfo helper
|
|
// (app/utils/BuildInfo.h), which reads the nix-generated logos_build_info.h.
|
|
|
|
QString MainUIBackend::buildVersion() const { return LogosBasecampBuildInfo::version(); }
|
|
bool MainUIBackend::isPortableBuild() const { return LogosBasecampBuildInfo::isPortableBuild(); }
|
|
QVariantList MainUIBackend::buildCommits() const { return LogosBasecampBuildInfo::commits(); }
|
|
|