aboutsummaryrefslogtreecommitdiff
path: root/libs/uibase/src/registry.cpp
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
commit7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch)
tree27fb39be241fdb5ac2734c574de678977d1856d0 /libs/uibase/src/registry.cpp
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/uibase/src/registry.cpp')
-rw-r--r--libs/uibase/src/registry.cpp189
1 files changed, 189 insertions, 0 deletions
diff --git a/libs/uibase/src/registry.cpp b/libs/uibase/src/registry.cpp
new file mode 100644
index 0000000..0b6834f
--- /dev/null
+++ b/libs/uibase/src/registry.cpp
@@ -0,0 +1,189 @@
+/*
+This file is part of Mod Organizer.
+
+Mod Organizer is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Mod Organizer is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <uibase/registry.h>
+#include <uibase/log.h>
+#include <uibase/report.h>
+#include <QApplication>
+#include <QFile>
+#include <QFileInfo>
+#include <QList>
+#include <QMessageBox>
+#include <QString>
+#include <QTextStream>
+
+namespace MOBase
+{
+
+// Line-by-line INI writer that preserves the file format.
+// Unlike QSettings::IniFormat, this does NOT interpret backslashes as
+// line continuations, does NOT URL-encode spaces in key names, and does
+// NOT reorder keys. It only modifies the target key=value pair and
+// leaves everything else untouched.
+static bool writeIniValueDirect(const QString& section, const QString& key,
+ const QString& value, const QString& fileName)
+{
+ QStringList lines;
+ bool fileExists = QFileInfo::exists(fileName);
+
+ if (fileExists) {
+ QFile file(fileName);
+ if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ return false;
+ }
+ QTextStream in(&file);
+ while (!in.atEnd()) {
+ lines.append(in.readLine());
+ }
+ file.close();
+ }
+
+ // Find the target section and key
+ QString sectionHeader = "[" + section + "]";
+ int sectionStart = -1;
+ int sectionEnd = lines.size(); // end of file if section is last
+ int keyLine = -1;
+
+ for (int i = 0; i < lines.size(); ++i) {
+ QString trimmed = lines[i].trimmed();
+ if (trimmed.compare(sectionHeader, Qt::CaseInsensitive) == 0) {
+ sectionStart = i;
+ // Find end of this section (next section header or EOF)
+ for (int j = i + 1; j < lines.size(); ++j) {
+ QString t = lines[j].trimmed();
+ if (t.startsWith('[') && t.endsWith(']')) {
+ sectionEnd = j;
+ break;
+ }
+ }
+ break;
+ }
+ }
+
+ if (sectionStart >= 0) {
+ // Section found, look for the key within it
+ for (int i = sectionStart + 1; i < sectionEnd; ++i) {
+ QString trimmed = lines[i].trimmed();
+ // Skip comments and empty lines
+ if (trimmed.isEmpty() || trimmed.startsWith(';') || trimmed.startsWith('#')) {
+ continue;
+ }
+ int eqPos = trimmed.indexOf('=');
+ if (eqPos > 0) {
+ QString existingKey = trimmed.left(eqPos).trimmed();
+ if (existingKey.compare(key, Qt::CaseInsensitive) == 0) {
+ keyLine = i;
+ break;
+ }
+ }
+ }
+
+ if (keyLine >= 0) {
+ // Key found, replace the line preserving indentation
+ QString original = lines[keyLine];
+ int eqPos = original.indexOf('=');
+ // Preserve everything up to and including '='
+ lines[keyLine] = original.left(eqPos + 1) + value;
+ } else {
+ // Key not found in section, insert it after the section header
+ lines.insert(sectionStart + 1, key + "=" + value);
+ }
+ } else {
+ // Section not found, append it
+ if (!lines.isEmpty() && !lines.last().trimmed().isEmpty()) {
+ lines.append(""); // blank line before new section
+ }
+ lines.append(sectionHeader);
+ lines.append(key + "=" + value);
+ }
+
+ // Write back
+ QFile outFile(fileName);
+ if (!outFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
+ return false;
+ }
+ QTextStream out(&outFile);
+ for (int i = 0; i < lines.size(); ++i) {
+ out << lines[i];
+ if (i < lines.size() - 1) {
+ out << '\n';
+ }
+ }
+ // Preserve trailing newline if original had one, or add one
+ out << '\n';
+ outFile.close();
+
+ return true;
+}
+
+bool WriteRegistryValue(const QString& appName, const QString& keyName,
+ const QString& value, const QString& fileName)
+{
+ if (writeIniValueDirect(appName, keyName, value, fileName)) {
+ return true;
+ }
+
+ // Write failed, check if the file is read-only
+ QFileInfo fileInfo(fileName);
+
+ QMessageBox::StandardButton result =
+ MOBase::TaskDialog(qApp->activeModalWidget(),
+ QObject::tr("INI file is read-only"))
+ .main(QObject::tr("INI file is read-only"))
+ .content(QObject::tr("Mod Organizer is attempting to write to \"%1\" "
+ "which is currently set to read-only.")
+ .arg(fileInfo.fileName()))
+ .icon(QMessageBox::Warning)
+ .button({QObject::tr("Clear the read-only flag"), QMessageBox::Yes})
+ .button({QObject::tr("Allow the write once"),
+ QObject::tr("The file will be set to read-only again."),
+ QMessageBox::Ignore})
+ .button({QObject::tr("Skip this file"), QMessageBox::No})
+ .remember("clearReadOnly", fileInfo.fileName())
+ .exec();
+
+ if (result & (QMessageBox::Yes | QMessageBox::Ignore)) {
+ // Make the file writable
+ QFile file(fileName);
+ file.setPermissions(file.permissions() | QFile::WriteUser | QFile::WriteOwner);
+
+ bool ok = writeIniValueDirect(appName, keyName, value, fileName);
+
+ if (result == QMessageBox::Ignore) {
+ // Set back to read-only
+ file.setPermissions(file.permissions() & ~(QFile::WriteUser | QFile::WriteOwner));
+ }
+
+ return ok;
+ }
+
+ return false;
+}
+
+#ifdef _WIN32
+bool WriteRegistryValue(const wchar_t* appName, const wchar_t* keyName,
+ const wchar_t* value, const wchar_t* fileName)
+{
+ return WriteRegistryValue(
+ QString::fromWCharArray(appName),
+ QString::fromWCharArray(keyName),
+ QString::fromWCharArray(value),
+ QString::fromWCharArray(fileName));
+}
+#endif
+
+} // namespace MOBase