diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-05-30 11:17:16 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-05-30 11:32:29 -0500 |
| commit | 2464c836085cab832089b0f58c4bd74dfb679603 (patch) | |
| tree | 6e156cf8bb03c37e39195fe9832d5560d4a15ce4 /src | |
| parent | 06adc0d1399b73561348d9de9c5e2feba23c5d9d (diff) | |
release: prepare 0.2.3 stable
Diffstat (limited to 'src')
| -rw-r--r-- | src/src/nxmhandler_linux.cpp | 336 | ||||
| -rw-r--r-- | src/src/nxmhandler_linux.h | 3 | ||||
| -rw-r--r-- | src/src/settings.cpp | 6 | ||||
| -rw-r--r-- | src/src/settings.h | 1 | ||||
| -rw-r--r-- | src/src/settingsdialog.ui | 21 | ||||
| -rw-r--r-- | src/src/settingsdialognexus.cpp | 9 | ||||
| -rw-r--r-- | src/src/settingsdialognexus.h | 1 |
7 files changed, 295 insertions, 82 deletions
diff --git a/src/src/nxmhandler_linux.cpp b/src/src/nxmhandler_linux.cpp index aaf942e..520ab55 100644 --- a/src/src/nxmhandler_linux.cpp +++ b/src/src/nxmhandler_linux.cpp @@ -20,6 +20,13 @@ using namespace MOBase; namespace { +const QString NxmDesktopFile = QStringLiteral("com.fluorine.manager.nxm-handler.desktop"); +const QString LegacyNxmDesktopFile = QStringLiteral("mo2-nxm-handler.desktop"); +const QStringList UrlSchemes = { + QStringLiteral("x-scheme-handler/nxm"), + QStringLiteral("x-scheme-handler/modl"), +}; + QString ensureDir(const QString& path) { QDir const dir(path); @@ -41,83 +48,237 @@ bool writeTextFile(const QString& path, const QString& content) return stream.status() == QTextStream::Ok; } +QStringList desktopFilesFromEntry(const QString& line) +{ + const int equals = line.indexOf('='); + if (equals < 0) { + return {}; + } + + QStringList result; + for (const auto& desktopFile : line.mid(equals + 1).split(';', Qt::SkipEmptyParts)) { + const auto trimmed = desktopFile.trimmed(); + if (!trimmed.isEmpty() && !result.contains(trimmed)) { + result.append(trimmed); + } + } + return result; +} + +QString desktopFilesEntry(const QString& mimeType, const QStringList& desktopFiles) +{ + return mimeType + "=" + desktopFiles.join(';') + ";"; +} + +QStringList readMimeAppsList(const QString& path) +{ + QFile file(path); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return {}; + } + + auto lines = QString::fromUtf8(file.readAll()).split('\n'); + while (!lines.isEmpty() && lines.back().isEmpty()) { + lines.removeLast(); + } + return lines; +} + +int findSection(const QStringList& lines, const QString& sectionName) +{ + for (int i = 0; i < lines.size(); ++i) { + if (lines.at(i).trimmed() == sectionName) { + return i; + } + } + return -1; +} + +int sectionEnd(const QStringList& lines, int sectionStart) +{ + for (int i = sectionStart + 1; i < lines.size(); ++i) { + if (lines.at(i).trimmed().startsWith('[')) { + return i; + } + } + return lines.size(); +} + +template <typename Update> +void updateMimeSection(QStringList& lines, const QString& sectionName, + const QString& mimeType, Update update, bool createSection) +{ + int sectionStart = findSection(lines, sectionName); + if (sectionStart < 0) { + if (!createSection) { + return; + } + + if (!lines.isEmpty() && !lines.back().isEmpty()) { + lines.append(QString()); + } + sectionStart = lines.size(); + lines.append(sectionName); + } + + const int end = sectionEnd(lines, sectionStart); + for (int i = sectionStart + 1; i < end; ++i) { + const QString item = lines.at(i).trimmed(); + if (!item.startsWith(mimeType + "=")) { + continue; + } + + const QStringList updated = update(desktopFilesFromEntry(item)); + if (updated.isEmpty()) { + lines.removeAt(i); + } else { + lines[i] = desktopFilesEntry(mimeType, updated); + } + return; + } + + const QStringList updated = update(QStringList{}); + if (!updated.isEmpty()) { + lines.insert(sectionStart + 1, desktopFilesEntry(mimeType, updated)); + } +} + +void prependDesktopFile(QStringList& desktopFiles, const QString& desktopFile) +{ + desktopFiles.removeAll(desktopFile); + desktopFiles.prepend(desktopFile); +} + +void removeDesktopFile(QStringList& desktopFiles, const QString& desktopFile) +{ + desktopFiles.removeAll(desktopFile); +} + +void runDesktopCommand(const QString& program, const QStringList& arguments) +{ + const int result = QProcess::execute(program, arguments); + if (result == -2) { + log::debug("{} is not available", program); + } else if (result != 0) { + log::warn("{} exited with code {}", program, result); + } +} + +void setDefaultAssociation(const QString& mimeType, const QString& desktopFile) +{ + runDesktopCommand(QStringLiteral("xdg-mime"), + QStringList{QStringLiteral("default"), desktopFile, mimeType}); + + // Some GLib/GNOME stacks cache associations independently of direct + // mimeapps.list edits. gio is optional, so absence is only logged at debug. + runDesktopCommand(QStringLiteral("gio"), + QStringList{QStringLiteral("mime"), mimeType, desktopFile}); +} + +void refreshDesktopAssociationCaches(const QString& appsDir) +{ + runDesktopCommand(QStringLiteral("update-desktop-database"), QStringList{appsDir}); + runDesktopCommand(QStringLiteral("xdg-desktop-menu"), + QStringList{QStringLiteral("forceupdate")}); +} + // xdg-desktop-portal remembers chooser picks in its permission store. An // earlier build registered both com.fluorine.manager.desktop and // mo2-nxm-handler.desktop for nxm:// — anyone who picked Fluorine Manager // from the chooser had it persisted as their always-use app, which kept // routing nxm:// to the wrong handler (full MO2 launch with no URL) even -// after the bad MimeType was removed. Strip the stale com.fluorine.manager -// entry so existing users self-heal on next launch. +// after the bad MimeType was removed. Strip known stale app IDs so existing +// users self-heal on next launch or when they re-associate links. void clearStalePortalChoice(const QString& mimeType) { - QDBusMessage msg = QDBusMessage::createMethodCall( - "org.freedesktop.impl.portal.PermissionStore", - "/org/freedesktop/impl/portal/PermissionStore", - "org.freedesktop.impl.portal.PermissionStore", "DeletePermission"); - msg << QStringLiteral("desktop-used-apps") << mimeType - << QStringLiteral("com.fluorine.manager"); + const QStringList staleAppIds = { + QStringLiteral("com.fluorine.manager"), + QStringLiteral("mo2-nxm-handler"), + QStringLiteral("ModOrganizer"), + QStringLiteral("modorganizer"), + QStringLiteral("vortex"), + QStringLiteral("Vortex"), + QStringLiteral("com.nexusmods.vortex"), + QStringLiteral("nexusmods-vortex"), + }; - // Fire-and-forget on the session bus. The reply is uninteresting: a missing - // entry returns an error and we don't want to log on every clean startup. - QDBusConnection::sessionBus().call(msg, QDBus::NoBlock); + for (const auto& appId : staleAppIds) { + QDBusMessage msg = QDBusMessage::createMethodCall( + "org.freedesktop.impl.portal.PermissionStore", + "/org/freedesktop/impl/portal/PermissionStore", + "org.freedesktop.impl.portal.PermissionStore", "DeletePermission"); + msg << QStringLiteral("desktop-used-apps") << mimeType << appId; + + // Fire-and-forget on the session bus. The reply is uninteresting: a missing + // entry returns an error and we don't want to log on every clean startup. + QDBusConnection::sessionBus().call(msg, QDBus::NoBlock); + } } void updateMimeAppsList(const QString& path, const QString& mimeType, const QString& desktopFile) { - const QString entry = mimeType + "=" + desktopFile; + QStringList lines = readMimeAppsList(path); - QStringList lines; - { - QFile file(path); - if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { - const auto content = QString::fromUtf8(file.readAll()); - lines = content.split('\n'); - while (!lines.isEmpty() && lines.back().isEmpty()) { - lines.removeLast(); - } - } - } + updateMimeSection( + lines, QStringLiteral("[Default Applications]"), mimeType, + [&](QStringList) { + return QStringList{desktopFile}; + }, + true); - int defaultHeader = -1; - bool replaced = false; + updateMimeSection( + lines, QStringLiteral("[Added Associations]"), mimeType, + [&](QStringList desktopFiles) { + removeDesktopFile(desktopFiles, LegacyNxmDesktopFile); + prependDesktopFile(desktopFiles, desktopFile); + return desktopFiles; + }, + true); - for (int i = 0; i <= lines.size(); ++i) { - const bool atEnd = i == lines.size(); - const QString trimmed = - atEnd ? QString() : lines.at(i).trimmed(); + updateMimeSection( + lines, QStringLiteral("[Removed Associations]"), mimeType, + [&](QStringList desktopFiles) { + removeDesktopFile(desktopFiles, desktopFile); + removeDesktopFile(desktopFiles, LegacyNxmDesktopFile); + return desktopFiles; + }, + false); - if (!atEnd && trimmed.startsWith('[')) { - if (trimmed == "[Default Applications]") { - defaultHeader = i; - } - continue; - } + writeTextFile(path, lines.join('\n') + "\n"); +} - if (defaultHeader >= 0 && (atEnd || trimmed.startsWith('['))) { - const int sectionEnd = atEnd ? lines.size() : i; - for (int j = defaultHeader + 1; j < sectionEnd; ++j) { - const QString item = lines.at(j).trimmed(); - if (item.startsWith(mimeType + "=")) { - lines[j] = entry; - replaced = true; - break; - } - } - if (!replaced) { - lines.insert(defaultHeader + 1, entry); - } - break; - } - } +void removeMimeAppsAssociation(const QString& path, const QString& mimeType, + const QString& desktopFile) +{ + QStringList lines = readMimeAppsList(path); - if (defaultHeader < 0) { - if (!lines.isEmpty() && !lines.back().isEmpty()) { - lines.append(QString()); - } - lines.append("[Default Applications]"); - lines.append(entry); - } + updateMimeSection( + lines, QStringLiteral("[Default Applications]"), mimeType, + [&](QStringList desktopFiles) { + removeDesktopFile(desktopFiles, desktopFile); + removeDesktopFile(desktopFiles, LegacyNxmDesktopFile); + return desktopFiles; + }, + false); + + updateMimeSection( + lines, QStringLiteral("[Added Associations]"), mimeType, + [&](QStringList desktopFiles) { + removeDesktopFile(desktopFiles, desktopFile); + removeDesktopFile(desktopFiles, LegacyNxmDesktopFile); + return desktopFiles; + }, + false); + + updateMimeSection( + lines, QStringLiteral("[Removed Associations]"), mimeType, + [&](QStringList desktopFiles) { + prependDesktopFile(desktopFiles, desktopFile); + prependDesktopFile(desktopFiles, LegacyNxmDesktopFile); + return desktopFiles; + }, + true); writeTextFile(path, lines.join('\n') + "\n"); } @@ -241,10 +402,10 @@ void NxmHandlerLinux::registerHandler() // browser or desktop environment invokes the URL scheme handler. const QString execLine = wrapperPath + " nxm-handle %u"; - const QString desktopPath = appsDir + "/mo2-nxm-handler.desktop"; + const QString desktopPath = appsDir + "/" + NxmDesktopFile; const QString desktop = QString("[Desktop Entry]\n" "Type=Application\n" - "Name=Mod Organizer 2 NXM Handler\n" + "Name=Fluorine Manager NXM Handler\n" "Exec=%1\n" "MimeType=x-scheme-handler/nxm;x-scheme-handler/modl;\n" "NoDisplay=true\n").arg(execLine); @@ -254,23 +415,50 @@ void NxmHandlerLinux::registerHandler() return; } - updateMimeAppsList(configDir + "/mimeapps.list", "x-scheme-handler/nxm", - "mo2-nxm-handler.desktop"); - updateMimeAppsList(appsDir + "/mimeapps.list", "x-scheme-handler/nxm", - "mo2-nxm-handler.desktop"); - updateMimeAppsList(configDir + "/mimeapps.list", "x-scheme-handler/modl", - "mo2-nxm-handler.desktop"); - updateMimeAppsList(appsDir + "/mimeapps.list", "x-scheme-handler/modl", - "mo2-nxm-handler.desktop"); + QFile::remove(appsDir + "/" + LegacyNxmDesktopFile); + + for (const auto& scheme : UrlSchemes) { + updateMimeAppsList(configDir + "/mimeapps.list", scheme, NxmDesktopFile); + updateMimeAppsList(appsDir + "/mimeapps.list", scheme, NxmDesktopFile); + setDefaultAssociation(scheme, NxmDesktopFile); + } + + refreshDesktopAssociationCaches(appsDir); + + for (const auto& scheme : UrlSchemes) { + clearStalePortalChoice(scheme); + } +} + +void NxmHandlerLinux::unregisterHandler() +{ + const QString home = QDir::homePath(); + if (home.isEmpty()) { + log::error("cannot remove nxm handler: home path is empty"); + return; + } + + const QString appsDir = ensureDir(home + "/.local/share/applications"); + const QString configDir = ensureDir(home + "/.config"); - const auto result = - QProcess::execute("update-desktop-database", QStringList() << appsDir); - if (result != 0) { - log::warn("update-desktop-database exited with code {}", result); + if (appsDir.isEmpty() || configDir.isEmpty()) { + log::error("cannot remove nxm handler: failed to create required directories"); + return; + } + + for (const auto& scheme : UrlSchemes) { + removeMimeAppsAssociation(configDir + "/mimeapps.list", scheme, NxmDesktopFile); + removeMimeAppsAssociation(appsDir + "/mimeapps.list", scheme, NxmDesktopFile); } - clearStalePortalChoice("x-scheme-handler/nxm"); - clearStalePortalChoice("x-scheme-handler/modl"); + QFile::remove(appsDir + "/" + NxmDesktopFile); + QFile::remove(appsDir + "/" + LegacyNxmDesktopFile); + + refreshDesktopAssociationCaches(appsDir); + + for (const auto& scheme : UrlSchemes) { + clearStalePortalChoice(scheme); + } } bool NxmHandlerLinux::startListener() diff --git a/src/src/nxmhandler_linux.h b/src/src/nxmhandler_linux.h index ab58165..26e2361 100644 --- a/src/src/nxmhandler_linux.h +++ b/src/src/nxmhandler_linux.h @@ -33,7 +33,8 @@ public: explicit NxmHandlerLinux(QObject* parent = nullptr); ~NxmHandlerLinux() override; - static void registerHandler() ; + static void registerHandler(); + static void unregisterHandler(); bool startListener(); static bool sendToSocket(const QString& url); diff --git a/src/src/settings.cpp b/src/src/settings.cpp index 6cea57f..7f8a447 100644 --- a/src/src/settings.cpp +++ b/src/src/settings.cpp @@ -2002,10 +2002,14 @@ void NexusSettings::setCategoryMappings(bool b) const void NexusSettings::registerAsNXMHandler(bool force) { Q_UNUSED(force); - NxmHandlerLinux handler; NxmHandlerLinux::registerHandler(); } +void NexusSettings::unregisterAsNXMHandler() +{ + NxmHandlerLinux::unregisterHandler(); +} + std::vector<std::chrono::seconds> NexusSettings::validationTimeouts() const { using namespace std::chrono_literals; diff --git a/src/src/settings.h b/src/src/settings.h index 92f71fa..9c4023e 100644 --- a/src/src/settings.h +++ b/src/src/settings.h @@ -527,6 +527,7 @@ public: // said earlier not to // static void registerAsNXMHandler(bool force); + static void unregisterAsNXMHandler(); std::vector<std::chrono::seconds> validationTimeouts() const; diff --git a/src/src/settingsdialog.ui b/src/src/settingsdialog.ui index 50c47e0..fe57b49 100644 --- a/src/src/settingsdialog.ui +++ b/src/src/settingsdialog.ui @@ -1411,11 +1411,22 @@ If you disable this feature, MO will only display official DLCs this way. Please <number>0</number> </property> <item> - <widget class="QPushButton" name="associateButton"> - <property name="text"> - <string>Associate with "Download with manager" links</string> - </property> - </widget> + <layout class="QHBoxLayout" name="nxmAssociationLayout"> + <item> + <widget class="QPushButton" name="associateButton"> + <property name="text"> + <string>Associate NXM Links</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="removeAssociationButton"> + <property name="text"> + <string>Remove NXM Association</string> + </property> + </widget> + </item> + </layout> </item> <item> <widget class="QPushButton" name="clearCacheButton"> diff --git a/src/src/settingsdialognexus.cpp b/src/src/settingsdialognexus.cpp index d4008be..bbc9b93 100644 --- a/src/src/settingsdialognexus.cpp +++ b/src/src/settingsdialognexus.cpp @@ -393,6 +393,9 @@ NexusSettingsTab::NexusSettingsTab(Settings& s, SettingsDialog& d) : SettingsTab QObject::connect(ui->associateButton, &QPushButton::clicked, [&] { associate(); }); + QObject::connect(ui->removeAssociationButton, &QPushButton::clicked, [&] { + removeAssociation(); + }); updateNexusData(); } @@ -462,6 +465,11 @@ void NexusSettingsTab::associate() Settings::instance().nexus().registerAsNXMHandler(true); } +void NexusSettingsTab::removeAssociation() +{ + Settings::instance().nexus().unregisterAsNXMHandler(); +} + void NexusSettingsTab::updateNexusData() { const auto user = NexusInterface::instance().getAPIUserAccount(); @@ -486,4 +494,3 @@ void NexusSettingsTab::updateNexusData() ui->nexusHourlyRequests->setText(QObject::tr("N/A")); } } - diff --git a/src/src/settingsdialognexus.h b/src/src/settingsdialognexus.h index e43a318..187ff79 100644 --- a/src/src/settingsdialognexus.h +++ b/src/src/settingsdialognexus.h @@ -69,6 +69,7 @@ private: void clearCache(); void associate(); + void removeAssociation(); void updateNexusData(); }; |
