From cb7c334b404a0a73f1709295cb70b0e0945cd1be Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Sat, 7 Mar 2026 19:50:52 -0600 Subject: Fix LOOT sort copy-back, add Linux desktop shortcuts, remove Start Menu - Fix LOOT sorted order not applying: prefer Plugins.txt (capital P) over our pre-deployed plugins.txt (lowercase) since LOOT writes capital P on Linux case-sensitive FS. Same for Loadorder.txt. - Force plugin list refresh after LOOT sort (refreshESPList(true)) - Remove Start Menu shortcut option on Linux (no-op, Windows-only concept) - Implement .desktop file creation for Desktop shortcuts on Linux: - Use $APPIMAGE env var for stable executable path (not ephemeral mount) - Quote paths with spaces in Exec line - Use absolute instance directory path for portable instances - Install Fluorine icon to ~/.local/share/icons/ for persistence - Skip .exe icons (can't render on Linux) - Fix moshortcut:// instance matching for portable instances using abs paths Co-Authored-By: Claude Opus 4.6 --- src/src/envshortcut.cpp | 158 ++++++++++++++++++++++++++++++++++++++++----- src/src/loot.cpp | 35 ++++++---- src/src/mainwindow.cpp | 10 ++- src/src/moshortcut.cpp | 12 +++- src/src/pluginlistview.cpp | 2 +- 5 files changed, 182 insertions(+), 35 deletions(-) (limited to 'src') diff --git a/src/src/envshortcut.cpp b/src/src/envshortcut.cpp index 1bb39f6..49a122b 100644 --- a/src/src/envshortcut.cpp +++ b/src/src/envshortcut.cpp @@ -155,7 +155,7 @@ Shortcut::Shortcut(const Executable& exe) : Shortcut() Shortcut& Shortcut::name(const QString& s) { - m_name = MOBase::sanitizeFileName(s); + m_name = s; return *this; } @@ -353,16 +353,76 @@ QString toString(Shortcut::Locations loc) #include #include #include +#include +#include #include +#include -class Executable; +#include "executableslist.h" +#include "instancemanager.h" namespace env { +// Returns the path to the AppImage file itself, or falls back to the +// running binary if not running from an AppImage. +static QString appImageOrBinary() +{ + QString appImage = QProcessEnvironment::systemEnvironment().value("APPIMAGE"); + if (!appImage.isEmpty() && QFile::exists(appImage)) { + return appImage; + } + return QFileInfo(qApp->applicationFilePath()).absoluteFilePath(); +} + Shortcut::Shortcut() : m_iconIndex(0) {} -Shortcut::Shortcut(const Executable&) : Shortcut() {} +Shortcut::Shortcut(const Executable& exe) : Shortcut() +{ + const auto i = *InstanceManager::singleton().currentInstance(); + + m_name = exe.title(); + m_target = appImageOrBinary(); + + // For portable instances, use the absolute directory path so MO2 can + // find it (line 595 in instancemanager.cpp handles abs path lookup). + // For global instances, use the display name. + QString instanceId = i.isPortable() ? QDir(i.directory()).absolutePath() + : i.displayName(); + m_arguments = QString("\"moshortcut://%1:%2\"") + .arg(instanceId) + .arg(exe.title()); + + m_description = QString("Run %1 with Fluorine").arg(exe.title()); + + // .exe icons can't be used on Linux — only use native icon formats. + // Fall back to the Fluorine app icon bundled in the AppImage. + if (exe.usesOwnIcon()) { + QString iconPath = exe.binaryInfo().absoluteFilePath(); + if (!iconPath.endsWith(".exe", Qt::CaseInsensitive)) { + m_icon = iconPath; + } + } + if (m_icon.isEmpty()) { + // Install the Fluorine icon to the user's icon theme so it persists + // after the AppImage unmounts, then reference it by icon name. + QString appDir = QProcessEnvironment::systemEnvironment().value("APPDIR"); + if (!appDir.isEmpty()) { + QString bundled = appDir + "/usr/share/icons/hicolor/256x256/apps/com.fluorine.manager.png"; + if (QFile::exists(bundled)) { + QString destDir = QDir::homePath() + "/.local/share/icons/hicolor/256x256/apps"; + QString dest = destDir + "/com.fluorine.manager.png"; + if (!QFile::exists(dest)) { + QDir().mkpath(destDir); + QFile::copy(bundled, dest); + } + } + } + m_icon = "com.fluorine.manager"; + } + + m_workingDirectory = QFileInfo(m_target).absolutePath(); +} Shortcut& Shortcut::name(const QString& s) { @@ -401,41 +461,105 @@ Shortcut& Shortcut::workingDirectory(const QString& s) return *this; } -bool Shortcut::exists(Locations) const +bool Shortcut::exists(Locations loc) const { - // .lnk shortcuts are a Windows concept; no-op on Linux - return false; + return QFile::exists(shortcutPath(loc)); } -bool Shortcut::toggle(Locations) +bool Shortcut::toggle(Locations loc) { - return false; + if (exists(loc)) { + return remove(loc); + } else { + return add(loc); + } } -bool Shortcut::add(Locations) +bool Shortcut::add(Locations loc) { - // TODO: could create .desktop files on Linux in the future - return false; + if (loc != Desktop) { + return false; + } + + const QString path = shortcutPath(loc); + if (path.isEmpty()) { + return false; + } + + QDir().mkpath(QFileInfo(path).absolutePath()); + + QFile file(path); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + return false; + } + + QTextStream out(&file); + out << "[Desktop Entry]\n"; + out << "Type=Application\n"; + out << "Name=" << (m_name.isEmpty() ? "Fluorine" : m_name) << "\n"; + if (!m_description.isEmpty()) { + out << "Comment=" << m_description << "\n"; + } + // .desktop Exec values require quoting paths that contain spaces + out << "Exec=\"" << m_target << "\""; + if (!m_arguments.isEmpty()) { + out << " " << m_arguments; + } + out << "\n"; + if (!m_workingDirectory.isEmpty()) { + out << "Path=" << m_workingDirectory << "\n"; + } + if (!m_icon.isEmpty()) { + out << "Icon=" << m_icon << "\n"; + } + out << "Terminal=false\n"; + out << "Categories=Game;\n"; + + file.close(); + + // Make it executable (required by some desktop environments) + file.setPermissions(file.permissions() | QFileDevice::ExeUser); + + return true; } -bool Shortcut::remove(Locations) +bool Shortcut::remove(Locations loc) { - return false; + const QString path = shortcutPath(loc); + if (path.isEmpty()) { + return false; + } + return QFile::remove(path); } -QString Shortcut::shortcutPath(Locations) const +QString Shortcut::shortcutPath(Locations loc) const { - return {}; + const QString dir = shortcutDirectory(loc); + if (dir.isEmpty()) { + return {}; + } + return dir + "/" + shortcutFilename(); } -QString Shortcut::shortcutDirectory(Locations) const +QString Shortcut::shortcutDirectory(Locations loc) const { + if (loc == Desktop) { + // XDG desktop directory + QString desktop = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); + if (!desktop.isEmpty()) { + return desktop; + } + return QDir::homePath() + "/Desktop"; + } return {}; } QString Shortcut::shortcutFilename() const { - return {}; + if (m_name.isEmpty()) { + return "fluorine.desktop"; + } + return m_name + ".desktop"; } QString toString(Shortcut::Locations loc) diff --git a/src/src/loot.cpp b/src/src/loot.cpp index 5e796df..c96a26c 100644 --- a/src/src/loot.cpp +++ b/src/src/loot.cpp @@ -1496,30 +1496,37 @@ static bool launchLootGui(QWidget* parent, OrganizerCore& core) bool isFileTime = core.managedGame()->loadOrderMechanism() == MOBase::IPluginGame::LoadOrderMechanism::FileTime; - // LOOT may write "Plugins.txt" (capital P, Windows convention) instead of - // "plugins.txt", so check both case variants on case-sensitive Linux. - QString lootPluginsActual = lootPlugins; - if (!QFile::exists(lootPluginsActual)) { - QString alt = localPath + "/Plugins.txt"; - if (QFile::exists(alt)) { - lootPluginsActual = alt; + // LOOT may write "Plugins.txt" (capital P) while we deployed "plugins.txt" + // (lowercase). On case-sensitive Linux these are two different files — the + // lowercase one is our pre-LOOT copy, the capital-P one is LOOT's output. + // Prefer the capital-P variant when it exists; otherwise fall back to the + // lowercase file (in case a future LOOT version writes lowercase). + QString lootPluginsActual; + { + QString capitalP = localPath + "/Plugins.txt"; + if (QFile::exists(capitalP)) { + lootPluginsActual = capitalP; + } else if (QFile::exists(lootPlugins)) { + lootPluginsActual = lootPlugins; } } - if (QFile::exists(lootPluginsActual)) { + if (!lootPluginsActual.isEmpty()) { QFile::remove(profilePlugins); QFile::copy(lootPluginsActual, profilePlugins); log::info("Copied LOOT {} back to profile", lootPluginsActual); } if (!isFileTime) { - QString lootLoadOrderActual = lootLoadOrder; - if (!QFile::exists(lootLoadOrderActual)) { - QString alt = localPath + "/Loadorder.txt"; - if (QFile::exists(alt)) { - lootLoadOrderActual = alt; + QString lootLoadOrderActual; + { + QString capitalL = localPath + "/Loadorder.txt"; + if (QFile::exists(capitalL)) { + lootLoadOrderActual = capitalL; + } else if (QFile::exists(lootLoadOrder)) { + lootLoadOrderActual = lootLoadOrder; } } - if (QFile::exists(lootLoadOrderActual)) { + if (!lootLoadOrderActual.isEmpty()) { QFile::remove(profileLoadOrder); QFile::copy(lootLoadOrderActual, profileLoadOrder); log::info("Copied LOOT {} back to profile", lootLoadOrderActual); diff --git a/src/src/mainwindow.cpp b/src/src/mainwindow.cpp index 1eb5e36..e94939f 100644 --- a/src/src/mainwindow.cpp +++ b/src/src/mainwindow.cpp @@ -372,8 +372,12 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore, this, SLOT(linkToolbar())); m_LinkDesktop = linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Desktop"), this, SLOT(linkDesktop())); +#ifdef _WIN32 m_LinkStartMenu = linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Start Menu"), this, SLOT(linkMenu())); +#else + m_LinkStartMenu = nullptr; +#endif ui->linkButton->setMenu(linkMenu); ui->listOptionsBtn->setMenu( @@ -2823,8 +2827,10 @@ void MainWindow::on_linkButton_pressed() m_LinkDesktop->setIcon(shortcut.exists(env::Shortcut::Desktop) ? removeIcon : addIcon); - m_LinkStartMenu->setIcon(shortcut.exists(env::Shortcut::StartMenu) ? removeIcon - : addIcon); + if (m_LinkStartMenu) { + m_LinkStartMenu->setIcon(shortcut.exists(env::Shortcut::StartMenu) ? removeIcon + : addIcon); + } } void MainWindow::on_actionSettings_triggered() diff --git a/src/src/moshortcut.cpp b/src/src/moshortcut.cpp index 2dc5c05..0172f50 100644 --- a/src/src/moshortcut.cpp +++ b/src/src/moshortcut.cpp @@ -1,5 +1,7 @@ #include "moshortcut.h" +#include + MOShortcut::MOShortcut(const QString& link) : m_valid(link.startsWith("moshortcut://")), m_hasInstance(false), m_hasExecutable(false) @@ -59,7 +61,15 @@ bool MOShortcut::isForInstance(const Instance& i) const // empty instance name means portable return i.isPortable(); } else { - return (i.displayName() == m_instance); + // Compare by display name, or by absolute directory path for portable + // instances identified by their filesystem path. + if (i.displayName() == m_instance) { + return true; + } + if (QDir::isAbsolutePath(m_instance) && i.isPortable()) { + return QDir(i.directory()).absolutePath() == QDir(m_instance).absolutePath(); + } + return false; } } diff --git a/src/src/pluginlistview.cpp b/src/src/pluginlistview.cpp index 58b0548..225c3bc 100644 --- a/src/src/pluginlistview.cpp +++ b/src/src/pluginlistview.cpp @@ -216,7 +216,7 @@ void PluginListView::onSortButtonClicked() m_didUpdateMasterList = true; } - m_core->refreshESPList(false); + m_core->refreshESPList(true); m_core->savePluginList(); } } -- cgit v1.3.1