diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index de72aa2577..d87073df02 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -142,7 +142,7 @@ Launcher::DataFilesPage::DataFilesPage(const Files::ConfigurationManager& cfg, C ui.setupUi(this); setObjectName("DataFilesPage"); mSelector = new ContentSelectorView::ContentSelector(ui.contentSelectorWidget, /*showOMWScripts=*/true); - const QString encoding = mGameSettings.value("encoding", "win1252"); + const QString encoding = mGameSettings.value("encoding", { "win1252" }).value; mSelector->setEncoding(encoding); QVector> languages = { { "English", tr("English") }, { "French", tr("French") }, @@ -163,11 +163,11 @@ Launcher::DataFilesPage::DataFilesPage(const Files::ConfigurationManager& cfg, C connect(ui.directoryInsertButton, &QPushButton::released, this, [this]() { this->addSubdirectories(false); }); connect(ui.directoryUpButton, &QPushButton::released, this, [this]() { this->moveDirectory(-1); }); connect(ui.directoryDownButton, &QPushButton::released, this, [this]() { this->moveDirectory(1); }); - connect(ui.directoryRemoveButton, &QPushButton::released, this, [this]() { this->removeDirectory(); }); + connect(ui.directoryRemoveButton, &QPushButton::released, this, &DataFilesPage::removeDirectory); connect(ui.archiveUpButton, &QPushButton::released, this, [this]() { this->moveArchives(-1); }); connect(ui.archiveDownButton, &QPushButton::released, this, [this]() { this->moveArchives(1); }); - connect( - ui.directoryListWidget->model(), &QAbstractItemModel::rowsMoved, this, [this]() { this->sortDirectories(); }); + connect(ui.directoryListWidget->model(), &QAbstractItemModel::rowsMoved, this, &DataFilesPage::sortDirectories); + connect(ui.archiveListWidget->model(), &QAbstractItemModel::rowsMoved, this, &DataFilesPage::sortArchives); buildView(); loadSettings(); @@ -271,39 +271,50 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName) ui.archiveListWidget->clear(); ui.directoryListWidget->clear(); - QStringList directories = mLauncherSettings.getDataDirectoryList(contentModelName); - if (directories.isEmpty()) - directories = mGameSettings.getDataDirs(); + QList directories = mGameSettings.getDataDirs(); + QStringList contentModelDirectories = mLauncherSettings.getDataDirectoryList(contentModelName); + if (!contentModelDirectories.isEmpty()) + { + directories.erase(std::remove_if(directories.begin(), directories.end(), + [&](const Config::SettingValue& dir) { return mGameSettings.isUserSetting(dir); }), + directories.end()); + for (const auto& dir : contentModelDirectories) + directories.push_back({ dir }); + } mDataLocal = mGameSettings.getDataLocal(); if (!mDataLocal.isEmpty()) - directories.insert(0, mDataLocal); + directories.insert(0, { mDataLocal }); const auto& resourcesVfs = mGameSettings.getResourcesVfs(); if (!resourcesVfs.isEmpty()) - directories.insert(0, resourcesVfs); + directories.insert(0, { resourcesVfs }); std::unordered_set visitedDirectories; - for (const QString& currentDir : directories) + for (const Config::SettingValue& currentDir : directories) { - // normalize user supplied directories: resolve symlink, convert to native separator, make absolute - const QString canonicalDirPath = QDir(QDir::cleanPath(currentDir)).canonicalPath(); + // normalize user supplied directories: resolve symlink, convert to native separator + const QString canonicalDirPath = QDir(QDir::cleanPath(currentDir.value)).canonicalPath(); if (!visitedDirectories.insert(canonicalDirPath).second) continue; // add new achives files presents in current directory - addArchivesFromDir(currentDir); + addArchivesFromDir(currentDir.value); QStringList tooltip; // add content files presents in current directory - mSelector->addFiles(currentDir, mNewDataDirs.contains(canonicalDirPath)); + mSelector->addFiles(currentDir.value, mNewDataDirs.contains(canonicalDirPath)); // add current directory to list - ui.directoryListWidget->addItem(currentDir); + ui.directoryListWidget->addItem(currentDir.originalRepresentation); auto row = ui.directoryListWidget->count() - 1; auto* item = ui.directoryListWidget->item(row); + item->setData(Qt::UserRole, QVariant::fromValue(currentDir)); + + if (currentDir.value != currentDir.originalRepresentation) + tooltip << tr("Resolved as %1").arg(currentDir.value); // Display new content with custom formatting if (mNewDataDirs.contains(canonicalDirPath)) @@ -316,18 +327,22 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName) } // deactivate data-local and resources/vfs: they are always included - if (currentDir == mDataLocal || currentDir == resourcesVfs) + // same for ones from non-user config files + if (currentDir.value == mDataLocal || currentDir.value == resourcesVfs + || !mGameSettings.isUserSetting(currentDir)) { auto flags = item->flags(); item->setFlags(flags & ~(Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEnabled)); + if (currentDir.value == mDataLocal) + tooltip << tr("This is the data-local directory and cannot be disabled"); + else if (currentDir.value == resourcesVfs) + tooltip << tr("This directory is part of OpenMW and cannot be disabled"); + else + tooltip << tr("This directory is enabled in an openmw.cfg other than the user one"); } - if (currentDir == mDataLocal) - tooltip << tr("This is the data-local directory and cannot be disabled"); - else if (currentDir == resourcesVfs) - tooltip << tr("This directory is part of OpenMW and cannot be disabled"); // Add a "data file" icon if the directory contains a content file - if (mSelector->containsDataFiles(currentDir)) + if (mSelector->containsDataFiles(currentDir.value)) { item->setIcon(QIcon(":/images/openmw-plugin.png")); @@ -345,15 +360,22 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName) } mSelector->sortFiles(); - QStringList selectedArchives = mLauncherSettings.getArchiveList(contentModelName); - if (selectedArchives.isEmpty()) - selectedArchives = mGameSettings.getArchiveList(); + QList selectedArchives = mGameSettings.getArchiveList(); + QStringList contentModelSelectedArchives = mLauncherSettings.getArchiveList(contentModelName); + if (contentModelSelectedArchives.isEmpty()) + { + selectedArchives.erase(std::remove_if(selectedArchives.begin(), selectedArchives.end(), + [&](const Config::SettingValue& dir) { return mGameSettings.isUserSetting(dir); }), + selectedArchives.end()); + for (const auto& dir : contentModelSelectedArchives) + selectedArchives.push_back({ dir }); + } // sort and tick BSA according to profile int row = 0; for (const auto& archive : selectedArchives) { - const auto match = ui.archiveListWidget->findItems(archive, Qt::MatchExactly); + const auto match = ui.archiveListWidget->findItems(archive.value, Qt::MatchExactly); if (match.isEmpty()) continue; const auto name = match[0]->text(); @@ -361,9 +383,25 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName) ui.archiveListWidget->takeItem(oldrow); ui.archiveListWidget->insertItem(row, name); ui.archiveListWidget->item(row)->setCheckState(Qt::Checked); + ui.archiveListWidget->item(row)->setData(Qt::UserRole, QVariant::fromValue(archive)); + if (!mGameSettings.isUserSetting(archive)) + { + auto flags = ui.archiveListWidget->item(row)->flags(); + ui.archiveListWidget->item(row)->setFlags( + flags & ~(Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEnabled)); + ui.archiveListWidget->item(row)->setToolTip( + tr("This archive is enabled in an openmw.cfg other than the user one")); + } row++; } + QStringList nonUserContent; + for (const auto& content : mGameSettings.getContentList()) + { + if (!mGameSettings.isUserSetting(content)) + nonUserContent.push_back(content.value); + } + mSelector->setNonUserContent(nonUserContent); mSelector->setProfileContent(mLauncherSettings.getContentListFiles(contentModelName)); } @@ -391,7 +429,19 @@ void Launcher::DataFilesPage::saveSettings(const QString& profile) { fileNames.append(item->fileName()); } - mLauncherSettings.setContentList(profileName, dirList, selectedArchivePaths(), fileNames); + QStringList dirNames; + for (const auto& dir : dirList) + { + if (mGameSettings.isUserSetting(dir)) + dirNames.push_back(dir.originalRepresentation); + } + QStringList archiveNames; + for (const auto& archive : selectedArchivePaths()) + { + if (mGameSettings.isUserSetting(archive)) + archiveNames.push_back(archive.originalRepresentation); + } + mLauncherSettings.setContentList(profileName, dirNames, archiveNames, fileNames); mGameSettings.setContentList(dirList, selectedArchivePaths(), fileNames); QString language(mSelector->languageBox()->currentData().toString()); @@ -400,38 +450,38 @@ void Launcher::DataFilesPage::saveSettings(const QString& profile) if (language == QLatin1String("Polish")) { - mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1250")); + mGameSettings.setValue(QLatin1String("encoding"), { "win1250" }); } else if (language == QLatin1String("Russian")) { - mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1251")); + mGameSettings.setValue(QLatin1String("encoding"), { "win1251" }); } else { - mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1252")); + mGameSettings.setValue(QLatin1String("encoding"), { "win1252" }); } } -QStringList Launcher::DataFilesPage::selectedDirectoriesPaths() const +QList Launcher::DataFilesPage::selectedDirectoriesPaths() const { - QStringList dirList; + QList dirList; for (int i = 0; i < ui.directoryListWidget->count(); ++i) { const QListWidgetItem* item = ui.directoryListWidget->item(i); if (item->flags() & Qt::ItemIsEnabled) - dirList.append(item->text()); + dirList.append(qvariant_cast(item->data(Qt::UserRole))); } return dirList; } -QStringList Launcher::DataFilesPage::selectedArchivePaths() const +QList Launcher::DataFilesPage::selectedArchivePaths() const { - QStringList archiveList; + QList archiveList; for (int i = 0; i < ui.archiveListWidget->count(); ++i) { const QListWidgetItem* item = ui.archiveListWidget->item(i); if (item->checkState() == Qt::Checked) - archiveList.append(item->text()); + archiveList.append(qvariant_cast(item->data(Qt::UserRole))); } return archiveList; } @@ -585,7 +635,20 @@ void Launcher::DataFilesPage::on_cloneProfileAction_triggered() if (profile.isEmpty()) return; - mLauncherSettings.setContentList(profile, selectedDirectoriesPaths(), selectedArchivePaths(), selectedFilePaths()); + const auto& dirList = selectedDirectoriesPaths(); + QStringList dirNames; + for (const auto& dir : dirList) + { + if (mGameSettings.isUserSetting(dir)) + dirNames.push_back(dir.originalRepresentation); + } + QStringList archiveNames; + for (const auto& archive : selectedArchivePaths()) + { + if (mGameSettings.isUserSetting(archive)) + archiveNames.push_back(archive.originalRepresentation); + } + mLauncherSettings.setContentList(profile, dirNames, archiveNames, selectedFilePaths()); addProfile(profile, true); } @@ -704,6 +767,21 @@ void Launcher::DataFilesPage::sortDirectories() } } +void Launcher::DataFilesPage::sortArchives() +{ + // Ensure disabled entries (aka ones from non-user config files) are always at the top. + for (auto i = 1; i < ui.archiveListWidget->count(); ++i) + { + if (!(ui.archiveListWidget->item(i)->flags() & Qt::ItemIsEnabled) + && (ui.archiveListWidget->item(i - 1)->flags() & Qt::ItemIsEnabled)) + { + const auto item = ui.archiveListWidget->takeItem(i); + ui.archiveListWidget->insertItem(i - 1, item); + ui.archiveListWidget->setCurrentRow(i); + } + } +} + void Launcher::DataFilesPage::moveDirectory(int step) { int selectedRow = ui.directoryListWidget->currentRow(); diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index e568137e8f..1b92354dab 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -25,6 +25,7 @@ namespace ContentSelectorView namespace Config { class GameSettings; + struct SettingValue; class LauncherSettings; } @@ -73,6 +74,7 @@ namespace Launcher void updateCloneProfileOkButton(const QString& text); void addSubdirectories(bool append); void sortDirectories(); + void sortArchives(); void removeDirectory(); void moveArchives(int step); void moveDirectory(int step); @@ -146,8 +148,8 @@ namespace Launcher * @return the file paths of all selected content files */ QStringList selectedFilePaths() const; - QStringList selectedArchivePaths() const; - QStringList selectedDirectoriesPaths() const; + QList selectedArchivePaths() const; + QList selectedDirectoriesPaths() const; }; } #endif diff --git a/apps/launcher/importpage.cpp b/apps/launcher/importpage.cpp index 44c5867c0d..47075db1bc 100644 --- a/apps/launcher/importpage.cpp +++ b/apps/launcher/importpage.cpp @@ -37,9 +37,9 @@ Launcher::ImportPage::ImportPage(const Files::ConfigurationManager& cfg, Config: // Detect Morrowind configuration files QStringList iniPaths; - for (const QString& path : mGameSettings.getDataDirs()) + for (const auto& path : mGameSettings.getDataDirs()) { - QDir dir(path); + QDir dir(path.value); dir.setPath(dir.canonicalPath()); // Resolve symlinks if (dir.exists(QString("Morrowind.ini"))) @@ -125,7 +125,7 @@ void Launcher::ImportPage::on_importerButton_clicked() arguments.append(QString("--fonts")); arguments.append(QString("--encoding")); - arguments.append(mGameSettings.value(QString("encoding"), QString("win1252"))); + arguments.append(mGameSettings.value(QString("encoding"), { "win1252" }).value); arguments.append(QString("--ini")); arguments.append(settingsComboBox->currentText()); arguments.append(QString("--cfg")); diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index f9d07d54a5..aca8a64e31 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -320,7 +320,7 @@ bool Launcher::MainDialog::setupGameSettings() QFile file; - auto loadFile = [&](const QString& path, bool (Config::GameSettings::*reader)(QTextStream&, bool), + auto loadFile = [&](const QString& path, bool (Config::GameSettings::*reader)(QTextStream&, const QString&, bool), bool ignoreContent = false) -> std::optional { file.setFileName(path); if (file.exists()) @@ -337,7 +337,7 @@ bool Launcher::MainDialog::setupGameSettings() QTextStream stream(&file); Misc::ensureUtf8Encoding(stream); - (mGameSettings.*reader)(stream, ignoreContent); + (mGameSettings.*reader)(stream, QFileInfo(path).dir().path(), ignoreContent); file.close(); return true; } @@ -360,12 +360,12 @@ bool Launcher::MainDialog::setupGameSettings() bool Launcher::MainDialog::setupGameData() { - QStringList dataDirs; + bool foundData = false; // Check if the paths actually contain data files - for (const QString& path3 : mGameSettings.getDataDirs()) + for (const auto& path3 : mGameSettings.getDataDirs()) { - QDir dir(path3); + QDir dir(path3.value); QStringList filters; filters << "*.esp" << "*.esm" @@ -373,10 +373,10 @@ bool Launcher::MainDialog::setupGameData() << "*.omwaddon"; if (!dir.entryList(filters).isEmpty()) - dataDirs.append(path3); + foundData = true; } - if (dataDirs.isEmpty()) + if (!foundData) { QMessageBox msgBox; msgBox.setWindowTitle(tr("Error detecting Morrowind installation")); diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index 93a724909e..0df871c90d 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -340,7 +340,7 @@ bool Launcher::SettingsPage::loadSettings() { loadSettingBool(Settings::input().mGrabCursor, *grabCursorCheckBox); - bool skipMenu = mGameSettings.value("skip-menu").toInt() == 1; + bool skipMenu = mGameSettings.value("skip-menu").value.toInt() == 1; if (skipMenu) { skipMenuCheckBox->setCheckState(Qt::Checked); @@ -348,8 +348,8 @@ bool Launcher::SettingsPage::loadSettings() startDefaultCharacterAtLabel->setEnabled(skipMenu); startDefaultCharacterAtField->setEnabled(skipMenu); - startDefaultCharacterAtField->setText(mGameSettings.value("start")); - runScriptAfterStartupField->setText(mGameSettings.value("script-run")); + startDefaultCharacterAtField->setText(mGameSettings.value("start").value); + runScriptAfterStartupField->setText(mGameSettings.value("script-run").value); } return true; } @@ -536,17 +536,17 @@ void Launcher::SettingsPage::saveSettings() saveSettingBool(*grabCursorCheckBox, Settings::input().mGrabCursor); int skipMenu = skipMenuCheckBox->checkState() == Qt::Checked; - if (skipMenu != mGameSettings.value("skip-menu").toInt()) - mGameSettings.setValue("skip-menu", QString::number(skipMenu)); + if (skipMenu != mGameSettings.value("skip-menu").value.toInt()) + mGameSettings.setValue("skip-menu", { QString::number(skipMenu) }); QString startCell = startDefaultCharacterAtField->text(); - if (startCell != mGameSettings.value("start")) + if (startCell != mGameSettings.value("start").value) { - mGameSettings.setValue("start", startCell); + mGameSettings.setValue("start", { startCell }); } QString scriptRun = runScriptAfterStartupField->text(); - if (scriptRun != mGameSettings.value("script-run")) - mGameSettings.setValue("script-run", scriptRun); + if (scriptRun != mGameSettings.value("script-run").value) + mGameSettings.setValue("script-run", { scriptRun }); } } diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp index e8bd6f7007..0b5cadd979 100644 --- a/apps/wizard/mainwizard.cpp +++ b/apps/wizard/mainwizard.cpp @@ -24,6 +24,8 @@ #include "installationpage.hpp" #endif +#include + using namespace Process; Wizard::MainWizard::MainWizard(Files::ConfigurationManager&& cfgMgr, QWidget* parent) @@ -167,7 +169,7 @@ void Wizard::MainWizard::setupGameSettings() QTextStream stream(&file); Misc::ensureUtf8Encoding(stream); - mGameSettings.readUserFile(stream); + mGameSettings.readUserFile(stream, QFileInfo(path).dir().path()); } file.close(); @@ -196,7 +198,7 @@ void Wizard::MainWizard::setupGameSettings() QTextStream stream(&file); Misc::ensureUtf8Encoding(stream); - mGameSettings.readFile(stream); + mGameSettings.readFile(stream, QFileInfo(path2).dir().path()); } file.close(); } @@ -241,11 +243,11 @@ void Wizard::MainWizard::setupLauncherSettings() void Wizard::MainWizard::setupInstallations() { // Check if the paths actually contain a Morrowind installation - for (const QString& path : mGameSettings.getDataDirs()) + for (const auto& path : mGameSettings.getDataDirs()) { - if (findFiles(QLatin1String("Morrowind"), path)) - addInstallation(path); + if (findFiles(QLatin1String("Morrowind"), path.value)) + addInstallation(path.value); } } @@ -332,10 +334,12 @@ void Wizard::MainWizard::addInstallation(const QString& path) mInstallations.insert(QDir::toNativeSeparators(path), install); // Add it to the openmw.cfg too - if (!mGameSettings.getDataDirs().contains(path)) + const auto& dataDirs = mGameSettings.getDataDirs(); + if (std::none_of( + dataDirs.begin(), dataDirs.end(), [&](const Config::SettingValue& dir) { return dir.value == path; })) { - mGameSettings.setMultiValue(QLatin1String("data"), path); - mGameSettings.addDataDir(path); + mGameSettings.setMultiValue(QLatin1String("data"), { path }); + mGameSettings.addDataDir({ path }); } } @@ -394,15 +398,15 @@ void Wizard::MainWizard::writeSettings() if (language == QLatin1String("Polish")) { - mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1250")); + mGameSettings.setValue(QLatin1String("encoding"), { "win1250" }); } else if (language == QLatin1String("Russian")) { - mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1251")); + mGameSettings.setValue(QLatin1String("encoding"), { "win1251" }); } else { - mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1252")); + mGameSettings.setValue(QLatin1String("encoding"), { "win1252" }); } // Write the installation path so that openmw can find them @@ -410,7 +414,7 @@ void Wizard::MainWizard::writeSettings() // Make sure the installation path is the last data= entry mGameSettings.removeDataDir(path); - mGameSettings.addDataDir(path); + mGameSettings.addDataDir({ path }); QString userPath(Files::pathToQString(mCfgMgr.getUserConfigPath())); QDir dir(userPath); diff --git a/components/config/gamesettings.cpp b/components/config/gamesettings.cpp index 7cace721bf..9aed4656bc 100644 --- a/components/config/gamesettings.cpp +++ b/components/config/gamesettings.cpp @@ -13,7 +13,8 @@ const char Config::GameSettings::sDirectoryKey[] = "data"; namespace { - QStringList reverse(QStringList values) + template + QList reverse(QList values) { std::reverse(values.begin(), values.end()); return values; @@ -27,70 +28,69 @@ Config::GameSettings::GameSettings(const Files::ConfigurationManager& cfg) void Config::GameSettings::validatePaths() { - QStringList paths = mSettings.values(QString("data")); - Files::PathContainer dataDirs; + QList paths = mSettings.values(QString("data")); - for (const QString& path : paths) - { - dataDirs.emplace_back(Files::pathFromQString(path)); - } - - // Parse the data dirs to convert the tokenized paths - mCfgMgr.processPaths(dataDirs, /*basePath=*/""); mDataDirs.clear(); - for (const auto& dataDir : dataDirs) + for (const auto& dataDir : paths) { - if (is_directory(dataDir)) - mDataDirs.append(Files::pathToQString(dataDir)); + if (QDir(dataDir.value).exists()) + mDataDirs.append(dataDir); } // Do the same for data-local - QString local = mSettings.value(QString("data-local")); + const QString& local = mSettings.value(QString("data-local")).value; - if (local.isEmpty()) - return; - - dataDirs.clear(); - dataDirs.emplace_back(Files::pathFromQString(local)); - - mCfgMgr.processPaths(dataDirs, /*basePath=*/""); - - if (!dataDirs.empty()) - { - const auto& path = dataDirs.front(); - if (is_directory(path)) - mDataLocal = Files::pathToQString(path); - } + if (!local.isEmpty() && QDir(local).exists()) + mDataLocal = local; } QString Config::GameSettings::getResourcesVfs() const { - QString resources = mSettings.value(QString("resources"), QString("./resources")); + QString resources = mSettings.value(QString("resources"), { "./resources", "", "" }).value; resources += "/vfs"; return QFileInfo(resources).canonicalFilePath(); } -QStringList Config::GameSettings::values(const QString& key, const QStringList& defaultValues) const +QList Config::GameSettings::values( + const QString& key, const QList& defaultValues) const { if (!mSettings.values(key).isEmpty()) return mSettings.values(key); return defaultValues; } -bool Config::GameSettings::readFile(QTextStream& stream, bool ignoreContent) +bool Config::GameSettings::containsValue(const QString& key, const QString& value) const { - return readFile(stream, mSettings, ignoreContent); + auto [itr, end] = mSettings.equal_range(key); + while (itr != end) + { + if (itr->value == value) + return true; + ++itr; + } + return false; } -bool Config::GameSettings::readUserFile(QTextStream& stream, bool ignoreContent) +bool Config::GameSettings::readFile(QTextStream& stream, const QString& context, bool ignoreContent) { - return readFile(stream, mUserSettings, ignoreContent); + if (readFile(stream, mSettings, context, ignoreContent)) + { + mContexts.push_back(context); + return true; + } + return false; } -bool Config::GameSettings::readFile(QTextStream& stream, QMultiMap& settings, bool ignoreContent) +bool Config::GameSettings::readUserFile(QTextStream& stream, const QString& context, bool ignoreContent) { - QMultiMap cache; + return readFile(stream, mUserSettings, context, ignoreContent); +} + +bool Config::GameSettings::readFile( + QTextStream& stream, QMultiMap& settings, const QString& context, bool ignoreContent) +{ + QMultiMap cache; QRegularExpression replaceRe("^\\s*replace\\s*=\\s*(.+)$"); QRegularExpression keyRe("^([^=]+)\\s*=\\s*(.+)$"); @@ -129,7 +129,7 @@ bool Config::GameSettings::readFile(QTextStream& stream, QMultiMap values = cache.values(key); values.append(settings.values(key)); - if (!values.contains(value)) + bool exists = false; + for (const auto& existingValue : values) + { + if (existingValue.value == value.value) + { + exists = true; + break; + } + } + if (!exists) { cache.insert(key, value); } @@ -216,7 +230,7 @@ bool Config::GameSettings::writeFile(QTextStream& stream) // Equivalent to stream << std::quoted(i.value(), '"', '&'), which won't work on QStrings. QChar delim = '\"'; QChar escape = '&'; - QString string = i.value(); + QString string = i.value().originalRepresentation; stream << delim; for (auto& it : string) @@ -231,7 +245,7 @@ bool Config::GameSettings::writeFile(QTextStream& stream) continue; } - stream << i.key() << "=" << i.value() << "\n"; + stream << i.key() << "=" << i.value().originalRepresentation << "\n"; } return true; @@ -386,10 +400,11 @@ bool Config::GameSettings::writeFileWithComments(QFile& file) *iter = QString(); // assume no match QString key = match.captured(1); QString keyVal = match.captured(1) + "=" + match.captured(2); - QMultiMap::const_iterator i = mUserSettings.find(key); + QMultiMap::const_iterator i = mUserSettings.find(key); while (i != mUserSettings.end() && i.key() == key) { - QString settingLine = i.key() + "=" + i.value(); + // todo: does this need to handle paths? + QString settingLine = i.key() + "=" + i.value().originalRepresentation; QRegularExpressionMatch keyMatch = settingRegex.match(settingLine); if (keyMatch.hasMatch()) { @@ -441,7 +456,7 @@ bool Config::GameSettings::writeFileWithComments(QFile& file) // Equivalent to settingLine += std::quoted(it.value(), '"', '&'), which won't work on QStrings. QChar delim = '\"'; QChar escape = '&'; - QString string = it.value(); + QString string = it.value().originalRepresentation; settingLine += delim; for (auto& iter : string) @@ -453,7 +468,7 @@ bool Config::GameSettings::writeFileWithComments(QFile& file) settingLine += delim; } else - settingLine = it.key() + "=" + it.value(); + settingLine = it.key() + "=" + it.value().originalRepresentation; QRegularExpressionMatch match = settingRegex.match(settingLine); if (match.hasMatch()) @@ -512,11 +527,11 @@ bool Config::GameSettings::writeFileWithComments(QFile& file) bool Config::GameSettings::hasMaster() { bool result = false; - QStringList content = mSettings.values(QString(Config::GameSettings::sContentKey)); + QList content = mSettings.values(QString(Config::GameSettings::sContentKey)); for (int i = 0; i < content.count(); ++i) { - if (content.at(i).endsWith(QLatin1String(".omwgame"), Qt::CaseInsensitive) - || content.at(i).endsWith(QLatin1String(".esm"), Qt::CaseInsensitive)) + if (content.at(i).value.endsWith(QLatin1String(".omwgame"), Qt::CaseInsensitive) + || content.at(i).value.endsWith(QLatin1String(".esm"), Qt::CaseInsensitive)) { result = true; break; @@ -527,39 +542,49 @@ bool Config::GameSettings::hasMaster() } void Config::GameSettings::setContentList( - const QStringList& dirNames, const QStringList& archiveNames, const QStringList& fileNames) + const QList& dirNames, const QList& archiveNames, const QStringList& fileNames) { auto const reset = [this](const char* key, const QStringList& list) { remove(key); for (auto const& item : list) - setMultiValue(key, item); + setMultiValue(key, { item }); }; - reset(sDirectoryKey, dirNames); - reset(sArchiveKey, archiveNames); + remove(sDirectoryKey); + for (auto const& item : dirNames) + setMultiValue(sDirectoryKey, item); + remove(sArchiveKey); + for (auto const& item : archiveNames) + setMultiValue(sArchiveKey, item); reset(sContentKey, fileNames); } -QStringList Config::GameSettings::getDataDirs() const +QList Config::GameSettings::getDataDirs() const { return reverse(mDataDirs); } -QStringList Config::GameSettings::getArchiveList() const +QList Config::GameSettings::getArchiveList() const { // QMap returns multiple rows in LIFO order, so need to reverse return reverse(values(sArchiveKey)); } -QStringList Config::GameSettings::getContentList() const +QList Config::GameSettings::getContentList() const { // QMap returns multiple rows in LIFO order, so need to reverse return reverse(values(sContentKey)); } +bool Config::GameSettings::isUserSetting(const SettingValue& settingValue) const +{ + return settingValue.context.isEmpty() || settingValue.context == getUserContext(); +} + void Config::GameSettings::clear() { mSettings.clear(); + mContexts.clear(); mUserSettings.clear(); mDataDirs.clear(); mDataLocal.clear(); diff --git a/components/config/gamesettings.hpp b/components/config/gamesettings.hpp index bef108e2c7..14a8fcb155 100644 --- a/components/config/gamesettings.hpp +++ b/components/config/gamesettings.hpp @@ -17,33 +17,48 @@ namespace Files namespace Config { + struct SettingValue + { + QString value = ""; + // value as found in openmw.cfg, e.g. relative path with ?slug? + QString originalRepresentation = value; + // path of openmw.cfg, e.g. to resolve relative paths + QString context = ""; + + friend auto operator<=>(const SettingValue&, const SettingValue&) = default; + }; + class GameSettings { public: explicit GameSettings(const Files::ConfigurationManager& cfg); - inline QString value(const QString& key, const QString& defaultValue = QString()) + inline SettingValue value(const QString& key, const SettingValue& defaultValue = {}) { - return mSettings.value(key).isEmpty() ? defaultValue : mSettings.value(key); + return mSettings.contains(key) ? mSettings.value(key) : defaultValue; } - inline void setValue(const QString& key, const QString& value) + inline void setValue(const QString& key, const SettingValue& value) { mSettings.remove(key); mSettings.insert(key, value); mUserSettings.remove(key); - mUserSettings.insert(key, value); + if (isUserSetting(value)) + mUserSettings.insert(key, value); } - inline void setMultiValue(const QString& key, const QString& value) + inline void setMultiValue(const QString& key, const SettingValue& value) { - QStringList values = mSettings.values(key); + QList values = mSettings.values(key); if (!values.contains(value)) mSettings.insert(key, value); - values = mUserSettings.values(key); - if (!values.contains(value)) - mUserSettings.insert(key, value); + if (isUserSetting(value)) + { + values = mUserSettings.values(key); + if (!values.contains(value)) + mUserSettings.insert(key, value); + } } inline void remove(const QString& key) @@ -52,36 +67,48 @@ namespace Config mUserSettings.remove(key); } - QStringList getDataDirs() const; + QList getDataDirs() const; QString getResourcesVfs() const; - inline void removeDataDir(const QString& dir) + inline void removeDataDir(const QString& existingDir) { - if (!dir.isEmpty()) - mDataDirs.removeAll(dir); + if (!existingDir.isEmpty()) + { + // non-user settings can't be removed as we can't edit the openmw.cfg they're in + std::remove_if(mDataDirs.begin(), mDataDirs.end(), + [&](const SettingValue& dir) { return isUserSetting(dir) && dir.value == existingDir; }); + } } - inline void addDataDir(const QString& dir) + + inline void addDataDir(const SettingValue& dir) { - if (!dir.isEmpty()) + if (!dir.value.isEmpty()) mDataDirs.append(dir); } + inline QString getDataLocal() const { return mDataLocal; } bool hasMaster(); - QStringList values(const QString& key, const QStringList& defaultValues = QStringList()) const; + QList values(const QString& key, const QList& defaultValues = {}) const; + bool containsValue(const QString& key, const QString& value) const; - bool readFile(QTextStream& stream, bool ignoreContent = false); - bool readFile(QTextStream& stream, QMultiMap& settings, bool ignoreContent = false); - bool readUserFile(QTextStream& stream, bool ignoreContent = false); + bool readFile(QTextStream& stream, const QString& context, bool ignoreContent = false); + bool readFile(QTextStream& stream, QMultiMap& settings, const QString& context, + bool ignoreContent = false); + bool readUserFile(QTextStream& stream, const QString& context, bool ignoreContent = false); bool writeFile(QTextStream& stream); bool writeFileWithComments(QFile& file); - QStringList getArchiveList() const; - void setContentList(const QStringList& dirNames, const QStringList& archiveNames, const QStringList& fileNames); - QStringList getContentList() const; + QList getArchiveList() const; + void setContentList( + const QList& dirNames, const QList& archiveNames, const QStringList& fileNames); + QList getContentList() const; + + const QString& getUserContext() const { return mContexts.back(); } + bool isUserSetting(const SettingValue& settingValue) const; void clear(); @@ -89,10 +116,12 @@ namespace Config const Files::ConfigurationManager& mCfgMgr; void validatePaths(); - QMultiMap mSettings; - QMultiMap mUserSettings; + QMultiMap mSettings; + QMultiMap mUserSettings; - QStringList mDataDirs; + QStringList mContexts; + + QList mDataDirs; QString mDataLocal; static const char sArchiveKey[]; @@ -102,4 +131,7 @@ namespace Config static bool isOrderedLine(const QString& line); }; } + +Q_DECLARE_METATYPE(Config::SettingValue) + #endif // GAMESETTINGS_HPP diff --git a/components/config/launchersettings.cpp b/components/config/launchersettings.cpp index 2f4decb762..f9f067e58a 100644 --- a/components/config/launchersettings.cpp +++ b/components/config/launchersettings.cpp @@ -223,9 +223,25 @@ QStringList Config::LauncherSettings::getContentLists() void Config::LauncherSettings::setContentList(const GameSettings& gameSettings) { // obtain content list from game settings (if present) - QStringList dirs(gameSettings.getDataDirs()); - const QStringList archives(gameSettings.getArchiveList()); - const QStringList files(gameSettings.getContentList()); + QList dirs(gameSettings.getDataDirs()); + dirs.erase(std::remove_if( + dirs.begin(), dirs.end(), [&](const SettingValue& dir) { return !gameSettings.isUserSetting(dir); }), + dirs.end()); + // archives and content files aren't preprocessed, so we don't need to track their original form + const QList archivesOriginal(gameSettings.getArchiveList()); + QStringList archives; + for (const auto& archive : archivesOriginal) + { + if (gameSettings.isUserSetting(archive)) + archives.push_back(archive.value); + } + const QList filesOriginal(gameSettings.getContentList()); + QStringList files; + for (const auto& file : filesOriginal) + { + if (gameSettings.isUserSetting(file)) + files.push_back(file.value); + } // if openmw.cfg has no content, exit so we don't create an empty content list. if (dirs.isEmpty() || files.isEmpty()) @@ -236,14 +252,25 @@ void Config::LauncherSettings::setContentList(const GameSettings& gameSettings) // local data directory and resources/vfs are not part of any profile const auto resourcesVfs = gameSettings.getResourcesVfs(); const auto dataLocal = gameSettings.getDataLocal(); - dirs.removeAll(resourcesVfs); - dirs.removeAll(dataLocal); + dirs.erase( + std::remove_if(dirs.begin(), dirs.end(), [&](const SettingValue& dir) { return dir.value == resourcesVfs; }), + dirs.end()); + dirs.erase( + std::remove_if(dirs.begin(), dirs.end(), [&](const SettingValue& dir) { return dir.value == dataLocal; }), + dirs.end()); // if any existing profile in launcher matches the content list, make that profile the default for (const QString& listName : getContentLists()) { - if (files == getContentListFiles(listName) && archives == getArchiveList(listName) - && dirs == getDataDirectoryList(listName)) + const auto& listDirs = getDataDirectoryList(listName); + if (dirs.length() != listDirs.length()) + continue; + for (int i = 0; i < dirs.length(); ++i) + { + if (dirs[i].value != listDirs[i]) + continue; + } + if (files == getContentListFiles(listName) && archives == getArchiveList(listName)) { setCurrentContentListName(listName); return; @@ -253,7 +280,10 @@ void Config::LauncherSettings::setContentList(const GameSettings& gameSettings) // otherwise, add content list QString newContentListName(makeNewContentListName()); setCurrentContentListName(newContentListName); - setContentList(newContentListName, dirs, archives, files); + QStringList newListDirs; + for (const auto& dir : dirs) + newListDirs.push_back(dir.value); + setContentList(newContentListName, newListDirs, archives, files); } void Config::LauncherSettings::setContentList(const QString& contentListName, const QStringList& dirNames, diff --git a/components/contentselector/model/contentmodel.cpp b/components/contentselector/model/contentmodel.cpp index 003f2ee241..66fde2063f 100644 --- a/components/contentselector/model/contentmodel.cpp +++ b/components/contentselector/model/contentmodel.cpp @@ -220,7 +220,8 @@ QVariant ContentSelectorModel::ContentModel::data(const QModelIndex& index, int if (file == mGameFile) return QVariant(); - return (file->builtIn() || file->fromAnotherConfigFile() || mCheckedFiles.contains(file)) ? Qt::Checked : Qt::Unchecked; + return (file->builtIn() || file->fromAnotherConfigFile() || mCheckedFiles.contains(file)) ? Qt::Checked + : Qt::Unchecked; } case Qt::UserRole: @@ -467,6 +468,8 @@ void ContentSelectorModel::ContentModel::addFiles(const QString& path, bool newf if (info.fileName().compare("builtin.omwscripts", Qt::CaseInsensitive) == 0) file->setBuiltIn(true); + file->setFromAnotherConfigFile(mNonUserContent.contains(info.fileName().toLower())); + if (info.fileName().endsWith(".omwscripts", Qt::CaseInsensitive)) { file->setDate(info.lastModified()); @@ -660,6 +663,28 @@ void ContentSelectorModel::ContentModel::setNew(const QString& filepath, bool is mNewFiles[filepath] = isNew; } +void ContentSelectorModel::ContentModel::setNonUserContent(const QStringList& fileList) +{ + mNonUserContent.clear(); + for (const auto& file : fileList) + mNonUserContent.insert(file.toLower()); + for (auto* file : mFiles) + file->setFromAnotherConfigFile(mNonUserContent.contains(file->fileName().toLower())); + + int insertPosition = 0; + while (mFiles.at(insertPosition)->builtIn()) + ++insertPosition; + + for (const auto& filepath : fileList) + { + const EsmFile* file = item(filepath); + int filePosition = indexFromItem(file).row(); + mFiles.move(filePosition, insertPosition++); + } + + sortFiles(); +} + bool ContentSelectorModel::ContentModel::isLoadOrderError(const EsmFile* file) const { return mPluginsWithLoadOrderError.contains(file->filePath()); diff --git a/components/contentselector/model/contentmodel.hpp b/components/contentselector/model/contentmodel.hpp index f754b9ea30..3cc05fd3cb 100644 --- a/components/contentselector/model/contentmodel.hpp +++ b/components/contentselector/model/contentmodel.hpp @@ -62,6 +62,7 @@ namespace ContentSelectorModel bool setCheckState(const QString& filepath, bool isChecked); bool isNew(const QString& filepath) const; void setNew(const QString& filepath, bool isChecked); + void setNonUserContent(const QStringList& fileList); void setContentList(const QStringList& fileList); ContentFileList checkedItems() const; void uncheckAll(); @@ -85,7 +86,7 @@ namespace ContentSelectorModel const EsmFile* mGameFile; ContentFileList mFiles; - QStringList mArchives; + QSet mNonUserContent; std::set mCheckedFiles; QHash mNewFiles; QSet mPluginsWithLoadOrderError; diff --git a/components/contentselector/view/contentselector.cpp b/components/contentselector/view/contentselector.cpp index 00c32e272d..a3fd224390 100644 --- a/components/contentselector/view/contentselector.cpp +++ b/components/contentselector/view/contentselector.cpp @@ -123,6 +123,11 @@ void ContentSelectorView::ContentSelector::buildContextMenu() mContextMenu->addAction(tr("&Copy Path(s) to Clipboard"), this, SLOT(slotCopySelectedItemsPaths())); } +void ContentSelectorView::ContentSelector::setNonUserContent(const QStringList& fileList) +{ + mContentModel->setNonUserContent(fileList); +} + void ContentSelectorView::ContentSelector::setProfileContent(const QStringList& fileList) { clearCheckStates(); @@ -336,4 +341,4 @@ void ContentSelectorView::ContentSelector::slotSearchFilterTextChanged(const QSt void ContentSelectorView::ContentSelector::slotRowsMoved() { ui->addonView->selectionModel()->clearSelection(); -} \ No newline at end of file +} diff --git a/components/contentselector/view/contentselector.hpp b/components/contentselector/view/contentselector.hpp index 2b739645ba..2fdd38c799 100644 --- a/components/contentselector/view/contentselector.hpp +++ b/components/contentselector/view/contentselector.hpp @@ -40,6 +40,7 @@ namespace ContentSelectorView void sortFiles(); bool containsDataFiles(const QString& path); void clearFiles(); + void setNonUserContent(const QStringList& fileList); void setProfileContent(const QStringList& fileList); void clearCheckStates();