fix(framework): resolve mime-apps lists in spec order and keep optimistic Wi-Fi settings until confirmed

pull/18/head
reionwong 3 weeks ago
parent 51e42cf1a8
commit 0d35f58b13

@ -109,6 +109,53 @@ QString MimeApps::userTerminalsListPath()
+ QLatin1String("/xdg-terminals.list");
}
QStringList MimeApps::listPaths()
{
QStringList paths;
const QStringList desktops = DesktopEntry::currentDesktops();
for (const auto location :
{QStandardPaths::GenericConfigLocation, QStandardPaths::ApplicationsLocation}) {
for (const QString &directory : QStandardPaths::standardLocations(location)) {
for (const QString &desktop : desktops)
paths.append(directory + '/' + desktop.toLower() + QLatin1String("-mimeapps.list"));
paths.append(directory + QLatin1String("/mimeapps.list"));
}
}
paths.removeDuplicates();
return paths;
}
static QStringList fileLines(const QString &path)
{
QFile file(path);
if (!file.open(QIODevice::ReadOnly))
return {};
return QString::fromUtf8(file.readAll()).split('\n');
}
// The lists' additions for the type that no earlier list removed, and every removal, which
// also applies to the applications listing the type themselves.
struct ListedAssociations {
QStringList added;
QStringList removed;
};
static ListedAssociations listedAssociations(const QStringList &paths, const QString &mimeType)
{
ListedAssociations listed;
for (const QString &path : paths) {
const QStringList lines = fileLines(path);
if (lines.isEmpty())
continue;
for (const QString &app : readEntries(lines, "Added Associations", mimeType)) {
if (!listed.removed.contains(app) && !listed.added.contains(app))
listed.added.append(app);
}
listed.removed += readEntries(lines, "Removed Associations", mimeType);
}
return listed;
}
QStringList MimeApps::associations(const QString &path, const QString &group,
const QString &mimeType)
{
@ -148,7 +195,7 @@ bool MimeApps::setDefault(const QString &path, const QString &mimeType, const QS
return writeLines(path, lines);
}
QStringList MimeApps::applications(const QMimeType &mimeType)
QStringList MimeApps::applications(const QMimeType &mimeType, const QStringList &paths)
{
const QList<DesktopEntry *> entries = ApplicationRegistry::instance()->entries();
QMimeDatabase mimeDatabase;
@ -164,9 +211,23 @@ QStringList MimeApps::applications(const QMimeType &mimeType)
for (const QMimeType &type : std::as_const(mimeTypes))
names << type.name() << type.aliases();
QStringList removed;
QList<DesktopEntry *> added;
for (const QString &name : std::as_const(names)) {
const ListedAssociations listed = listedAssociations(paths, name);
removed += listed.removed;
for (const QString &app : listed.added) {
DesktopEntry *entry = entryForDesktopId(app);
if (entry && !added.contains(entry)) {
added.append(entry);
apps.append(entry->path());
}
}
}
QList<DesktopEntry *> matches;
for (DesktopEntry *entry : entries) {
if (!opensFiles(entry))
if (!opensFiles(entry) || removed.contains(QFileInfo(entry->path()).fileName()))
continue;
const QStringList entryTypes = entry->mimeTypes();
@ -180,7 +241,6 @@ QStringList MimeApps::applications(const QMimeType &mimeType)
return QFileInfo(a->path()).birthTime() < QFileInfo(b->path()).birthTime();
});
QList<DesktopEntry *> added;
for (DesktopEntry *entry : std::as_const(matches)) {
// One program installed under two desktop files is offered once.
const bool duplicate = std::any_of(added.cbegin(), added.cend(),
@ -208,23 +268,28 @@ QStringList MimeApps::applications(const QMimeType &mimeType)
return apps;
}
QString MimeApps::chosenApplication(const QString &path, const QMimeType &mimeType)
QString MimeApps::chosenApplication(const QStringList &paths, const QMimeType &mimeType)
{
for (const QString &group :
{QStringLiteral("Default Applications"), QStringLiteral("Added Associations")}) {
const auto apps = associations(path, group, mimeType.name());
for (const QString &app : apps) {
QStringList removed;
for (const QString &path : paths) {
const QStringList lines = fileLines(path);
if (lines.isEmpty())
continue;
for (const QString &app : readEntries(lines, "Default Applications", mimeType.name())) {
if (removed.contains(app))
continue;
if (const DesktopEntry *entry = entryForDesktopId(app))
return entry->path();
}
removed += readEntries(lines, "Removed Associations", mimeType.name());
}
return {};
}
QString MimeApps::defaultApplication(const QString &path, const QMimeType &mimeType)
QString MimeApps::defaultApplication(const QStringList &paths, const QMimeType &mimeType)
{
const QString chosen = chosenApplication(path, mimeType);
return chosen.isEmpty() ? applications(mimeType).value(0) : chosen;
const QString chosen = chosenApplication(paths, mimeType);
return chosen.isEmpty() ? applications(mimeType, paths).value(0) : chosen;
}
QString MimeApps::defaultTerminal(const QString &path)

@ -14,17 +14,22 @@ namespace MimeApps {
// The user's files under $XDG_CONFIG_HOME, which every choice is written to.
QString userListPath();
QString userTerminalsListPath();
// Every mimeapps.list that applies, the user's first: the config directories, then the
// applications directories, each with the current desktops' own list before the plain one.
QStringList listPaths();
QStringList associations(const QString &path, const QString &group, const QString &mimeType);
bool setDefault(const QString &path, const QString &mimeType, const QString &desktopId);
// Desktop file paths of the ApplicationRegistry entries that open the type: those listing it,
// else its parents a generation at a time, earliest installed first.
QStringList applications(const QMimeType &mimeType);
// The installed application the list at path names for the type, or an empty string.
QString chosenApplication(const QString &path, const QMimeType &mimeType);
// What opens the type: the choice in the list at path, else the first of applications().
QString defaultApplication(const QString &path, const QMimeType &mimeType);
// Desktop file paths of the installed applications that open the type: those the lists add,
// then those listing it themselves, else its parents a generation at a time, earliest
// installed first. An application a list removes is left out, unless an earlier list adds it.
QStringList applications(const QMimeType &mimeType, const QStringList &paths);
// The installed application the first list naming one chooses for the type, or an empty
// string. An application removed by an earlier list is not chosen by a later one.
QString chosenApplication(const QStringList &paths, const QMimeType &mimeType);
// What opens the type: the choice in the lists, else the first of applications().
QString defaultApplication(const QStringList &paths, const QMimeType &mimeType);
// xdg-terminals.list of the xdg-terminal-exec spec: desktop entry IDs, most preferred first.
QString defaultTerminal(const QString &path);

@ -2,11 +2,21 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "mimeapps.h"
#include "applicationregistry.h"
#include "desktopentry.h"
#include <QFile>
#include <QMimeDatabase>
#include <QTemporaryDir>
#include <QtTest>
static void writeList(const QString &path, const QByteArray &contents)
{
QFile file(path);
QVERIFY(file.open(QIODevice::WriteOnly));
file.write(contents);
}
class MimeAppsTest : public QObject
{
Q_OBJECT
@ -19,6 +29,59 @@ private slots:
qputenv("XDG_CONFIG_HOME", dir.path().toUtf8());
QCOMPARE(MimeApps::userListPath(), dir.filePath("mimeapps.list"));
QCOMPARE(MimeApps::userTerminalsListPath(), dir.filePath("xdg-terminals.list"));
// The desktop's own list in the config home comes first, the plain one after it,
// and the system's lists follow.
const QStringList paths = MimeApps::listPaths();
QVERIFY(paths.first().startsWith(dir.path()));
QVERIFY(paths.first().endsWith("-mimeapps.list"));
const int user = paths.indexOf(dir.filePath("mimeapps.list"));
QVERIFY(user > 0);
QVERIFY(paths.size() > user + 1);
QVERIFY(!paths.at(user + 1).startsWith(dir.path()));
}
void earlierListsOverrideLaterOnes()
{
QTemporaryDir dir;
const QString user = dir.filePath("user.list");
const QString system = dir.filePath("system.list");
writeList(user, "[Default Applications]\ntext/plain=gone.desktop;\n"
"[Removed Associations]\ntext/plain=system-editor.desktop;\n");
writeList(system, "[Default Applications]\ntext/plain=system-editor.desktop;\n"
"[Added Associations]\ntext/plain=viewer.desktop;system-editor.desktop;\n");
auto *registry = ApplicationRegistry::instance();
QTRY_VERIFY(!registry->loading());
DesktopEntryData systemEditor;
DesktopEntryData viewer;
DesktopEntryData otherEditor;
QVERIFY(DesktopEntry::parse(
"system-editor", "/applications/system-editor.desktop",
"[Desktop Entry]\nType=Application\nName=System Editor\nExec=system-editor %F\n"
"MimeType=text/plain;\n", &systemEditor));
QVERIFY(DesktopEntry::parse(
"viewer", "/applications/viewer.desktop",
"[Desktop Entry]\nType=Application\nName=Viewer\nExec=viewer %F\n", &viewer));
QVERIFY(DesktopEntry::parse(
"other-editor", "/applications/other-editor.desktop",
"[Desktop Entry]\nType=Application\nName=Other Editor\nExec=other-editor %F\n"
"MimeType=text/plain;\n", &otherEditor));
registry->applyScan({systemEditor, viewer, otherEditor});
const QMimeType text = QMimeDatabase().mimeTypeForName("text/plain");
const QStringList chain {user, system};
// The user's default is not installed, and the user removed the system's default.
QVERIFY(MimeApps::chosenApplication(chain, text).isEmpty());
// The system's addition comes before the entries that list the type; the removed
// application is out even though the system list adds it and it lists the type.
QCOMPARE(MimeApps::applications(text, chain),
QStringList({"/applications/viewer.desktop", "/applications/other-editor.desktop"}));
QCOMPARE(MimeApps::defaultApplication(chain, text), "/applications/viewer.desktop");
QCOMPARE(MimeApps::chosenApplication({system}, text), "/applications/system-editor.desktop");
QCOMPARE(MimeApps::applications(text, {}),
QStringList({"/applications/system-editor.desktop",
"/applications/other-editor.desktop"}));
}
void preservesAssociations()

@ -263,6 +263,10 @@ private slots:
QVERIFY(!item.availableToOtherUsers());
item.setAutoConnect(false);
// An unrelated change arriving before the profile is re-read must not
// put the switch back to what the stale cache says.
Q_EMIT NetworkClient::instance()->changed();
QVERIFY(!item.autoConnect());
QTRY_COMPARE(nm->count(QStringLiteral("Update")), 1);
const auto settings = qdbus_cast<NMVariantMapMap>(nm->last(QStringLiteral("Update")).arguments().value(0));

@ -19,6 +19,7 @@ void WirelessItemSettings::setPath(const QString &path)
if (m_path == path)
return;
m_path = path;
m_pending.clear();
Q_EMIT pathChanged();
refresh();
}
@ -71,22 +72,45 @@ void WirelessItemSettings::update(const QString &key, const QVariant &value)
// Without secrets in it, an update keeps the ones NetworkManager and the keyring hold.
NMVariantMapMap settings = m_client->settings(m_path);
settings[connectionSetting].insert(key, value);
m_pending.insert(key, value);
const QString path = m_path;
m_client->call(m_path, connectionInterface, QStringLiteral("Update"), {QVariant::fromValue(settings)}, this,
[](const QDBusMessage &reply) {
if (reply.type() == QDBusMessage::ErrorMessage)
qWarning() << "Cannot update the network:" << reply.errorMessage();
[this, path, key, value](const QDBusMessage &reply) {
if (reply.type() != QDBusMessage::ErrorMessage)
return;
qWarning() << "Cannot update the network:" << reply.errorMessage();
// The profile keeps its old value, so show that again.
if (path == m_path && m_pending.value(key) == value) {
m_pending.remove(key);
refresh();
}
},
2 * 60 * 1000);
}
// The cache is re-read after NetworkManager reports the profile updated, which
// comes after any unrelated change. Until then a written value is the truth.
QVariant WirelessItemSettings::cached(const QString &key, const QVariant &defaultValue)
{
const QVariant value = m_client->setting(m_path, connectionSetting, key);
const auto pending = m_pending.constFind(key);
if (pending == m_pending.constEnd())
return value;
// NetworkManager leaves out a value that is the default.
if ((value.isValid() ? value : defaultValue) != pending.value())
return pending.value();
m_pending.erase(pending);
return value;
}
void WirelessItemSettings::refresh()
{
if (!m_client->readable(m_path))
return;
const QString name = m_client->setting(m_path, connectionSetting, QStringLiteral("id")).toString();
const QVariant autoConnect = m_client->setting(m_path, connectionSetting, QStringLiteral("autoconnect"));
const bool available = m_client->setting(m_path, connectionSetting, QStringLiteral("permissions")).toStringList().isEmpty();
const QString name = cached(QStringLiteral("id")).toString();
const QVariant autoConnect = cached(QStringLiteral("autoconnect"), true);
const bool available = cached(QStringLiteral("permissions"), QStringList()).toStringList().isEmpty();
if (name != m_name) {
m_name = name;

@ -4,6 +4,7 @@
#include <network_export.h>
#include <QObject>
#include <QVariantMap>
#include <QtQml/qqmlregistration.h>
class NetworkClient;
@ -39,10 +40,13 @@ Q_SIGNALS:
private:
void update(const QString &key, const QVariant &value);
QVariant cached(const QString &key, const QVariant &defaultValue = {});
void refresh();
NetworkClient *m_client;
QString m_path;
// Values written but not yet read back; the stale cache must not undo them.
QVariantMap m_pending;
QString m_name;
bool m_autoConnect = true;
bool m_availableToOtherUsers = false;

Loading…
Cancel
Save