2025-12-10 10:57:16 -05:00
|
|
|
#include "MainUIBackend.h"
|
2026-06-08 14:49:33 +02:00
|
|
|
#include "AppsFilterProxy.h"
|
|
|
|
|
#include "AppsModel.h"
|
2026-04-17 01:16:38 -03:00
|
|
|
#include "CoreModuleManager.h"
|
|
|
|
|
#include "UIPluginManager.h"
|
|
|
|
|
#include "PackageCoordinator.h"
|
2026-04-22 08:47:28 -03:00
|
|
|
#include "BuildInfo.h"
|
2026-04-17 01:16:38 -03:00
|
|
|
|
2025-12-10 10:57:16 -05:00
|
|
|
#include <QDebug>
|
2026-06-08 14:49:33 +02:00
|
|
|
#include <QTimer>
|
2025-12-10 10:57:16 -05:00
|
|
|
|
|
|
|
|
MainUIBackend::MainUIBackend(LogosAPI* logosAPI, QObject* parent)
|
|
|
|
|
: QObject(parent)
|
2026-02-02 18:05:41 +05:30
|
|
|
, m_currentActiveSectionIndex(0)
|
2025-12-10 10:57:16 -05:00
|
|
|
, m_logosAPI(logosAPI)
|
|
|
|
|
, m_ownsLogosAPI(false)
|
2026-04-17 01:16:38 -03:00
|
|
|
, m_coreModuleManager(nullptr)
|
|
|
|
|
, m_uiPluginManager(nullptr)
|
|
|
|
|
, m_packageCoordinator(nullptr)
|
2025-12-10 10:57:16 -05:00
|
|
|
{
|
|
|
|
|
if (!m_logosAPI) {
|
|
|
|
|
m_logosAPI = new LogosAPI("core", this);
|
|
|
|
|
m_ownsLogosAPI = true;
|
|
|
|
|
}
|
2026-04-06 11:00:30 -03:00
|
|
|
|
2026-04-17 01:16:38 -03:00
|
|
|
// 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).
|
2026-06-08 14:49:33 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
m_appsModel = new AppsModel(this);
|
|
|
|
|
|
|
|
|
|
m_uiAppsProxy = new AppsFilterProxy(this);
|
|
|
|
|
m_uiAppsProxy->setSourceModel(m_appsModel);
|
|
|
|
|
m_uiAppsProxy->setTypeFilter(QStringLiteral("ui_qml"));
|
|
|
|
|
m_uiAppsProxy->setExcludeMainUi(true);
|
|
|
|
|
|
|
|
|
|
m_requiredPackagesModel = new AppsFilterProxy(this);
|
|
|
|
|
m_requiredPackagesModel->setSourceModel(m_appsModel);
|
|
|
|
|
m_requiredPackagesModel->setExcludeMainUi(false);
|
|
|
|
|
m_requiredPackagesModel->setInstallStateFilter(QString());
|
|
|
|
|
|
2026-04-17 01:16:38 -03:00
|
|
|
m_coreModuleManager = new CoreModuleManager(m_logosAPI, this);
|
|
|
|
|
m_uiPluginManager = new UIPluginManager(m_logosAPI, m_coreModuleManager, this);
|
2026-06-08 14:49:33 +02:00
|
|
|
m_packageCoordinator = new PackageCoordinator(m_logosAPI, m_coreModuleManager, m_uiPluginManager, m_appsModel, this);
|
|
|
|
|
m_packageCoordinator->setRequiredPackagesModel(m_requiredPackagesModel);
|
2026-04-17 01:16:38 -03:00
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
connect(m_uiPluginManager, &UIPluginManager::uiModulesChanged,
|
|
|
|
|
this, &MainUIBackend::uiModulesChanged);
|
|
|
|
|
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::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);
|
|
|
|
|
|
|
|
|
|
// PackageCoordinator emits its own dialog-request signals; forward both.
|
|
|
|
|
connect(m_packageCoordinator, &PackageCoordinator::installConfirmationRequested,
|
|
|
|
|
this, &MainUIBackend::installConfirmationRequested);
|
|
|
|
|
connect(m_packageCoordinator, &PackageCoordinator::uninstallCascadeConfirmationRequested,
|
|
|
|
|
this, &MainUIBackend::uninstallCascadeConfirmationRequested);
|
2026-06-02 17:29:15 -03:00
|
|
|
// 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);
|
2026-05-06 00:31:21 +05:30
|
|
|
connect(m_packageCoordinator, &PackageCoordinator::uninstallMultiCascadeConfirmationRequested,
|
|
|
|
|
this, &MainUIBackend::uninstallMultiCascadeConfirmationRequested);
|
2026-06-08 14:49:33 +02:00
|
|
|
connect(m_packageCoordinator, &PackageCoordinator::addApplicationRequested,
|
|
|
|
|
this, &MainUIBackend::addApplicationRequested);
|
|
|
|
|
connect(m_packageCoordinator, &PackageCoordinator::launchAppRequested,
|
|
|
|
|
this, &MainUIBackend::launchAppRequested);
|
|
|
|
|
connect(m_packageCoordinator, &PackageCoordinator::catalogInstallStageChanged,
|
|
|
|
|
this, &MainUIBackend::catalogInstallStageChanged);
|
|
|
|
|
connect(m_packageCoordinator, &PackageCoordinator::catalogInstallFinished,
|
|
|
|
|
this, &MainUIBackend::catalogInstallFinished);
|
|
|
|
|
connect(m_packageCoordinator, &PackageCoordinator::catalogInstallFailed,
|
|
|
|
|
this, &MainUIBackend::catalogInstallFailed);
|
2026-04-17 01:16:38 -03:00
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
// Qt coalesces redundant property-change notifies within a frame so the
|
|
|
|
|
// multi-connect doesn't cause visible flicker.
|
|
|
|
|
connect(m_uiPluginManager, &UIPluginManager::coreModulesChanged,
|
|
|
|
|
this, &MainUIBackend::coreModulesChanged);
|
|
|
|
|
connect(m_coreModuleManager, &CoreModuleManager::coreModulesChanged,
|
|
|
|
|
this, &MainUIBackend::coreModulesChanged);
|
|
|
|
|
|
|
|
|
|
// 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.
|
2026-06-08 14:49:33 +02:00
|
|
|
QTimer::singleShot(0, this, [this]() {
|
|
|
|
|
m_packageCoordinator->refresh();
|
|
|
|
|
});
|
2026-03-27 10:57:53 -03:00
|
|
|
|
2025-12-10 10:57:16 -05:00
|
|
|
qDebug() << "MainUIBackend created";
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 01:16:38 -03:00
|
|
|
MainUIBackend::~MainUIBackend() = default;
|
2026-01-13 16:42:24 -05:00
|
|
|
|
2026-02-02 18:05:41 +05:30
|
|
|
int MainUIBackend::currentActiveSectionIndex() const
|
2025-12-10 10:57:16 -05:00
|
|
|
{
|
2026-02-02 18:05:41 +05:30
|
|
|
return m_currentActiveSectionIndex;
|
2025-12-10 10:57:16 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 18:05:41 +05:30
|
|
|
void MainUIBackend::setCurrentActiveSectionIndex(int index)
|
2025-12-10 10:57:16 -05:00
|
|
|
{
|
2026-06-08 17:20:07 +02:00
|
|
|
// 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) {
|
2026-02-02 18:05:41 +05:30
|
|
|
m_currentActiveSectionIndex = index;
|
|
|
|
|
emit currentActiveSectionIndexChanged();
|
2025-12-10 10:57:16 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 01:16:38 -03:00
|
|
|
// --- 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.
|
2025-12-10 10:57:16 -05:00
|
|
|
QVariantList MainUIBackend::coreModules() const
|
|
|
|
|
{
|
|
|
|
|
QVariantList modules;
|
2026-04-17 01:16:38 -03:00
|
|
|
if (!m_coreModuleManager) return modules;
|
2026-03-18 10:53:38 -04:00
|
|
|
|
2026-04-24 11:00:06 -03:00
|
|
|
const QStringList known = m_coreModuleManager->knownModules();
|
|
|
|
|
const QStringList loaded = m_coreModuleManager->loadedModules();
|
2026-03-18 10:53:38 -04:00
|
|
|
|
2026-04-17 01:16:38 -03:00
|
|
|
for (const QString& name : known) {
|
2025-12-10 10:57:16 -05:00
|
|
|
QVariantMap module;
|
|
|
|
|
module["name"] = name;
|
2026-04-17 01:16:38 -03:00
|
|
|
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();
|
2026-03-18 10:53:38 -04:00
|
|
|
|
2026-04-17 01:16:38 -03:00
|
|
|
const QVariantMap stats = m_coreModuleManager->moduleStats(name);
|
|
|
|
|
if (!stats.isEmpty()) {
|
|
|
|
|
module["cpu"] = stats["cpu"];
|
|
|
|
|
module["memory"] = stats["memory"];
|
2025-12-10 10:57:16 -05:00
|
|
|
} else {
|
|
|
|
|
module["cpu"] = "0.0";
|
|
|
|
|
module["memory"] = "0.0";
|
|
|
|
|
}
|
2026-03-18 10:53:38 -04:00
|
|
|
|
2025-12-10 10:57:16 -05:00
|
|
|
modules.append(module);
|
|
|
|
|
}
|
2026-03-18 10:53:38 -04:00
|
|
|
|
2025-12-10 10:57:16 -05:00
|
|
|
return modules;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 01:16:38 -03:00
|
|
|
// --- 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).
|
2026-03-18 10:53:38 -04:00
|
|
|
|
2026-04-17 01:16:38 -03:00
|
|
|
QVariantList MainUIBackend::uiModules() const { return m_uiPluginManager->uiModules(); }
|
|
|
|
|
QVariantList MainUIBackend::launcherApps() const { return m_uiPluginManager->launcherApps(); }
|
|
|
|
|
QString MainUIBackend::currentVisibleApp() const{ return m_uiPluginManager->currentVisibleApp(); }
|
|
|
|
|
QStringList MainUIBackend::loadingModules() const { return m_uiPluginManager->loadingModules(); }
|
2026-03-18 10:53:38 -04:00
|
|
|
|
2026-04-17 01:16:38 -03:00
|
|
|
// 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() { m_uiPluginManager->refreshUiModules(); }
|
|
|
|
|
void MainUIBackend::onAppLauncherClicked(const QString& n) { m_uiPluginManager->onAppLauncherClicked(n); }
|
|
|
|
|
void MainUIBackend::onPluginWindowClosed(const QString& n) { m_uiPluginManager->onPluginWindowClosed(n); }
|
|
|
|
|
void MainUIBackend::setCurrentVisibleApp(const QString& n) { m_uiPluginManager->setCurrentVisibleApp(n); }
|
|
|
|
|
|
|
|
|
|
// PackageCoordinator — package_manager IPC and package-lifecycle cascade.
|
|
|
|
|
void MainUIBackend::installPluginFromPath(const QString& p) { m_packageCoordinator->installPluginFromPath(p); }
|
|
|
|
|
void MainUIBackend::openInstallPluginDialog() { m_packageCoordinator->openInstallPluginDialog(); }
|
|
|
|
|
void MainUIBackend::uninstallUiModule(const QString& n) { m_packageCoordinator->uninstallUiModule(n); }
|
|
|
|
|
void MainUIBackend::uninstallCoreModule(const QString& n) { m_packageCoordinator->uninstallCoreModule(n); }
|
|
|
|
|
void MainUIBackend::confirmUninstallCascade(const QString& n) { m_packageCoordinator->confirmUninstallCascade(n); }
|
2026-05-06 00:31:21 +05:30
|
|
|
void MainUIBackend::confirmUninstallMultiCascade(const QStringList& names) { m_packageCoordinator->confirmUninstallMultiCascade(names); }
|
|
|
|
|
void MainUIBackend::cancelMultiUninstall(const QStringList& names) { m_packageCoordinator->cancelMultiUninstall(names); }
|
2026-04-17 01:16:38 -03:00
|
|
|
void MainUIBackend::confirmInstall() { m_packageCoordinator->confirmInstall(); }
|
|
|
|
|
void MainUIBackend::cancelInstall() { m_packageCoordinator->cancelInstall(); }
|
2026-06-08 14:49:33 +02:00
|
|
|
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); }
|
2026-04-17 01:16:38 -03:00
|
|
|
|
|
|
|
|
// 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);
|
2025-12-10 10:57:16 -05:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 01:16:38 -03:00
|
|
|
// --- CoreModuleManager delegations ----------------------------------------
|
2026-01-20 13:28:27 -05:00
|
|
|
|
2026-04-17 01:16:38 -03:00
|
|
|
void MainUIBackend::refreshCoreModules() { m_coreModuleManager->refresh(); }
|
|
|
|
|
QString MainUIBackend::getCoreModuleMethods(const QString& n) { return m_coreModuleManager->getMethods(n); }
|
2026-06-04 17:18:04 -03:00
|
|
|
QString MainUIBackend::getCoreModuleEvents(const QString& n) { return m_coreModuleManager->getEvents(n); }
|
2026-04-17 01:16:38 -03:00
|
|
|
QString MainUIBackend::callCoreModuleMethod(const QString& n,
|
|
|
|
|
const QString& m,
|
|
|
|
|
const QString& a) { return m_coreModuleManager->callMethod(n, m, a); }
|
2026-04-22 08:47:28 -03:00
|
|
|
|
|
|
|
|
// --- 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(); }
|
2026-05-25 11:13:50 -04:00
|
|
|
|
|
|
|
|
// --- Sidebar tooltip -------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
void MainUIBackend::setSidebarTooltipText(const QString& text) {
|
|
|
|
|
if (m_sidebarTooltipText != text) {
|
|
|
|
|
m_sidebarTooltipText = text;
|
|
|
|
|
emit sidebarTooltipChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MainUIBackend::setSidebarTooltipY(qreal y) {
|
|
|
|
|
if (m_sidebarTooltipY != y) {
|
|
|
|
|
m_sidebarTooltipY = y;
|
|
|
|
|
emit sidebarTooltipChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|