aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/src/datatab.cpp42
-rw-r--r--src/src/datatab.h3
-rw-r--r--src/src/envshortcut.cpp106
-rw-r--r--src/src/envshortcut.h8
-rw-r--r--src/src/loot.cpp43
-rw-r--r--src/src/mainwindow.cpp14
-rw-r--r--src/src/mainwindow.ui10
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>