From c22c9c271d6cfab22ac2f6c35714eeb266b2fa0c Mon Sep 17 00:00:00 2001 From: cc9cii Date: Sun, 14 Jun 2015 14:51:01 +1000 Subject: [PATCH] Allow comments (lines starting with # character) and blank lines in openmw.cfg. Should resolve Feature #2535. - allows moving various config entries up or down - comment lines above config entries stay as a pair --- apps/launcher/maindialog.cpp | 7 +- components/config/gamesettings.cpp | 209 +++++++++++++++++++++++++++++ components/config/gamesettings.hpp | 4 + 3 files changed, 216 insertions(+), 4 deletions(-) diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index fd36993bf..2f7a9db33 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -490,7 +490,7 @@ bool Launcher::MainDialog::writeSettings() // Game settings QFile file(userPath + QString("openmw.cfg")); - if (!file.open(QIODevice::ReadWrite | QIODevice::Text | QIODevice::Truncate)) { + if (!file.open(QIODevice::ReadWrite | QIODevice::Text)) { // File cannot be opened or created QMessageBox msgBox; msgBox.setWindowTitle(tr("Error writing OpenMW configuration file")); @@ -503,10 +503,8 @@ bool Launcher::MainDialog::writeSettings() return false; } - QTextStream stream(&file); - stream.setCodec(QTextCodec::codecForName("UTF-8")); - mGameSettings.writeFile(stream); + mGameSettings.writeFileWithComments(file); file.close(); // Graphics settings @@ -525,6 +523,7 @@ bool Launcher::MainDialog::writeSettings() return false; } + QTextStream stream(&file); stream.setDevice(&file); stream.setCodec(QTextCodec::codecForName("UTF-8")); diff --git a/components/config/gamesettings.cpp b/components/config/gamesettings.cpp index 0481235c7..4339369cf 100644 --- a/components/config/gamesettings.cpp +++ b/components/config/gamesettings.cpp @@ -1,6 +1,7 @@ #include "gamesettings.hpp" #include "launchersettings.hpp" +#include #include #include #include @@ -173,6 +174,214 @@ bool Config::GameSettings::writeFile(QTextStream &stream) return true; } +bool Config::GameSettings::isOrderedLine(const QString& line) const +{ + return line.contains(QRegExp("^\\s*fallback-archive\\s*=")) + || line.contains(QRegExp("^\\s*fallback\\s*=")) + || line.contains(QRegExp("^\\s*data\\s*=")) + || line.contains(QRegExp("^\\s*data-local\\s*=")) + || line.contains(QRegExp("^\\s*resources\\s*=")) + || line.contains(QRegExp("^\\s*content\\s*=")); +} + +// Policy: +// +// - Always ignore a line beginning with '#' or empty lines; added above a config +// entry. +// +// - If a line in file exists with matching key and first part of value (before ',', +// '\n', etc) also matches, then replace the line with that of mUserSettings. +// - else remove line +// +// - If there is no corresponding line in file, add at the end +// +bool Config::GameSettings::writeFileWithComments(QFile &file) +{ + QTextStream stream(&file); + stream.setCodec(QTextCodec::codecForName("UTF-8")); + + // slurp + std::vector fileCopy; + QString line = stream.readLine(); + while (!line.isNull()) + { + fileCopy.push_back(line); + line = stream.readLine(); + } + stream.seek(0); + + // empty file, no comments to keep + if (fileCopy.empty()) + return writeFile(stream); + + // start + // | + // | +----------------------------------------------------------+ + // | | | + // v v | + // skip non-"ordered" lines (remove "ordered" lines) | + // | ^ | + // | | | + // | non-"ordered" line, write saved comments | + // | ^ | + // v | | + // blank or comment line, save in temp buffer <--------+ | + // | | | | + // v +------- comment line ------+ | + // "ordered" line | + // | | + // v | + // save in a separate map of comments keyed by "ordered" line | + // | | + // +----------------------------------------------------------+ + // + // + QRegExp settingRegex("^([^=]+)\\s*=\\s*([^,]+)(.*)$"); + std::vector comments; + std::vector::iterator commentStart = fileCopy.end(); + std::map > commentsMap; + for (std::vector::iterator iter = fileCopy.begin(); iter != fileCopy.end(); ++iter) + { + if (isOrderedLine(*iter)) + { + // save in a separate map of comments keyed by "ordered" line + if (!comments.empty()) + { + if (settingRegex.indexIn(*iter) != -1) + { + commentsMap[settingRegex.cap(1)+"="+settingRegex.cap(2)] = comments; + comments.clear(); + commentStart = fileCopy.end(); + } + // else do nothing, malformed line + } + + *iter = QString(); // "ordered" lines to be removed later + } + else if ((*iter).isEmpty() || (*iter).contains(QRegExp("^\\s*#"))) + { + // comment line, save in temp buffer + if (comments.empty()) + commentStart = iter; + comments.push_back(*iter); + *iter = QString(); // assume to be deleted later + } + else + { + int index = settingRegex.indexIn(*iter); + + // blank or non-"ordered" line, write saved comments + if (!comments.empty() && index != -1 && settingRegex.captureCount() >= 2 && + mUserSettings.find(settingRegex.cap(1)) != mUserSettings.end()) + { + for (std::vector::const_iterator it = comments.begin(); it != comments.end(); ++it) + { + *commentStart = *it; + ++commentStart; + } + comments.clear(); + commentStart = fileCopy.end(); + } + + // keep blank lines and non-"ordered" lines other than comments + + // look for a key in the line + if (index == -1 || settingRegex.captureCount() < 2) + { + // no key or first part of value found in line, replace with a null string which + // will be remved later + *iter = QString(); + comments.clear(); + commentStart = fileCopy.end(); + continue; + } + + // look for a matching key in user settings + *iter = QString(); // assume no match + QString key = settingRegex.cap(1); + QString keyVal = settingRegex.cap(1)+"="+settingRegex.cap(2); + QMap::const_iterator i = mUserSettings.find(key); + while (i != mUserSettings.end() && i.key() == key) + { + QString settingLine = i.key() + "=" + i.value(); + if (settingRegex.indexIn(settingLine) != -1) + { + if ((settingRegex.cap(1)+"="+settingRegex.cap(2)) == keyVal) + { + *iter = settingLine; + break; + } + } + ++i; + } + } + } + + // comments at top of file + for (std::vector::iterator iter = fileCopy.begin(); iter != fileCopy.end(); ++iter) + { + if ((*iter).isNull()) + continue; + + // Below is based on readFile() code, if that changes corresponding change may be + // required (for example duplicates may be inserted if the rules don't match) + if (/*(*iter).isEmpty() ||*/ (*iter).contains(QRegExp("^\\s*#"))) + { + stream << *iter << "\n"; + continue; + } + } + + // Iterate in reverse order to preserve insertion order + QString settingLine; + QMapIterator it(mUserSettings); + it.toBack(); + + while (it.hasPrevious()) + { + it.previous(); + + // Quote paths with spaces + if ((it.key() == QLatin1String("data") + || it.key() == QLatin1String("data-local") + || it.key() == QLatin1String("resources")) && it.value().contains(QChar(' '))) + { + QString stripped = it.value(); + stripped.remove(QChar('\"')); // Remove quotes + + settingLine = it.key() + "=\"" + stripped + "\""; + } + else + settingLine = it.key() + "=" + it.value(); + + if (settingRegex.indexIn(settingLine) != -1) + { + std::map >::const_iterator i = + commentsMap.find(settingRegex.cap(1)+"="+settingRegex.cap(2)); + + if (i != commentsMap.end()) + { + std::vector cLines = i->second; + for (std::vector::const_iterator ci = cLines.begin(); ci != cLines.end(); ++ci) + stream << *ci << "\n"; + } + } + + stream << settingLine << "\n"; + } + + // flush any end comments + if (!comments.empty()) + { + for (std::vector::const_iterator ci = comments.begin(); ci != comments.end(); ++ci) + stream << *ci << "\n"; + } + + file.resize(file.pos()); + + return true; +} + bool Config::GameSettings::hasMaster() { bool result = false; diff --git a/components/config/gamesettings.hpp b/components/config/gamesettings.hpp index cc5033f35..992a3e565 100644 --- a/components/config/gamesettings.hpp +++ b/components/config/gamesettings.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -66,6 +67,7 @@ namespace Config bool readUserFile(QTextStream &stream); bool writeFile(QTextStream &stream); + bool writeFileWithComments(QFile &file); void setContentList(const QStringList& fileNames); QStringList getContentList() const; @@ -81,6 +83,8 @@ namespace Config QString mDataLocal; static const char sContentKey[]; + + bool isOrderedLine(const QString& line) const; }; } #endif // GAMESETTINGS_HPP