fix: add application modal state is wrong when clicked in between an ongoing download

This commit is contained in:
Khushboo Mehta
2026-06-23 17:00:29 +02:00
parent 5f046bc0ac
commit cb02b9c333
14 changed files with 488 additions and 124 deletions
+11
View File
@@ -99,6 +99,17 @@ int AppsFilterProxy::alreadyInstalledCount() const
return c;
}
int AppsFilterProxy::installingCount() const
{
int c = 0;
const int n = rowCount();
for (int i = 0; i < n; ++i) {
if (data(index(i, 0), AppsModel::ActionRole).toString()
== QStringLiteral("installing")) ++c;
}
return c;
}
int AppsFilterProxy::errorCount() const
{
int c = 0;
+2
View File
@@ -23,6 +23,7 @@ class AppsFilterProxy : public QSortFilterProxyModel {
Q_PROPERTY(int upgradeCount READ upgradeCount NOTIFY breakdownChanged)
Q_PROPERTY(int reinstallCount READ reinstallCount NOTIFY breakdownChanged)
Q_PROPERTY(int alreadyInstalledCount READ alreadyInstalledCount NOTIFY breakdownChanged)
Q_PROPERTY(int installingCount READ installingCount NOTIFY breakdownChanged)
Q_PROPERTY(int errorCount READ errorCount NOTIFY breakdownChanged)
Q_PROPERTY(bool hasResolutionErrors READ hasResolutionErrors NOTIFY hasResolutionErrorsChanged)
Q_PROPERTY(int visibleCount READ visibleCount NOTIFY visibleCountChanged)
@@ -52,6 +53,7 @@ public:
int upgradeCount() const;
int reinstallCount() const;
int alreadyInstalledCount() const;
int installingCount() const;
int errorCount() const;
bool hasResolutionErrors() const;
int visibleCount() const { return rowCount(); }
+60 -24
View File
@@ -1,5 +1,7 @@
#include "AppsModel.h"
#include "InstallRegistry.h"
#include <QSet>
namespace {
@@ -46,12 +48,23 @@ QVariant AppsModel::data(const QModelIndex& index, int role) const
case MissingDepsRole: return r.missingDeps;
case InstallStatusRole: return static_cast<int>(r.installStatus);
case InstallTypeRole: return r.installType;
case ActionRole: return r.action;
case ActionRole: {
if (m_installRegistry) {
if (m_installRegistry->isInFlight(r.name))
return QStringLiteral("installing");
if (m_installRegistry->stage(r.name) == InstallStage::Installed)
return QStringLiteral("installed");
}
return r.action;
}
case ToVersionRole: return r.toVersion;
case IsTopLevelRole: return r.isTopLevel;
case ResolverErrorRole: return r.resolverError;
case InstallStageRole: return static_cast<int>(r.installStage);
case InstallErrorRole: return r.installError;
case InstallStageRole:
return m_installRegistry ? m_installRegistry->stage(r.name)
: static_cast<int>(InstallStage::None);
case InstallErrorRole:
return m_installRegistry ? m_installRegistry->error(r.name) : QString();
}
return {};
}
@@ -302,6 +315,30 @@ void AppsModel::markInstalled(const QString& name,
}
}
void AppsModel::replaceInstalledSet(const QHash<QString, QString>& versionByName,
const QHash<QString, QString>& hashByName)
{
const bool wasBulk = m_inBulkInstalledUpdate;
if (!wasBulk) beginBulkInstalledUpdate();
for (int idx = 0; idx < m_rows.size(); ++idx) {
Row& r = m_rows[idx];
const auto it = versionByName.find(r.name);
const bool isInstalled = (it != versionByName.end());
const QString newVersion = isInstalled ? it.value() : QString();
const QString newHash = isInstalled ? hashByName.value(r.name) : QString();
if (r.installedVersion == newVersion && r.installedHash == newHash) continue;
r.installedVersion = newVersion;
r.installedHash = newHash;
recomputeInstallStatus(r);
const QModelIndex mi = index(idx);
emit dataChanged(mi, mi, {InstalledVersionRole, HasUpdateRole,
IsInstalledRole, InstallStatusRole});
}
if (!wasBulk) endBulkInstalledUpdate();
}
void AppsModel::beginBulkInstalledUpdate()
{
m_inBulkInstalledUpdate = true;
@@ -357,20 +394,26 @@ void AppsModel::setMissingDeps(const QString& name, const QStringList& missing)
}
}
// ── Mutation: live install stage ──────────────────────────────────────────
// ── Wiring: live install state ────────────────────────────────────────────
void AppsModel::setInstallStage(const QString& name,
InstallStage::Value stage,
const QString& error)
void AppsModel::setInstallRegistry(InstallRegistry* installRegistry)
{
for (int idx : m_indicesByName.values(name)) {
Row& r = m_rows[idx];
if (r.installStage == stage && r.installError == error) continue;
r.installStage = stage;
r.installError = stage == InstallStage::Failed ? error : QString();
const QModelIndex mi = index(idx);
emit dataChanged(mi, mi, {InstallStageRole, InstallErrorRole});
}
if (m_installRegistry == installRegistry) return;
if (m_installRegistry) m_installRegistry->disconnect(this);
m_installRegistry = installRegistry;
if (!m_installRegistry) return;
auto refresh = [this](const QString& name) {
const QList<int> roles{InstallStageRole, InstallErrorRole, ActionRole};
for (int idx : m_indicesByName.values(name)) {
const QModelIndex mi = index(idx);
emit dataChanged(mi, mi, roles);
}
};
connect(m_installRegistry, &InstallRegistry::stageChanged, this,
[refresh](const QString& name, InstallStage::Value) { refresh(name); });
connect(m_installRegistry, &InstallRegistry::errorChanged, this,
[refresh](const QString& name, const QString&) { refresh(name); });
}
// ── Mutation: resolver overlay ─────────────────────────────────────────────
@@ -386,16 +429,9 @@ void AppsModel::setResolverOverlay(const QList<ResolverRow>& rows)
r.toVersion = src.toVersion;
r.isTopLevel = src.isTopLevel;
r.resolverError = src.resolverError;
// Wipe any sticky pipeline state from a previous session
QList<int> changedRoles{ActionRole, ToVersionRole, IsTopLevelRole, ResolverErrorRole};
if (r.installStage != InstallStage::None
|| !r.installError.isEmpty()) {
r.installStage = InstallStage::None;
r.installError.clear();
changedRoles << InstallStageRole << InstallErrorRole;
}
const QModelIndex mi = index(idx);
emit dataChanged(mi, mi, changedRoles);
emit dataChanged(mi, mi,
{ActionRole, ToVersionRole, IsTopLevelRole, ResolverErrorRole});
}
}
+13 -8
View File
@@ -4,10 +4,13 @@
#include <QAbstractListModel>
#include <QHash>
#include <QPointer>
#include <QString>
#include <QVariantList>
#include <QVariantMap>
class InstallRegistry;
// AppsModel — the single source of truth for every package the App Manager
// (and Modules tab) cares about.
class AppsModel : public QAbstractListModel {
@@ -65,12 +68,13 @@ public:
void markInstalled(const QString& name,
const QString& installedVersion,
const QString& installedHash = {});
void replaceInstalledSet(const QHash<QString, QString>& versionByName,
const QHash<QString, QString>& hashByName);
void setInstallType(const QString& name, const QString& installType);
void setIconUrl(const QString& name, const QString& iconUrl);
void setMissingDeps(const QString& name, const QStringList& missing);
void setInstallStage(const QString& name,
InstallStage::Value stage,
const QString& error = {});
void setInstallRegistry(InstallRegistry* installRegistry);
void beginBulkInstalledUpdate();
void endBulkInstalledUpdate();
@@ -116,11 +120,9 @@ private:
QStringList missingDeps;
InstallStatus::Value installStatus = InstallStatus::NotInstalled;
// Live install pipeline state
InstallStage::Value installStage = InstallStage::None;
QString installError;
// Resolver overlay (per dialog session)
// Resolver overlay (per dialog session). Live install state lives
// on m_installRegistry — see setInstallRegistry. Per-row InstallStageRole /
// InstallErrorRole / ActionRole derive from there at read time.
QString action;
QString toVersion;
bool isTopLevel = false;
@@ -136,4 +138,7 @@ private:
QHash<QString, int> m_indexByKey; // (repo + "\n" + name) → row index
QMultiHash<QString, int> m_indicesByName;
bool m_inBulkInstalledUpdate = false;
// Source of truth for in-flight install state
QPointer<InstallRegistry> m_installRegistry;
};
@@ -112,6 +112,7 @@ Dialog {
readonly property bool actionEnabled:
d.targetName.length > 0
&& !d.installing
&& d.installingBuckets === 0
&& (d.actionMode === "launch" || !d.hasResolutionErrors)
readonly property int totalDeps:
@@ -131,6 +132,8 @@ Dialog {
root.requiredPackagesModel ? root.requiredPackagesModel.reinstallCount : 0
readonly property int alreadyInstalledBuckets:
root.requiredPackagesModel ? root.requiredPackagesModel.alreadyInstalledCount : 0
readonly property int installingBuckets:
root.requiredPackagesModel ? root.requiredPackagesModel.installingCount : 0
readonly property int errorBuckets:
root.requiredPackagesModel ? root.requiredPackagesModel.errorCount : 0
readonly property int changingBuckets:
@@ -144,6 +147,12 @@ Dialog {
return d.stageLabel
}
if (d.installingBuckets > 0 && d.changingBuckets === 0) {
return qsTr("%1 of %2's required package(s) are currently being installed. "
+ "Wait for the active install to finish before launching %2.")
.arg(d.installingBuckets).arg(d.targetDisplayName)
}
// Top-level + deps all match disk → nothing to do. The action
// button reads "Launch"; hide the footer entirely.
if (d.changingBuckets === 0) return ""
@@ -24,6 +24,8 @@ ItemDelegate {
readonly property string action: root.appRow ? (root.appRow.action || "") : ""
readonly property string toVersion: root.appRow ? (root.appRow.toVersion || "") : ""
readonly property bool isError: d.action === "error"
readonly property bool isInstalled:
root.appRow ? (root.appRow.isInstalled === true) : false
readonly property int rowStage:
root.appRow && root.appRow.installStage !== undefined
? root.appRow.installStage
@@ -128,8 +130,9 @@ ItemDelegate {
case "upgrade": return qsTr("Upgrade")
case "downgrade": return qsTr("Downgrade")
case "reinstall": return qsTr("Reinstall")
default: return qsTr("Installed")
case "installed": return qsTr("Installed")
}
return d.isInstalled ? qsTr("Installed") : qsTr("Install")
}
color: {
if (d.isError) return Theme.palette.error
@@ -145,8 +148,9 @@ ItemDelegate {
case "upgrade": return Theme.palette.info
case "downgrade": return Theme.palette.info
case "reinstall": return Theme.palette.info
default: return Theme.palette.textTertiary
case "installed": return Theme.palette.textTertiary
}
return d.isInstalled ? Theme.palette.textTertiary : Theme.palette.primary
}
radius: Theme.spacing.radiusLarge
+13 -4
View File
@@ -95,6 +95,7 @@ Item {
AddApplicationDialog {
id: addApplicationDialog
requiredPackagesModel: backend.requiredPackagesModel
onClosed: backend.notifyAddApplicationDialogClosed()
onInstallRequested: function(name, repositoryUrl, versionPins) {
addApplicationDialog.installStage = InstallStage.Downloading
backend.confirmCatalogInstall(name, repositoryUrl, versionPins)
@@ -195,15 +196,23 @@ Item {
backend.onAppLauncherClicked(name);
}
function onAddApplicationRequested(metadata, requiredPackages) {
if (addApplicationDialog.visible) {
function onRequestOpenAddApplicationDialog(metadata) {
if (!addApplicationDialog.visible) {
addApplicationDialog.openWith(metadata);
} else if (addApplicationDialog.metadata.name === metadata.name) {
// Version re-resolve while the same app's dialog is already open.
addApplicationDialog.metadata = metadata;
addApplicationDialog.installStage = metadata.installStage || InstallStage.None;
} else {
addApplicationDialog.openWith(metadata);
}
}
function onAddApplicationDataUpdated(metadata) {
if (!addApplicationDialog.visible) return;
if (addApplicationDialog.metadata.name !== metadata.name) return;
addApplicationDialog.metadata = metadata;
addApplicationDialog.installStage = metadata.installStage || InstallStage.None;
}
function onCatalogInstallStageChanged(name, stage) {
if (!addApplicationDialog.visible) return;
if (addApplicationDialog.metadata.name !== name) return;
+1
View File
@@ -118,6 +118,7 @@ set(SOURCES
CoreModuleManager.cpp
UIPluginManager.cpp
PackageCoordinator.cpp
InstallRegistry.cpp
AppsModel.cpp
PluginLoader.cpp
restricted/DenyAllReply.cpp
+125
View File
@@ -0,0 +1,125 @@
#include "InstallRegistry.h"
InstallRegistry::InstallRegistry(QObject* parent) : QObject(parent) {}
int InstallRegistry::stage(const QString& name) const
{
const auto it = m_ops.constFind(name);
return it == m_ops.cend() ? InstallStage::None
: static_cast<int>(it->stage);
}
bool InstallRegistry::isInFlight(const QString& name) const
{
const auto it = m_ops.constFind(name);
if (it == m_ops.cend()) return false;
switch (it->stage) {
case InstallStage::Downloading:
case InstallStage::Queued:
case InstallStage::Installing:
return true;
default:
return false;
}
}
QString InstallRegistry::error(const QString& name) const
{
const auto it = m_ops.constFind(name);
return it == m_ops.cend() ? QString() : it->error;
}
QString InstallRegistry::targetVersion(const QString& name) const
{
const auto it = m_ops.constFind(name);
return it == m_ops.cend() ? QString() : it->targetVersion;
}
QString InstallRegistry::targetHash(const QString& name) const
{
const auto it = m_ops.constFind(name);
return it == m_ops.cend() ? QString() : it->targetHash;
}
void InstallRegistry::begin(const QString& name,
const QString& targetVersion,
const QString& targetHash,
const QString& startedByTopLevel)
{
if (name.isEmpty()) return;
const bool added = !m_ops.contains(name);
Entry& e = m_ops[name];
e.name = name;
e.targetVersion = targetVersion;
e.targetHash = targetHash;
e.stage = InstallStage::Downloading;
e.error.clear();
e.startedByTopLevel = startedByTopLevel;
emit stageChanged(name, e.stage);
if (added) emit activeNamesChanged();
}
void InstallRegistry::setStage(const QString& name, InstallStage::Value stage)
{
auto it = m_ops.find(name);
if (it == m_ops.end()) return;
if (it->stage == stage) return;
it->stage = stage;
emit stageChanged(name, stage);
}
void InstallRegistry::fail(const QString& name, const QString& error)
{
auto it = m_ops.find(name);
if (it == m_ops.end()) return;
const bool stageChanged_ = it->stage != InstallStage::Failed;
it->stage = InstallStage::Failed;
it->error = error;
if (stageChanged_) emit stageChanged(name, InstallStage::Failed);
emit errorChanged(name, error);
}
void InstallRegistry::finish(const QString& name)
{
setStage(name, InstallStage::Installed);
}
void InstallRegistry::clear(const QString& name)
{
if (!m_ops.contains(name)) return;
m_ops.remove(name);
emit stageChanged(name, InstallStage::None);
emit errorChanged(name, QString());
emit activeNamesChanged();
}
void InstallRegistry::clearByTopLevel(const QString& topLevelName)
{
if (topLevelName.isEmpty()) return;
QStringList toRemove;
for (auto it = m_ops.cbegin(); it != m_ops.cend(); ++it) {
if (it->startedByTopLevel == topLevelName) toRemove.append(it.key());
}
if (toRemove.isEmpty()) return;
for (const QString& name : toRemove) m_ops.remove(name);
for (const QString& name : toRemove) {
emit stageChanged(name, InstallStage::None);
emit errorChanged(name, QString());
}
emit activeNamesChanged();
}
void InstallRegistry::beginOrTrack(const QString& name,
const QString& targetVersion,
const QString& targetHash,
const QString& startedByTopLevel)
{
if (name.isEmpty()) return;
if (!m_ops.contains(name)) {
begin(name, targetVersion, targetHash, startedByTopLevel);
return;
}
Entry& e = m_ops[name];
if (!targetVersion.isEmpty()) e.targetVersion = targetVersion;
if (!targetHash.isEmpty()) e.targetHash = targetHash;
}
+56
View File
@@ -0,0 +1,56 @@
#pragma once
#include "InstallEnums.h"
#include <QHash>
#include <QObject>
#include <QString>
#include <QStringList>
// Registry of in-flight install operations. One entry per package name
class InstallRegistry : public QObject {
Q_OBJECT
Q_PROPERTY(QStringList activeNames READ activeNames NOTIFY activeNamesChanged)
public:
struct Entry {
QString name;
QString targetVersion;
QString targetHash;
InstallStage::Value stage = InstallStage::None;
QString error;
QString startedByTopLevel;
};
explicit InstallRegistry(QObject* parent = nullptr);
Q_INVOKABLE bool has(const QString& name) const { return m_ops.contains(name); }
Q_INVOKABLE int stage(const QString& name) const;
Q_INVOKABLE bool isInFlight(const QString& name) const;
QString error(const QString& name) const;
QString targetVersion(const QString& name) const;
QString targetHash(const QString& name) const;
QStringList activeNames() const { return m_ops.keys(); }
void beginOrTrack(const QString& name,
const QString& targetVersion,
const QString& targetHash,
const QString& startedByTopLevel);
void begin(const QString& name,
const QString& targetVersion,
const QString& targetHash,
const QString& startedByTopLevel);
void setStage(const QString& name, InstallStage::Value stage);
void fail(const QString& name, const QString& error);
void finish(const QString& name);
void clear(const QString& name);
void clearByTopLevel(const QString& topLevelName);
signals:
void activeNamesChanged();
void stageChanged(const QString& name, InstallStage::Value stage);
void errorChanged(const QString& name, const QString& error);
private:
QHash<QString, Entry> m_ops;
};
+7 -2
View File
@@ -47,6 +47,7 @@ MainUIBackend::MainUIBackend(LogosAPI* logosAPI, QObject* parent)
m_uiPluginManager = new UIPluginManager(m_logosAPI, m_coreModuleManager, this);
m_packageCoordinator = new PackageCoordinator(m_logosAPI, m_coreModuleManager, m_uiPluginManager, m_appsModel, this);
m_packageCoordinator->setRequiredPackagesModel(m_requiredPackagesModel);
m_appsModel->setInstallRegistry(m_packageCoordinator->installRegistry());
// Setter-injection closes the cycle — UIPluginManager queries
// PackageCoordinator for installType / missing-deps when building its
@@ -99,8 +100,10 @@ MainUIBackend::MainUIBackend(LogosAPI* logosAPI, QObject* parent)
this, &MainUIBackend::upgradeCascadeConfirmationRequested);
connect(m_packageCoordinator, &PackageCoordinator::uninstallMultiCascadeConfirmationRequested,
this, &MainUIBackend::uninstallMultiCascadeConfirmationRequested);
connect(m_packageCoordinator, &PackageCoordinator::addApplicationRequested,
this, &MainUIBackend::addApplicationRequested);
connect(m_packageCoordinator, &PackageCoordinator::requestOpenAddApplicationDialog,
this, &MainUIBackend::requestOpenAddApplicationDialog);
connect(m_packageCoordinator, &PackageCoordinator::addApplicationDataUpdated,
this, &MainUIBackend::addApplicationDataUpdated);
connect(m_packageCoordinator, &PackageCoordinator::launchAppRequested,
this, &MainUIBackend::launchAppRequested);
connect(m_packageCoordinator, &PackageCoordinator::catalogInstallStageChanged,
@@ -241,6 +244,8 @@ void MainUIBackend::openApp(const QString& name, const QString& repositoryUrl, c
{ 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
+3 -1
View File
@@ -175,6 +175,7 @@ public slots:
Q_INVOKABLE void confirmCatalogInstall(const QString& name,
const QString& repositoryUrl,
const QVariantMap& versionPins = QVariantMap());
Q_INVOKABLE void notifyAddApplicationDialogClosed();
// Core Module operations — routing rule: cascade-aware (load/unload)
// goes through UIPluginManager so it can run the pre-flight dependent
@@ -209,7 +210,8 @@ signals:
// App-Manager dialog + install lifecycle. See PackageCoordinator for
// the contract — these are pure re-emits.
void addApplicationRequested(const QVariantMap& metadata);
void requestOpenAddApplicationDialog(const QVariantMap& metadata);
void addApplicationDataUpdated(const QVariantMap& metadata);
void launchAppRequested(const QString& name);
void catalogInstallStageChanged(const QString& name, InstallStage::Value stage);
void catalogInstallFinished(const QString& name);
+149 -74
View File
@@ -1,4 +1,5 @@
#include "PackageCoordinator.h"
#include "InstallRegistry.h"
#include "AppsFilterProxy.h"
#include "AppsModel.h"
#include "CoreModuleManager.h"
@@ -31,6 +32,7 @@ PackageCoordinator::PackageCoordinator(LogosAPI* logosAPI,
, m_coreModuleManager(coreModuleManager)
, m_uiPluginManager(uiPluginManager)
, m_appsModel(appsModel)
, m_installRegistry(new InstallRegistry(this))
{
subscribeToPackageInstallationEvents();
subscribeToPackageDownloaderEvents();
@@ -995,11 +997,6 @@ void PackageCoordinator::refreshDependencyInfo()
[self](QVariantList packages) {
if (!self) return;
self->m_installedPackagesCache = packages;
// Snapshot the previous installed set BEFORE the wholesale assignments
// below overwrite it.
const QSet<QString> previouslyInstalled = self->m_installedNameSet;
QMap<QString, QString> typeMap;
QSet<QString> nameSet;
QHash<QString, QString> versionByName;
@@ -1026,35 +1023,14 @@ void PackageCoordinator::refreshDependencyInfo()
if (!version.isEmpty()) versionByName.insert(lookupName, version);
if (!rootHash.isEmpty()) hashByName.insert(lookupName, rootHash);
}
self->m_installTypeByModule = typeMap;
self->m_installTypeByModule = std::move(typeMap);
self->m_displayNameByModule = std::move(displayNameMap);
self->m_installedNameSet = std::move(nameSet);
self->m_installedVersionByName = std::move(versionByName);
for (auto it = hashByName.cbegin(); it != hashByName.cend(); ++it) {
self->m_installedHashByName.insert(it.key(), it.value());
}
for (const QString& name : previouslyInstalled) {
if (!self->m_installedNameSet.contains(name)) {
self->m_installedHashByName.remove(name);
}
}
// Replay markInstalled across the full package set — fetchUiPluginMetadata
// only saw UI plugins. Idempotent on (version, hash).
self->m_installedHashByName = std::move(hashByName);
if (self->m_appsModel) {
self->m_appsModel->beginBulkInstalledUpdate();
for (const QString& name : previouslyInstalled) {
if (!self->m_installedNameSet.contains(name)) {
self->m_appsModel->markInstalled(name, QString(), QString());
}
}
for (auto it = self->m_installedVersionByName.cbegin();
it != self->m_installedVersionByName.cend(); ++it) {
self->m_appsModel->markInstalled(
it.key(), it.value(),
self->m_installedHashByName.value(it.key()));
}
self->m_appsModel->endBulkInstalledUpdate();
self->m_appsModel->replaceInstalledSet(
self->m_installedVersionByName, self->m_installedHashByName);
}
// Second pass — per-module missing/dependents queries. Dispatched
@@ -1213,6 +1189,34 @@ QVariantMap nameAndRepo(const QString& name, const QString& repo)
};
}
QVariantList PackageCoordinator::collectCatalogRequired(const QString& name,
const QString& repositoryUrl) const
{
QVariantList out;
QSet<QString> seen;
out.append(nameAndRepo(name, repositoryUrl));
seen.insert(name);
if (repositoryUrl.isEmpty() || !m_appsModel) return out;
QStringList queue;
queue << name;
for (int head = 0; head < queue.size(); ++head) {
const QVariantMap row = m_appsModel->rowDataByName(queue[head], repositoryUrl);
if (row.isEmpty()) continue;
const QVariantList deps = row.value("dependencies").toList();
for (const QVariant& d : deps) {
const QString depName = d.toMap().value("name").toString();
if (depName.isEmpty() || seen.contains(depName)) continue;
if (m_appsModel->rowDataByName(depName, repositoryUrl).isEmpty()) continue;
seen.insert(depName);
out.append(nameAndRepo(depName, repositoryUrl));
queue << depName;
}
}
return out;
}
QString PackageCoordinator::depAction(const QString& installedVersion,
const QString& resolvedVersion,
const QString& installedHash,
@@ -1280,12 +1284,11 @@ QVariantList PackageCoordinator::computeDepChanges(
return out;
}
void PackageCoordinator::setSessionStage(const QString& name, InstallStage::Value stage)
void PackageCoordinator::setOpStage(const QString& name, InstallStage::Value stage)
{
auto it = m_installSessions.find(name);
if (it == m_installSessions.end()) return;
if (it->stage == stage) return;
it->stage = stage;
if (!m_installRegistry->has(name)) return;
if (m_installRegistry->stage(name) == static_cast<int>(stage)) return;
m_installRegistry->setStage(name, stage);
emit catalogInstallStageChanged(name, stage);
}
@@ -1317,6 +1320,13 @@ void PackageCoordinator::openApp(const QString& name,
runResolverAndOpenDialog(name, repositoryUrl, versionPins);
}
void PackageCoordinator::notifyAddApplicationDialogClosed()
{
if (m_activeAddDialogName.isEmpty()) return;
++m_dialogResolveEpoch[m_activeAddDialogName];
m_activeAddDialogName.clear();
}
void PackageCoordinator::runResolverAndOpenDialog(const QString& name,
const QString& repositoryUrl,
const QVariantMap& versionPins)
@@ -1327,6 +1337,7 @@ void PackageCoordinator::runResolverAndOpenDialog(const QString& name,
const QString targetVersion = versionPins.value(name).toString();
const int epoch = ++m_dialogResolveEpoch[name];
m_activeAddDialogName = name;
const QString depsJson = buildResolverDepsJson(name, repositoryUrl, versionPins);
@@ -1334,7 +1345,12 @@ void PackageCoordinator::runResolverAndOpenDialog(const QString& name,
<< "repo=" << repositoryUrl << "targetVersion=" << targetVersion
<< "pins=" << versionPins.size() << "epoch=" << epoch;
emitDialogMetadata(name, repositoryUrl, targetVersion, catalogRow, /*changes=*/{});
QVariantList initialChanges;
if (m_installRegistry->isInFlight(name))
initialChanges = m_lastResolvedChangesByName.value(name);
// Sync stack frame only — QML may open the modal from this signal.
emitDialogMetadata(name, repositoryUrl, targetVersion, catalogRow, initialChanges,
/*requestOpen=*/true);
LogosModules logos(m_logosAPI);
QPointer<PackageCoordinator> self(this);
@@ -1349,7 +1365,13 @@ void PackageCoordinator::runResolverAndOpenDialog(const QString& name,
}
const QVariantList changes =
self->computeDepChanges(resolved, self->m_installedVersionByName);
self->emitDialogMetadata(name, repositoryUrl, targetVersion, catalogRow, changes);
if (!resolved.isEmpty())
self->m_lastResolvedRawByName.insert(name, resolved);
if (!changes.isEmpty())
self->m_lastResolvedChangesByName.insert(name, changes);
// Async refresh only — never reopens the modal.
self->emitDialogMetadata(name, repositoryUrl, targetVersion, catalogRow, changes,
/*requestOpen=*/false);
});
}
@@ -1357,8 +1379,12 @@ void PackageCoordinator::emitDialogMetadata(const QString& name,
const QString& repositoryUrl,
const QString& targetVersion,
const QVariantMap& catalogRow,
const QVariantList& changes)
const QVariantList& changes,
bool requestOpen)
{
if (name != m_activeAddDialogName)
return;
QVariantMap metadata;
metadata["name"] = name;
metadata["repositoryUrl"] = repositoryUrl;
@@ -1388,10 +1414,7 @@ void PackageCoordinator::emitDialogMetadata(const QString& name,
? QString()
: versionsList.first().toMap().value("manifest").toMap().value("version").toString();
metadata["installStage"] = static_cast<int>(
m_installSessions.contains(name)
? m_installSessions.value(name).stage
: InstallStage::None);
metadata["installStage"] = m_installRegistry->stage(name);
// {name, repo} entries so the filter pins each row to the resolver's
// chosen repo and multi-repo names don't duplicate. Always at least the
@@ -1424,10 +1447,43 @@ void PackageCoordinator::emitDialogMetadata(const QString& name,
m_appsModel->setResolverOverlay(overlay);
}
// Union in the catalog-derived dependency set.
for (const QVariant& v : collectCatalogRequired(name, repositoryUrl)) {
const QString depName = v.toMap().value("name").toString();
if (depName.isEmpty() || seen.contains(depName)) continue;
seen.insert(depName);
requiredEntries.append(v);
}
if (m_requiredPackagesModel)
m_requiredPackagesModel->setRequiredPackages(requiredEntries);
emit addApplicationRequested(metadata);
if (requestOpen)
emit requestOpenAddApplicationDialog(metadata);
else
emit addApplicationDataUpdated(metadata);
}
void PackageCoordinator::refreshOverlayAfterInstall(const QString& topLevelName)
{
if (!m_appsModel || topLevelName.isEmpty()) return;
const QVariantList raw = m_lastResolvedRawByName.value(topLevelName);
if (raw.isEmpty()) return;
const QVariantList changes =
computeDepChanges(raw, m_installedVersionByName);
if (!changes.isEmpty())
m_lastResolvedChangesByName.insert(topLevelName, changes);
// Only push UI updates while this app's dialog is still the active session.
if (topLevelName != m_activeAddDialogName) return;
const QString repositoryUrl = m_repoByName.value(topLevelName);
const QVariantMap catalogRow =
m_appsModel->rowDataByName(topLevelName, repositoryUrl);
emitDialogMetadata(topLevelName, repositoryUrl, QString(), catalogRow, changes,
/*requestOpen=*/false);
}
void PackageCoordinator::confirmCatalogInstall(const QString& name,
@@ -1436,26 +1492,28 @@ void PackageCoordinator::confirmCatalogInstall(const QString& name,
{
if (!m_logosAPI || name.isEmpty()) return;
if (m_installSessions.contains(name)) {
if (m_installRegistry->has(name)) {
qDebug() << "confirmCatalogInstall: session for" << name
<< "already in progress, ignoring";
return;
}
InstallSession s;
s.name = name;
s.stage = InstallStage::Downloading;
m_installSessions.insert(name, s);
if (m_appsModel) m_appsModel->setInstallStage(name, InstallStage::Downloading);
m_installRegistry->begin(name, /*targetVersion=*/{}, /*targetHash=*/{},
/*startedByTopLevel=*/name);
emit catalogInstallStageChanged(name, InstallStage::Downloading);
const QString depsJson = buildResolverDepsJson(name, repositoryUrl, versionPins);
LogosModules logos(m_logosAPI);
QPointer<PackageCoordinator> self(this);
// Default IPC deadline (20s) is too tight when the catalog blob is many
// MB or the user is on a slow connection
constexpr int kDownloadIpcDeadlineMs = 5 * 60 * 1000;
logos.package_downloader.downloadResolvedDependenciesAsync(depsJson,
[self, name](QVariantList results) {
if (!self) return;
if (!results.isEmpty())
self->m_lastResolvedRawByName.insert(name, results);
QVariantList toInstall;
for (const QVariant& v : results) {
@@ -1478,9 +1536,9 @@ void PackageCoordinator::confirmCatalogInstall(const QString& name,
|| installedHash.isEmpty()
|| resolvedHash == installedHash;
if (versionMatches && hashMatches) {
if (self->m_appsModel)
self->m_appsModel->setInstallStage(rowName,
InstallStage::Installed);
self->m_installRegistry->beginOrTrack(rowName, resolvedVersion,
resolvedHash, name);
self->m_installRegistry->setStage(rowName, InstallStage::Installed);
continue;
}
toInstall.append(v);
@@ -1489,25 +1547,31 @@ void PackageCoordinator::confirmCatalogInstall(const QString& name,
if (toInstall.isEmpty()) {
// Nothing left to do after the skip-already-installed
// filter; treat as a successful no-op rather than Failed.
self->setSessionStage(name, InstallStage::Installed);
self->setOpStage(name, InstallStage::Installed);
emit self->catalogInstallFinished(name);
self->refreshOverlayAfterInstall(name);
QTimer::singleShot(1500, self.data(), [self, name]() {
if (!self) return;
self->m_installSessions.remove(name);
self->m_installRegistry->clearByTopLevel(name);
});
return;
}
for (const QVariant& v : toInstall) {
const QString rowName = v.toMap().value("name").toString();
if (!rowName.isEmpty() && self->m_appsModel)
self->m_appsModel->setInstallStage(rowName,
InstallStage::Queued);
const QVariantMap m = v.toMap();
const QString rowName = m.value("name").toString();
if (rowName.isEmpty()) continue;
self->m_installRegistry->beginOrTrack(rowName,
m.value("version").toString(),
m.value("rootHash").toString(),
name);
self->m_installRegistry->setStage(rowName, InstallStage::Queued);
}
self->setSessionStage(name, InstallStage::Installing);
self->setOpStage(name, InstallStage::Installing);
self->installResultsSequential(toInstall, name, 0);
});
},
Timeout(kDownloadIpcDeadlineMs));
}
void PackageCoordinator::installOnePackage(const QVariantMap& dl,
@@ -1547,24 +1611,34 @@ void PackageCoordinator::installResultsSequential(const QVariantList& results,
<< "of" << results.size()
<< "rowName=" << rowName
<< "topLevel=" << topLevelName;
if (!rowName.isEmpty() && m_appsModel)
m_appsModel->setInstallStage(rowName, InstallStage::Installing);
if (!rowName.isEmpty()) {
m_installRegistry->beginOrTrack(rowName, dl.value("version").toString(),
dl.value("rootHash").toString(), topLevelName);
m_installRegistry->setStage(rowName, InstallStage::Installing);
}
QPointer<PackageCoordinator> self(this);
installOnePackage(dl,
[self, results, topLevelName, rowName, index, failures]
[self, results, topLevelName, rowName, index, failures, dl]
(bool success, const QString& err) mutable {
qDebug() << "installOnePackage callback rowName=" << rowName
<< "success=" << success << "err=" << err;
if (!self) return;
if (!rowName.isEmpty()) {
const InstallStage::Value stage = success
? InstallStage::Installed
: InstallStage::Failed;
if (self->m_appsModel)
self->m_appsModel->setInstallStage(rowName, stage,
success ? QString() : err);
if (success) {
const QString ver = dl.value("version").toString();
const QString hash = dl.value("rootHash").toString();
if (!ver.isEmpty())
self->m_installedVersionByName.insert(rowName, ver);
if (!hash.isEmpty())
self->m_installedHashByName.insert(rowName, hash);
if (self->m_appsModel)
self->m_appsModel->markInstalled(rowName, ver, hash);
self->m_installRegistry->finish(rowName);
} else {
self->m_installRegistry->fail(rowName, err);
}
}
if (!success) {
failures.append(rowName.isEmpty()
@@ -1584,21 +1658,22 @@ void PackageCoordinator::installResultsSequential(const QVariantList& results,
if (!failures.isEmpty()) {
qDebug() << " install loop complete with failures for"
<< topLevelName << ":" << failures.size();
self->setSessionStage(topLevelName, InstallStage::Failed);
self->setOpStage(topLevelName, InstallStage::Failed);
emit self->catalogInstallFailed(
topLevelName, failures.join(QStringLiteral("; ")));
QTimer::singleShot(2500, self.data(), [self, topLevelName]() {
if (!self) return;
self->m_installSessions.remove(topLevelName);
self->m_installRegistry->clearByTopLevel(topLevelName);
});
return;
}
self->setSessionStage(topLevelName, InstallStage::Installed);
self->setOpStage(topLevelName, InstallStage::Installed);
emit self->catalogInstallFinished(topLevelName);
self->refreshOverlayAfterInstall(topLevelName);
QTimer::singleShot(1500, self.data(), [self, topLevelName]() {
if (!self) return;
self->m_installSessions.remove(topLevelName);
self->m_installRegistry->clearByTopLevel(topLevelName);
});
});
}
+33 -9
View File
@@ -12,6 +12,7 @@
class AppsFilterProxy;
class AppsModel;
class InstallRegistry;
class CoreModuleManager;
class UIPluginManager;
@@ -133,6 +134,13 @@ public slots:
Q_INVOKABLE void removeRepository(const QString& url);
Q_INVOKABLE void setRepositoryEnabled(const QString& url, bool enabled);
// Called by QML when the Add Application dialog closes so stale async
// resolver callbacks for previously-opened apps don't mutate the shared
// required-packages model or reopen the dialog.
Q_INVOKABLE void notifyAddApplicationDialogClosed();
InstallRegistry* installRegistry() const { return m_installRegistry; }
signals:
// Tells MainUIBackend to refresh the uiModules / launcherApps / coreModules
// properties — their values compose installType/missing-deps from here with
@@ -148,7 +156,12 @@ signals:
// changed.
void uiPluginsFetched(const QVariantList& uiPlugins);
void addApplicationRequested(const QVariantMap& metadata);
// Sync entry from openApp() only — QML opens the modal if not already
// visible (or refreshes in place when re-resolving the same app).
void requestOpenAddApplicationDialog(const QVariantMap& metadata);
// Passive refresh — never opens the modal; stale callbacks are dropped in
// emitDialogMetadata before this is emitted.
void addApplicationDataUpdated(const QVariantMap& metadata);
void catalogInstallStageChanged(const QString& name, InstallStage::Value stage);
void catalogInstallFinished(const QString& name);
void catalogInstallFailed(const QString& name, const QString& error);
@@ -267,6 +280,10 @@ private:
QString buildResolverDepsJson(const QString& name,
const QString& repositoryUrl,
const QVariantMap& versionPins) const;
// Transitive required-package set ({name, repositoryUrl}) computed purely
// from the local catalog dependency graph — no async resolver.
QVariantList collectCatalogRequired(const QString& name,
const QString& repositoryUrl) const;
QString buildInstalledPackagesJson() const;
QVariantList computeDepChanges(const QVariantList& resolved,
const QHash<QString, QString>& installedByName) const;
@@ -286,7 +303,11 @@ private:
const QString& repositoryUrl,
const QString& targetVersion,
const QVariantMap& catalogRow,
const QVariantList& changes);
const QVariantList& changes,
bool requestOpen);
// Recompute resolver overlay from cached raw resolve + current disk state.
// Keeps dep badges correct after the install registry is cleared.
void refreshOverlayAfterInstall(const QString& topLevelName);
void installResultsSequential(const QVariantList& results,
const QString& topLevelName,
int index,
@@ -294,8 +315,9 @@ private:
void installOnePackage(const QVariantMap& downloadResult,
std::function<void(bool, const QString&)> onDone);
// Move a session's stage and emit catalogInstallStageChanged.
void setSessionStage(const QString& name, InstallStage::Value stage);
// Drive the in-flight registry. setOpStage updates the InstallRegistry entry
// and emits catalogInstallStageChanged.
void setOpStage(const QString& name, InstallStage::Value stage);
// Wiring (not owned — see ctor comment).
LogosAPI* m_logosAPI;
@@ -326,11 +348,13 @@ private:
// to feed AppsModel's
// DifferentHash detection.
QHash<QString, int> m_dialogResolveEpoch;
struct InstallSession {
QString name;
InstallStage::Value stage = InstallStage::None;
};
QHash<QString, InstallSession> m_installSessions;
QString m_activeAddDialogName;
// Last resolver output per top-level: raw IPC rows and derived changes.
QHash<QString, QVariantList> m_lastResolvedRawByName;
QHash<QString, QVariantList> m_lastResolvedChangesByName;
InstallRegistry* m_installRegistry = nullptr;
QVariantList m_repositories;
int m_repositoriesLoadingCount = 0;