diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-03-08 15:52:56 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-03-08 15:52:56 -0500 |
| commit | 630c3221bf8a6a6d3072ca7fef7d47210e9c9205 (patch) | |
| tree | b229d5cfb2556feb2488dc805ee8ef82eb4634ae /src | |
| parent | 2f3efb9100f51833f12f244e993155b85fa59621 (diff) | |
Fix LOOT sort for PluginsTxt games, improve desktop shortcuts, add Browse VFS
LOOT sort fix: For PluginsTxt games (FO4, SkyrimSE), LOOT only modifies
plugins.txt, not loadorder.txt. Previously the stale loadorder.txt was
copied back, causing readPluginLists() to use the old order. Now we
regenerate loadorder.txt from the sorted plugins.txt.
Desktop shortcuts: Add "Application Launcher" option on Linux that creates
.desktop entries in ~/.local/share/applications/. Include instance name in
both filename (New-Vegas-NVSE.desktop) and Name= field (NVSE (New Vegas))
to avoid collisions. Icons saved to ~/.local/share/icons/fluorine/ with
game-specific names.
Browse VFS: Add button in Data tab that mounts the FUSE VFS and opens the
game Data folder in the system file manager, allowing users to browse the
merged mod files as the game sees them.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/src/datatab.cpp | 42 | ||||
| -rw-r--r-- | src/src/datatab.h | 3 | ||||
| -rw-r--r-- | src/src/envshortcut.cpp | 106 | ||||
| -rw-r--r-- | src/src/envshortcut.h | 8 | ||||
| -rw-r--r-- | src/src/loot.cpp | 43 | ||||
| -rw-r--r-- | src/src/mainwindow.cpp | 14 | ||||
| -rw-r--r-- | src/src/mainwindow.ui | 10 |
7 files changed, 201 insertions, 25 deletions
diff --git a/src/src/datatab.cpp b/src/src/datatab.cpp index ea1430b..e55c417 100644 --- a/src/src/datatab.cpp +++ b/src/src/datatab.cpp @@ -9,6 +9,9 @@ #include <log.h>
#include <report.h>
+#include <QMessageBox>
+#include <utility.h>
+
using namespace MOShared;
using namespace MOBase;
@@ -21,6 +24,7 @@ DataTab::DataTab(OrganizerCore& core, PluginContainer& pc, QWidget* parent, ui{mwui->tabWidget,
mwui->dataTab,
mwui->dataTabRefresh,
+ mwui->dataTabBrowseVFS,
mwui->dataTree,
mwui->dataTabShowOnlyConflicts,
mwui->dataTabShowFromArchives,
@@ -42,6 +46,14 @@ DataTab::DataTab(OrganizerCore& core, PluginContainer& pc, QWidget* parent, ensureFullyLoaded();
});
+#ifdef _WIN32
+ ui.browseVFS->setVisible(false);
+#else
+ connect(ui.browseVFS, &QPushButton::clicked, [&] {
+ onBrowseVFS();
+ });
+#endif
+
connect(ui.refresh, &QPushButton::clicked, [&] {
onRefresh();
});
@@ -123,6 +135,36 @@ void DataTab::onRefresh() m_core.refreshDirectoryStructure();
}
+void DataTab::onBrowseVFS()
+{
+#ifndef _WIN32
+ QString dataPath = m_core.managedGame()->dataDirectory().absolutePath();
+
+ // Mount the FUSE VFS so the file manager sees the merged mod files.
+ log::info("Mounting VFS for Browse...");
+ m_core.prepareVFS();
+
+ // Open the game data folder in the system file manager.
+ shell::Explore(dataPath);
+
+ // Show a modal dialog that keeps the VFS mounted. When the user
+ // closes it, we unmount.
+ QMessageBox box(QMessageBox::Information,
+ QObject::tr("Browse VFS"),
+ QObject::tr("The virtual filesystem is mounted.\n\n"
+ "The game Data folder has been opened in your "
+ "file manager. You can browse the merged mod "
+ "files as the game would see them.\n\n"
+ "Close this dialog to unmount the VFS."),
+ QMessageBox::Close, m_parent);
+ box.setWindowModality(Qt::WindowModal);
+ box.exec();
+
+ log::info("Unmounting VFS after Browse...");
+ m_core.unmountVFS();
+#endif
+}
+
void DataTab::updateTree()
{
if (isActive()) {
diff --git a/src/src/datatab.h b/src/src/datatab.h index 35f8bd9..cf5bc99 100644 --- a/src/src/datatab.h +++ b/src/src/datatab.h @@ -4,6 +4,7 @@ #include "modinfo.h"
#include "modinfodialogfwd.h"
#include <QCheckBox>
+#include <QMessageBox>
#include <QPushButton>
#include <QTreeWidget>
#include <filterwidget.h>
@@ -51,6 +52,7 @@ private: QTabWidget* tabs;
QWidget* tab;
QPushButton* refresh;
+ QPushButton* browseVFS;
QTreeView* tree;
QCheckBox* conflicts;
QCheckBox* archives;
@@ -67,6 +69,7 @@ private: bool m_needUpdate;
void onRefresh();
+ void onBrowseVFS();
void onItemExpanded(QTreeWidgetItem* item);
void onConflicts();
void onArchives();
diff --git a/src/src/envshortcut.cpp b/src/src/envshortcut.cpp index 49a122b..55600fe 100644 --- a/src/src/envshortcut.cpp +++ b/src/src/envshortcut.cpp @@ -341,6 +341,9 @@ QString toString(Shortcut::Locations loc) case Shortcut::StartMenu:
return "start menu";
+ case Shortcut::ApplicationMenu:
+ return "application menu";
+
default:
return QString("? (%1)").arg(static_cast<int>(loc));
}
@@ -377,12 +380,62 @@ static QString appImageOrBinary() Shortcut::Shortcut() : m_iconIndex(0) {}
+// Sanitize a string for use in a .desktop filename (replace spaces/special chars).
+static QString sanitizeDesktopName(const QString& s)
+{
+ QString result;
+ for (const QChar& c : s) {
+ if (c.isLetterOrNumber() || c == '-' || c == '_' || c == '.') {
+ result += c;
+ } else if (c == ' ') {
+ result += '-';
+ }
+ }
+ return result;
+}
+
+// Install a game-specific icon to ~/.local/share/icons/fluorine/ and return
+// the absolute path. Falls back to the bundled Fluorine icon.
+static QString installIcon(const QString& iconBaseName)
+{
+ QString iconDir = QDir::homePath() + "/.local/share/icons/fluorine";
+ QString iconDest = iconDir + "/" + iconBaseName + ".png";
+
+ if (QFile::exists(iconDest)) {
+ return iconDest;
+ }
+
+ // No game-specific icon available — install the bundled Fluorine icon
+ // under the game-specific name so each shortcut gets its own file.
+ QDir().mkpath(iconDir);
+
+ 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)) {
+ QFile::copy(bundled, iconDest);
+ return iconDest;
+ }
+ }
+
+ // Also keep the hicolor copy for compatibility.
+ QString hicolorDir = QDir::homePath() + "/.local/share/icons/hicolor/256x256/apps";
+ QString hicolorDest = hicolorDir + "/com.fluorine.manager.png";
+ if (QFile::exists(hicolorDest)) {
+ QFile::copy(hicolorDest, iconDest);
+ return iconDest;
+ }
+
+ return "com.fluorine.manager";
+}
+
Shortcut::Shortcut(const Executable& exe) : Shortcut()
{
const auto i = *InstanceManager::singleton().currentInstance();
- m_name = exe.title();
- m_target = appImageOrBinary();
+ m_name = exe.title();
+ m_instanceName = i.displayName();
+ 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).
@@ -396,7 +449,7 @@ Shortcut::Shortcut(const Executable& exe) : Shortcut() 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.
+ // Install a game-specific icon to ~/.local/share/icons/fluorine/.
if (exe.usesOwnIcon()) {
QString iconPath = exe.binaryInfo().absoluteFilePath();
if (!iconPath.endsWith(".exe", Qt::CaseInsensitive)) {
@@ -404,21 +457,11 @@ Shortcut::Shortcut(const Executable& exe) : Shortcut() }
}
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";
+ // Build a unique icon name from the instance + executable title,
+ // e.g. "NewVegas-NVSE" → ~/.local/share/icons/fluorine/NewVegas-NVSE.png
+ QString iconBase = sanitizeDesktopName(m_instanceName) + "-" +
+ sanitizeDesktopName(m_name);
+ m_icon = installIcon(iconBase);
}
m_workingDirectory = QFileInfo(m_target).absolutePath();
@@ -477,7 +520,7 @@ bool Shortcut::toggle(Locations loc) bool Shortcut::add(Locations loc)
{
- if (loc != Desktop) {
+ if (loc != Desktop && loc != ApplicationMenu) {
return false;
}
@@ -493,10 +536,18 @@ bool Shortcut::add(Locations loc) return false;
}
+ // Include instance name in the display name to distinguish shortcuts
+ // for the same executable across different instances (e.g. "NVSE (New Vegas)"
+ // vs "NVSE (New Vegas 2)").
+ QString displayName = m_name.isEmpty() ? "Fluorine" : m_name;
+ if (!m_instanceName.isEmpty()) {
+ displayName += " (" + m_instanceName + ")";
+ }
+
QTextStream out(&file);
out << "[Desktop Entry]\n";
out << "Type=Application\n";
- out << "Name=" << (m_name.isEmpty() ? "Fluorine" : m_name) << "\n";
+ out << "Name=" << displayName << "\n";
if (!m_description.isEmpty()) {
out << "Comment=" << m_description << "\n";
}
@@ -551,6 +602,9 @@ QString Shortcut::shortcutDirectory(Locations loc) const }
return QDir::homePath() + "/Desktop";
}
+ if (loc == ApplicationMenu) {
+ return QDir::homePath() + "/.local/share/applications";
+ }
return {};
}
@@ -559,7 +613,14 @@ QString Shortcut::shortcutFilename() const if (m_name.isEmpty()) {
return "fluorine.desktop";
}
- return m_name + ".desktop";
+ // Include instance name in the filename to avoid collisions when
+ // multiple instances have the same executable (e.g. BodySlide_x64
+ // for both FNV and SkyrimSE). Example: "New Vegas-NVSE.desktop"
+ QString base = m_name;
+ if (!m_instanceName.isEmpty()) {
+ base = sanitizeDesktopName(m_instanceName) + "-" + base;
+ }
+ return base + ".desktop";
}
QString toString(Shortcut::Locations loc)
@@ -574,6 +635,9 @@ QString toString(Shortcut::Locations loc) case Shortcut::StartMenu:
return "start menu";
+ case Shortcut::ApplicationMenu:
+ return "application menu";
+
default:
return QString("? (%1)").arg(static_cast<int>(loc));
}
diff --git a/src/src/envshortcut.h b/src/src/envshortcut.h index 43830b4..2048d7f 100644 --- a/src/src/envshortcut.h +++ b/src/src/envshortcut.h @@ -22,8 +22,11 @@ public: // on the desktop
Desktop,
- // in the start menu
- StartMenu
+ // in the start menu (Windows only)
+ StartMenu,
+
+ // in ~/.local/share/applications/ (Linux only)
+ ApplicationMenu
};
// empty shortcut
@@ -78,6 +81,7 @@ public: private:
QString m_name;
+ QString m_instanceName;
QString m_target;
QString m_arguments;
QString m_description;
diff --git a/src/src/loot.cpp b/src/src/loot.cpp index c96a26c..00c66e2 100644 --- a/src/src/loot.cpp +++ b/src/src/loot.cpp @@ -1516,7 +1516,12 @@ static bool launchLootGui(QWidget* parent, OrganizerCore& core) log::info("Copied LOOT {} back to profile", lootPluginsActual);
}
- if (!isFileTime) {
+ bool isPluginsTxt = core.managedGame()->loadOrderMechanism() ==
+ MOBase::IPluginGame::LoadOrderMechanism::PluginsTxt;
+
+ if (!isFileTime && !isPluginsTxt) {
+ // For games that use a separate loadorder.txt (not FileTime, not
+ // PluginsTxt), copy LOOT's loadorder.txt back.
QString lootLoadOrderActual;
{
QString capitalL = localPath + "/Loadorder.txt";
@@ -1533,6 +1538,42 @@ static bool launchLootGui(QWidget* parent, OrganizerCore& core) }
}
+ if (isPluginsTxt && !lootPluginsActual.isEmpty()) {
+ // For PluginsTxt games (FO4, SkyrimSE, etc.), LOOT writes the sorted
+ // order into plugins.txt but does NOT update loadorder.txt. If we
+ // blindly copy the stale loadorder.txt back, readPluginLists() will
+ // prefer the old order from loadorder.txt over the freshly-sorted
+ // plugins.txt. Fix this by regenerating loadorder.txt from the
+ // sorted plugins.txt so both files are consistent.
+ QFile pluginsFile(profilePlugins);
+ if (pluginsFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ QStringList sortedOrder;
+ while (!pluginsFile.atEnd()) {
+ QString line = QString::fromUtf8(pluginsFile.readLine()).trimmed();
+ if (line.isEmpty() || line.startsWith('#'))
+ continue;
+ if (line.startsWith('*'))
+ line.remove(0, 1);
+ if (!line.isEmpty())
+ sortedOrder.append(line);
+ }
+ pluginsFile.close();
+
+ if (!sortedOrder.isEmpty()) {
+ QFile lo(profileLoadOrder);
+ if (lo.open(QIODevice::WriteOnly | QIODevice::Text)) {
+ QTextStream out(&lo);
+ for (const auto& p : sortedOrder) {
+ out << p << "\n";
+ }
+ lo.close();
+ log::info("Regenerated loadorder.txt from LOOT-sorted plugins.txt "
+ "({} plugins)", sortedOrder.size());
+ }
+ }
+ }
+ }
+
return true;
}
#endif
diff --git a/src/src/mainwindow.cpp b/src/src/mainwindow.cpp index e94939f..a01b1c4 100644 --- a/src/src/mainwindow.cpp +++ b/src/src/mainwindow.cpp @@ -376,7 +376,9 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore, m_LinkStartMenu = linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Start Menu"), this,
SLOT(linkMenu()));
#else
- m_LinkStartMenu = nullptr;
+ m_LinkStartMenu = linkMenu->addAction(QIcon(":/MO/gui/link"),
+ tr("Application Launcher"), this,
+ SLOT(linkMenu()));
#endif
ui->linkButton->setMenu(linkMenu);
@@ -2806,7 +2808,11 @@ void MainWindow::linkDesktop() void MainWindow::linkMenu()
{
if (auto* exe = getSelectedExecutable()) {
+#ifdef _WIN32
env::Shortcut(*exe).toggle(env::Shortcut::StartMenu);
+#else
+ env::Shortcut(*exe).toggle(env::Shortcut::ApplicationMenu);
+#endif
}
}
@@ -2828,8 +2834,14 @@ void MainWindow::on_linkButton_pressed() : addIcon);
if (m_LinkStartMenu) {
+#ifdef _WIN32
m_LinkStartMenu->setIcon(shortcut.exists(env::Shortcut::StartMenu) ? removeIcon
: addIcon);
+#else
+ m_LinkStartMenu->setIcon(shortcut.exists(env::Shortcut::ApplicationMenu)
+ ? removeIcon
+ : addIcon);
+#endif
}
}
diff --git a/src/src/mainwindow.ui b/src/src/mainwindow.ui index 0e8e2bf..c84bb41 100644 --- a/src/src/mainwindow.ui +++ b/src/src/mainwindow.ui @@ -1109,6 +1109,16 @@ </widget> </item> <item> + <widget class="QPushButton" name="dataTabBrowseVFS"> + <property name="toolTip"> + <string>Mount the virtual filesystem and open the game Data folder in a file manager.</string> + </property> + <property name="text"> + <string>Browse VFS</string> + </property> + </widget> + </item> + <item> <spacer name="horizontalSpacer_4"> <property name="orientation"> <enum>Qt::Horizontal</enum> |
