diff options
Diffstat (limited to 'src/settings.cpp')
| -rw-r--r-- | src/settings.cpp | 2424 |
1 files changed, 1615 insertions, 809 deletions
diff --git a/src/settings.cpp b/src/settings.cpp index 5cb2524f..5170a5de 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -18,75 +18,55 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. */ #include "settings.h" - -#include "pluginsetting.h" +#include "settingsutilities.h" #include "serverinfo.h" -#include "settingsdialog.h" -#include "versioninfo.h" +#include "executableslist.h" #include "appconfig.h" -#include "organizercore.h" +#include "env.h" +#include "envmetrics.h" +#include <expanderwidget.h> #include <utility.h> -#include <iplugin.h> #include <iplugingame.h> -#include <questionboxmemory.h> -#include <usvfsparameters.h> - -#include <QCheckBox> -#include <QCoreApplication> -#include <QComboBox> -#include <QDate> -#include <QDialog> -#include <QDir> -#include <QDirIterator> -#include <QFileInfo> -#include <QLineEdit> -#include <QSpinBox> -#include <QListWidgetItem> -#include <QLocale> -#include <QMessageBox> -#include <QApplication> -#include <QRegExp> -#include <QDir> -#include <QStringList> -#include <QVariantMap> -#include <QLabel> -#include <QPushButton> -#include <QPalette> -#include <Qt> // for Qt::UserRole, etc -#include <QtDebug> // for qDebug, qWarning - -#include <Windows.h> // For ShellExecuteW, HINSTANCE, etc -#include <wincred.h> // For storage +using namespace MOBase; -#include <algorithm> // for sort -#include <memory> -#include <stdexcept> // for runtime_error -#include <string> -#include <utility> // for pair, make_pair +EndorsementState endorsementStateFromString(const QString& s) +{ + if (s == "Endorsed") { + return EndorsementState::Accepted; + } else if (s == "Abstained") { + return EndorsementState::Refused; + } else { + return EndorsementState::NoDecision; + } +} -using namespace MOBase; +QString toString(EndorsementState s) +{ + switch (s) + { + case EndorsementState::Accepted: + return "Endorsed"; -template <typename T> -class QListWidgetItemEx : public QListWidgetItem { -public: - QListWidgetItemEx(const QString &text, int sortRole = Qt::DisplayRole, QListWidget *parent = 0, int type = Type) - : QListWidgetItem(text, parent, type), m_SortRole(sortRole) {} + case EndorsementState::Refused: + return "Abstained"; - virtual bool operator< ( const QListWidgetItem & other ) const { - return this->data(m_SortRole).value<T>() < other.data(m_SortRole).value<T>(); + case EndorsementState::NoDecision: // fall-through + default: + return {}; } -private: - int m_SortRole; -}; +} Settings *Settings::s_Instance = nullptr; - -Settings::Settings(const QSettings &settingsSource) - : m_Settings(settingsSource.fileName(), settingsSource.format()) +Settings::Settings(const QString& path) : + m_Settings(path, QSettings::IniFormat), + m_Game(m_Settings), m_Geometry(m_Settings), m_Widgets(m_Settings), + m_Colors(m_Settings), m_Plugins(m_Settings), m_Paths(m_Settings), + m_Network(m_Settings), m_Nexus(*this, m_Settings), m_Steam(*this, m_Settings), + m_Interface(m_Settings), m_Diagnostics(m_Settings) { if (s_Instance != nullptr) { throw std::runtime_error("second instance of \"Settings\" created"); @@ -95,13 +75,12 @@ Settings::Settings(const QSettings &settingsSource) } } - Settings::~Settings() { + MOBase::QuestionBoxMemory::setCallbacks({}, {}, {}); s_Instance = nullptr; } - Settings &Settings::instance() { if (s_Instance == nullptr) { @@ -110,447 +89,1188 @@ Settings &Settings::instance() return *s_Instance; } -void Settings::clearPlugins() +void Settings::processUpdates( + const QVersionNumber& currentVersion, const QVersionNumber& lastVersion) { - m_Plugins.clear(); - m_PluginSettings.clear(); + if (firstStart()) { + return; + } - m_PluginBlacklist.clear(); - int count = m_Settings.beginReadArray("pluginBlacklist"); - for (int i = 0; i < count; ++i) { - m_Settings.setArrayIndex(i); - m_PluginBlacklist.insert(m_Settings.value("name").toString()); + if (currentVersion == lastVersion) { + return; } - m_Settings.endArray(); + + log::info( + "updating from {} to {}", + lastVersion.toString(), currentVersion.toString()); + + auto version = [&](const QVersionNumber& v, auto&& f) { + if (lastVersion < v) { + log::debug("processing updates for {}", v.toString()); + f(); + } + }; + + version({2, 2, 0}, [&]{ + remove(m_Settings, "Settings", "steam_password"); + remove(m_Settings, "Settings", "nexus_username"); + remove(m_Settings, "Settings", "nexus_password"); + remove(m_Settings, "Settings", "nexus_login"); + remove(m_Settings, "Settings", "nexus_api_key"); + remove(m_Settings, "Settings", "ask_for_nexuspw"); + remove(m_Settings, "Settings", "nmm_version"); + + removeSection(m_Settings, "Servers"); + }); + + version({2, 2, 1}, [&]{ + remove(m_Settings, "General", "mod_info_tabs"); + remove(m_Settings, "General", "mod_info_conflict_expanders"); + remove(m_Settings, "General", "mod_info_conflicts"); + remove(m_Settings, "General", "mod_info_advanced_conflicts"); + remove(m_Settings, "General", "mod_info_conflicts_overwrite"); + remove(m_Settings, "General", "mod_info_conflicts_noconflict"); + remove(m_Settings, "General", "mod_info_conflicts_overwritten"); + }); + + version({2, 2, 2}, [&]{ + // log splitter is gone, it's a dock now + remove(m_Settings, "General", "log_split"); + + // moved to widgets + remove(m_Settings, "General", "mod_info_conflicts_tab"); + remove(m_Settings, "General", "mod_info_conflicts_general_expanders"); + remove(m_Settings, "General", "mod_info_conflicts_general_overwrite"); + remove(m_Settings, "General", "mod_info_conflicts_general_noconflict"); + remove(m_Settings, "General", "mod_info_conflicts_general_overwritten"); + remove(m_Settings, "General", "mod_info_conflicts_advanced_list"); + remove(m_Settings, "General", "mod_info_conflicts_advanced_options"); + remove(m_Settings, "General", "mod_info_tab_order"); + remove(m_Settings, "General", "mod_info_dialog_images_show_dds"); + + // moved to geometry + remove(m_Settings, "General", "window_geometry"); + remove(m_Settings, "General", "window_state"); + remove(m_Settings, "General", "toolbar_size"); + remove(m_Settings, "General", "toolbar_button_style"); + remove(m_Settings, "General", "menubar_visible"); + remove(m_Settings, "General", "statusbar_visible"); + remove(m_Settings, "General", "window_split"); + remove(m_Settings, "General", "window_monitor"); + remove(m_Settings, "General", "browser_geometry"); + remove(m_Settings, "General", "filters_visible"); + + // this was supposed to have been removed above when updating from 2.2.0, + // but it wasn't in Settings, it was in General + remove(m_Settings, "General", "ask_for_nexuspw"); + + m_Network.updateFromOldMap(); + }); + + //save version in all case + set(m_Settings, "General", "version", currentVersion.toString()); + + log::debug("updating done"); } -bool Settings::pluginBlacklisted(const QString &fileName) const +QString Settings::filename() const { - return m_PluginBlacklist.contains(fileName); + return m_Settings.fileName(); } -void Settings::registerAsNXMHandler(bool force) +bool Settings::checkForUpdates() const { - const auto nxmPath = QCoreApplication::applicationDirPath() + "/nxmhandler.exe"; - const auto executable = QCoreApplication::applicationFilePath(); + return get<bool>(m_Settings, "Settings", "check_for_updates", true); +} - QString mode = force ? "forcereg" : "reg"; - QString parameters = mode + " " + m_GamePlugin->gameShortName(); - for (const QString& altGame : m_GamePlugin->validShortNames()) { - parameters += "," + altGame; +void Settings::setCheckForUpdates(bool b) +{ + set(m_Settings, "Settings", "check_for_updates", b); +} + +bool Settings::usePrereleases() const +{ + return get<bool>(m_Settings, "Settings", "use_prereleases", false); +} + +void Settings::setUsePrereleases(bool b) +{ + set(m_Settings, "Settings", "use_prereleases", b); +} + +std::optional<QVersionNumber> Settings::version() const +{ + if (auto v=getOptional<QString>(m_Settings, "General", "version")) { + return QVersionNumber::fromString(*v).normalized(); } - parameters += " \"" + executable + "\""; - if (!shell::Execute(nxmPath, parameters)) { - QMessageBox::critical( - nullptr, tr("Failed"), tr("Failed to start the helper application")); + return {}; +} + +bool Settings::firstStart() const +{ + return get<bool>(m_Settings, "General", "first_start", true); +} + +void Settings::setFirstStart(bool b) +{ + set(m_Settings, "General", "first_start", b); +} + +QString Settings::executablesBlacklist() const +{ + static const QString def = (QStringList() + << "Chrome.exe" + << "Firefox.exe" + << "TSVNCache.exe" + << "TGitCache.exe" + << "Steam.exe" + << "GameOverlayUI.exe" + << "Discord.exe" + << "GalaxyClient.exe" + << "Spotify.exe" + ).join(";"); + + return get<QString>(m_Settings, "Settings", "executable_blacklist", def); +} + +bool Settings::isExecutableBlacklisted(const QString& s) const +{ + for (auto exec : executablesBlacklist().split(";")) { + if (exec.compare(s, Qt::CaseInsensitive) == 0) { + return true; + } + } + + return false; +} + +void Settings::setExecutablesBlacklist(const QString& s) +{ + set(m_Settings, "Settings", "executable_blacklist", s); +} + +void Settings::setMotdHash(uint hash) +{ + set(m_Settings, "General", "motd_hash", hash); +} + +unsigned int Settings::motdHash() const +{ + return get<unsigned int>(m_Settings, "General", "motd_hash", 0); +} + +bool Settings::archiveParsing() const +{ + return get<bool>(m_Settings, "Settings", "archive_parsing_experimental", false); +} + +void Settings::setArchiveParsing(bool b) +{ + set(m_Settings, "Settings", "archive_parsing_experimental", b); +} + +std::vector<std::map<QString, QVariant>> Settings::executables() const +{ + ScopedReadArray sra(m_Settings, "customExecutables"); + std::vector<std::map<QString, QVariant>> v; + + sra.for_each([&]{ + std::map<QString, QVariant> map; + + for (auto&& key : sra.keys()) { + map[key] = sra.get<QVariant>(key); + } + + v.push_back(map); + }); + + return v; +} + +void Settings::setExecutables(const std::vector<std::map<QString, QVariant>>& v) +{ + const auto current = executables(); + + if (current == v) { + // no change + return; } + + if (current.size() > v.size()) { + // Qt can't remove array elements, the section must be cleared + removeSection(m_Settings, "customExecutables"); + } + + ScopedWriteArray swa(m_Settings, "customExecutables", v.size()); + + for (const auto& map : v) { + swa.next(); + + for (auto&& p : map) { + swa.set(p.first, p.second); + } + } +} + +bool Settings::keepBackupOnInstall() const +{ + return get<bool>(m_Settings, "General", "backup_install", false); +} + +void Settings::setKeepBackupOnInstall(bool b) +{ + set(m_Settings, "General", "backup_install", b); +} + +GameSettings& Settings::game() +{ + return m_Game; +} + +const GameSettings& Settings::game() const +{ + return m_Game; +} + +GeometrySettings& Settings::geometry() +{ + return m_Geometry; +} + +const GeometrySettings& Settings::geometry() const +{ + return m_Geometry; +} + +WidgetSettings& Settings::widgets() +{ + return m_Widgets; +} + +const WidgetSettings& Settings::widgets() const +{ + return m_Widgets; +} + +ColorSettings& Settings::colors() +{ + return m_Colors; +} + +const ColorSettings& Settings::colors() const +{ + return m_Colors; +} + +PluginSettings& Settings::plugins() +{ + return m_Plugins; +} + +const PluginSettings& Settings::plugins() const +{ + return m_Plugins; +} + +PathSettings& Settings::paths() +{ + return m_Paths; +} + +const PathSettings& Settings::paths() const +{ + return m_Paths; } -bool Settings::colorSeparatorScrollbar() const +NetworkSettings& Settings::network() { - return m_Settings.value("Settings/colorSeparatorScrollbars", true).toBool(); + return m_Network; +} + +const NetworkSettings& Settings::network() const +{ + return m_Network; +} + +NexusSettings& Settings::nexus() +{ + return m_Nexus; +} + +const NexusSettings& Settings::nexus() const +{ + return m_Nexus; +} + +SteamSettings& Settings::steam() +{ + return m_Steam; +} + +const SteamSettings& Settings::steam() const +{ + return m_Steam; +} + +InterfaceSettings& Settings::interface() +{ + return m_Interface; +} + +const InterfaceSettings& Settings::interface() const +{ + return m_Interface; +} + +DiagnosticsSettings& Settings::diagnostics() +{ + return m_Diagnostics; +} + +const DiagnosticsSettings& Settings::diagnostics() const +{ + return m_Diagnostics; +} + +QSettings::Status Settings::sync() const +{ + m_Settings.sync(); + return m_Settings.status(); +} + +void Settings::dump() const +{ + static const QStringList ignore({ + "username", "password", "nexus_api_key" + }); + + log::debug("settings:"); + + { + ScopedGroup sg(m_Settings, "Settings"); + + for (auto k : m_Settings.allKeys()) { + if (ignore.contains(k, Qt::CaseInsensitive)) { + continue; + } + + log::debug(" . {}={}", k, m_Settings.value(k).toString()); + } + } + + m_Network.dump(); } void Settings::managedGameChanged(IPluginGame const *gamePlugin) { + m_Game.setPlugin(gamePlugin); +} + + +GameSettings::GameSettings(QSettings& settings) + : m_Settings(settings), m_GamePlugin(nullptr) +{ +} + +const MOBase::IPluginGame* GameSettings::plugin() +{ + return m_GamePlugin; +} + +void GameSettings::setPlugin(const MOBase::IPluginGame* gamePlugin) +{ m_GamePlugin = gamePlugin; } -void Settings::registerPlugin(IPlugin *plugin) +bool GameSettings::forceEnableCoreFiles() const { - m_Plugins.push_back(plugin); - m_PluginSettings.insert(plugin->name(), QVariantMap()); - m_PluginDescriptions.insert(plugin->name(), QVariantMap()); - for (const PluginSetting &setting : plugin->settings()) { - QVariant temp = m_Settings.value("Plugins/" + plugin->name() + "/" + setting.key, setting.defaultValue); - if (!temp.convert(setting.defaultValue.type())) { - qWarning("failed to interpret \"%s\" as correct type for \"%s\" in plugin \"%s\", using default", - qUtf8Printable(temp.toString()), qUtf8Printable(setting.key), qUtf8Printable(plugin->name())); - temp = setting.defaultValue; - } - m_PluginSettings[plugin->name()][setting.key] = temp; - m_PluginDescriptions[plugin->name()][setting.key] = QString("%1 (default: %2)").arg(setting.description).arg(setting.defaultValue.toString()); + return get<bool>(m_Settings, "Settings", "force_enable_core_files", true); +} + +void GameSettings::setForceEnableCoreFiles(bool b) +{ + set(m_Settings, "Settings", "force_enable_core_files", b); +} + +std::optional<QString> GameSettings::directory() const +{ + if (auto v=getOptional<QByteArray>(m_Settings, "General", "gamePath")) { + return QString::fromUtf8(*v); } + + return {}; } -bool Settings::obfuscate(const QString key, const QString data) +void GameSettings::setDirectory(const QString& path) { - QString finalKey("ModOrganizer2_" + key); - wchar_t* keyData = new wchar_t[finalKey.size()+1]; - finalKey.toWCharArray(keyData); - keyData[finalKey.size()] = L'\0'; - bool result = false; - if (data.isEmpty()) { - result = CredDeleteW(keyData, CRED_TYPE_GENERIC, 0); - if (!result) - if (GetLastError() == ERROR_NOT_FOUND) - result = true; - } else { - wchar_t* charData = new wchar_t[data.size()]; - data.toWCharArray(charData); + set(m_Settings, "General", "gamePath", QDir::toNativeSeparators(path).toUtf8()); +} - CREDENTIALW cred = {}; - cred.Flags = 0; - cred.Type = CRED_TYPE_GENERIC; - cred.TargetName = keyData; - cred.CredentialBlob = (LPBYTE)charData; - cred.CredentialBlobSize = sizeof(wchar_t) * data.size(); - cred.Persist = CRED_PERSIST_LOCAL_MACHINE; +std::optional<QString> GameSettings::name() const +{ + return getOptional<QString>(m_Settings, "General", "gameName"); +} - result = CredWriteW(&cred, 0); - delete[] charData; +void GameSettings::setName(const QString& name) +{ + set(m_Settings, "General", "gameName", name); +} + +std::optional<QString> GameSettings::edition() const +{ + return getOptional<QString>(m_Settings, "General", "game_edition"); +} + +void GameSettings::setEdition(const QString& name) +{ + set(m_Settings, "General", "game_edition", name); +} + +std::optional<QString> GameSettings::selectedProfileName() const +{ + if (auto v=getOptional<QByteArray>(m_Settings, "General", "selected_profile")) { + return QString::fromUtf8(*v); } - delete[] keyData; - return result; + + return {}; } -QString Settings::deObfuscate(const QString key) +void GameSettings::setSelectedProfileName(const QString& name) { - QString result; - QString finalKey("ModOrganizer2_" + key); - wchar_t* keyData = new wchar_t[finalKey.size()+1]; - finalKey.toWCharArray(keyData); - keyData[finalKey.size()] = L'\0'; - PCREDENTIALW creds; - if (CredReadW(keyData, 1, 0, &creds)) { - wchar_t *charData = (wchar_t *)creds->CredentialBlob; - result = QString::fromWCharArray(charData, creds->CredentialBlobSize / sizeof(wchar_t)); - CredFree(creds); - } else { - const auto e = GetLastError(); - if (e != ERROR_NOT_FOUND) { - qCritical().nospace() - << "Retrieving encrypted data failed: " - << formatSystemMessageQ(e); + set(m_Settings, "General", "selected_profile", name.toUtf8()); +} + +LoadMechanism::EMechanism GameSettings::loadMechanismType() const +{ + const auto def = LoadMechanism::LOAD_MODORGANIZER; + + const auto i = get<LoadMechanism::EMechanism>(m_Settings, + "Settings", "load_mechanism", def); + + switch (i) + { + // ok + case LoadMechanism::LOAD_MODORGANIZER: // fall-through + { + break; + } + + default: + { + log::error( + "invalid load mechanism {}, reverting to {}", + static_cast<int>(i), toString(def)); + + set(m_Settings, "Settings", "load_mechanism", def); + + return def; } } - delete[] keyData; - return result; + + return i; } -QColor Settings::getIdealTextColor(const QColor& rBackgroundColor) +void GameSettings::setLoadMechanism(LoadMechanism::EMechanism m) { - if (rBackgroundColor.alpha() == 0) - return QColor(Qt::black); + set(m_Settings, "Settings", "load_mechanism", m); +} - const int THRESHOLD = 106 * 255.0f / rBackgroundColor.alpha(); - int BackgroundDelta = (rBackgroundColor.red() * 0.299) + (rBackgroundColor.green() * 0.587) + (rBackgroundColor.blue() * 0.114); - return QColor((255 - BackgroundDelta <= THRESHOLD) ? Qt::black : Qt::white); +const LoadMechanism& GameSettings::loadMechanism() const +{ + return m_LoadMechanism; } +LoadMechanism& GameSettings::loadMechanism() +{ + return m_LoadMechanism; +} -bool Settings::hideUncheckedPlugins() const +bool GameSettings::hideUncheckedPlugins() const { - return m_Settings.value("Settings/hide_unchecked_plugins", false).toBool(); + return get<bool>(m_Settings, "Settings", "hide_unchecked_plugins", false); } -bool Settings::forceEnableCoreFiles() const +void GameSettings::setHideUncheckedPlugins(bool b) { - return m_Settings.value("Settings/force_enable_core_files", true).toBool(); + set(m_Settings, "Settings", "hide_unchecked_plugins", b); } -bool Settings::lockGUI() const + +GeometrySettings::GeometrySettings(QSettings& s) + : m_Settings(s), m_Reset(false) { - return m_Settings.value("Settings/lock_gui", true).toBool(); } -bool Settings::automaticLoginEnabled() const +void GeometrySettings::requestReset() { - return m_Settings.value("Settings/nexus_login", false).toBool(); + m_Reset = true; } -QString Settings::getSteamAppID() const +void GeometrySettings::resetIfNeeded() { - return m_Settings.value("Settings/app_id", m_GamePlugin->steamAPPId()).toString(); + if (!m_Reset) { + return; + } + + removeSection(m_Settings, "Geometry"); } -bool Settings::usePrereleases() const +void GeometrySettings::saveGeometry(const QMainWindow* w) { - return m_Settings.value("Settings/use_prereleases", false).toBool(); + saveWindowGeometry(w); } -void Settings::setDownloadSpeed(const QString &serverName, int bytesPerSecond) +bool GeometrySettings::restoreGeometry(QMainWindow* w) const { - m_Settings.beginGroup("Servers"); + return restoreWindowGeometry(w); +} - for (const QString &serverKey : m_Settings.childKeys()) { - QVariantMap data = m_Settings.value(serverKey).toMap(); - if (serverKey == serverName) { - data["downloadCount"] = data["downloadCount"].toInt() + 1; - data["downloadSpeed"] = data["downloadSpeed"].toDouble() + static_cast<double>(bytesPerSecond); - m_Settings.setValue(serverKey, data); - } +void GeometrySettings::saveGeometry(const QDialog* d) +{ + saveWindowGeometry(d); +} + +bool GeometrySettings::restoreGeometry(QDialog* d) const +{ + const auto r = restoreWindowGeometry(d); + + if (centerDialogs()) { + centerOnParent(d); } - m_Settings.endGroup(); - m_Settings.sync(); + return r; +} + +void GeometrySettings::saveWindowGeometry(const QWidget* w) +{ + set(m_Settings, "Geometry", geoSettingName(w), w->saveGeometry()); } -std::map<QString, int> Settings::getPreferredServers() +bool GeometrySettings::restoreWindowGeometry(QWidget* w) const { - std::map<QString, int> result; - m_Settings.beginGroup("Servers"); + if (auto v=getOptional<QByteArray>(m_Settings, "Geometry", geoSettingName(w))) { + w->restoreGeometry(*v); + ensureWindowOnScreen(w); + return true; + } - for (const QString &serverKey : m_Settings.childKeys()) { - QVariantMap data = m_Settings.value(serverKey).toMap(); - int preference = data["preferred"].toInt(); - if (preference > 0) { - result[serverKey] = preference; + return false; +} + +void GeometrySettings::ensureWindowOnScreen(QWidget* w) const +{ + // users report that the main window and/or dialogs are displayed off-screen; + // the usual workaround is keyboard navigation to move it + // + // qt should have code that deals with multiple monitors and off-screen + // geometries, but there seems to be bugs or inconsistencies that can't be + // reproduced + // + // the closest would probably be https://bugreports.qt.io/browse/QTBUG-64498, + // which is about multiple monitors and high dpi, but it seems fixed as of + // 5.12.4, which is shipped with 2.2.1 + // + // without being to reproduce the problem, some simple checks are made in a + // timer, which may mitigate the issues + + QTimer::singleShot(100, w, [w] { + const auto borders = 20; + + // desktop geometry, made smaller to make sure there isn't just a few pixels + const auto originalDg = env::Environment().metrics().desktopGeometry(); + const auto dg = originalDg.adjusted(borders, borders, -borders, -borders); + + const auto g = w->geometry(); + + if (!dg.intersects(g)) { + log::warn( + "window '{}' is offscreen, moving to main monitor; geo={}, desktop={}", + w->objectName(), g, originalDg); + + // widget is off-screen, center it on main monitor + centerOnMonitor(w, -1); + + log::warn("window '{}' now at {}", w->objectName(), w->geometry()); } + }); +} + +void GeometrySettings::saveState(const QMainWindow* w) +{ + set(m_Settings, "Geometry", stateSettingName(w), w->saveState()); +} + +bool GeometrySettings::restoreState(QMainWindow* w) const +{ + if (auto v=getOptional<QByteArray>(m_Settings, "Geometry", stateSettingName(w))) { + w->restoreState(*v); + return true; } - m_Settings.endGroup(); - return result; + return false; } -QString Settings::getConfigurablePath(const QString &key, - const QString &def, - bool resolve) const +void GeometrySettings::saveState(const QHeaderView* w) { - QString result = QDir::fromNativeSeparators( - m_Settings.value(QString("settings/") + key, QString("%BASE_DIR%/") + def) - .toString()); - if (resolve) { - result.replace("%BASE_DIR%", getBaseDirectory()); + set(m_Settings, "Geometry", stateSettingName(w), w->saveState()); +} + +bool GeometrySettings::restoreState(QHeaderView* w) const +{ + if (auto v=getOptional<QByteArray>(m_Settings, "Geometry", stateSettingName(w))) { + w->restoreState(*v); + return true; } - return result; + + return false; } -QString Settings::getBaseDirectory() const +void GeometrySettings::saveState(const QSplitter* w) { - return QDir::fromNativeSeparators(m_Settings.value( - "settings/base_directory", qApp->property("dataPath").toString()).toString()); + set(m_Settings, "Geometry", stateSettingName(w), w->saveState()); } -QString Settings::getDownloadDirectory(bool resolve) const +bool GeometrySettings::restoreState(QSplitter* w) const { - return getConfigurablePath("download_directory", ToQString(AppConfig::downloadPath()), resolve); + if (auto v=getOptional<QByteArray>(m_Settings, "Geometry", stateSettingName(w))) { + w->restoreState(*v); + return true; + } + + return false; } -QString Settings::getCacheDirectory(bool resolve) const +void GeometrySettings::saveState(const ExpanderWidget* expander) { - return getConfigurablePath("cache_directory", ToQString(AppConfig::cachePath()), resolve); + set(m_Settings, "Geometry", stateSettingName(expander), expander->saveState()); } -QString Settings::getModDirectory(bool resolve) const +bool GeometrySettings::restoreState(ExpanderWidget* expander) const { - return getConfigurablePath("mod_directory", ToQString(AppConfig::modsPath()), resolve); + if (auto v=getOptional<QByteArray>(m_Settings, "Geometry", stateSettingName(expander))) { + expander->restoreState(*v); + return true; + } + + return false; } -QString Settings::getManagedGameDirectory() const +void GeometrySettings::saveVisibility(const QWidget* w) { - return m_Settings.value("gamePath", "").toString(); + set(m_Settings, "Geometry", visibilitySettingName(w), w->isVisible()); } -QString Settings::getProfileDirectory(bool resolve) const +bool GeometrySettings::restoreVisibility(QWidget* w, std::optional<bool> def) const { - return getConfigurablePath("profiles_directory", ToQString(AppConfig::profilesPath()), resolve); + if (auto v=getOptional<bool>(m_Settings, "Geometry", visibilitySettingName(w), def)) { + w->setVisible(*v); + return true; + } + + return false; } -QString Settings::getOverwriteDirectory(bool resolve) const +void GeometrySettings::restoreToolbars(QMainWindow* w) const { - return getConfigurablePath("overwrite_directory", - ToQString(AppConfig::overwritePath()), resolve); + // all toolbars have the same size and button style settings + const auto size = getOptional<QSize>(m_Settings, "Geometry", "toolbar_size"); + const auto style = getOptional<int>(m_Settings, "Geometry", "toolbar_button_style"); + + for (auto* tb : w->findChildren<QToolBar*>()) { + if (size) { + tb->setIconSize(*size); + } + + if (style) { + tb->setToolButtonStyle(static_cast<Qt::ToolButtonStyle>(*style)); + } + + restoreVisibility(tb); + } } -bool Settings::getNexusApiKey(QString &apiKey) const +void GeometrySettings::saveToolbars(const QMainWindow* w) { - QString tempKey = deObfuscate("APIKEY"); - if (tempKey.isEmpty()) - return false; - apiKey = tempKey; - return true; + const auto tbs = w->findChildren<QToolBar*>(); + + // save visibility for all + for (auto* tb : tbs) { + saveVisibility(tb); + } + + // all toolbars have the same size and button style settings, just save the + // first one + if (!tbs.isEmpty()) { + const auto* tb = tbs[0]; + + set(m_Settings, "Geometry", "toolbar_size", tb->iconSize()); + set(m_Settings, "Geometry", "toolbar_button_style", static_cast<int>(tb->toolButtonStyle())); + } } -bool Settings::setNexusApiKey(const QString& apiKey) +QStringList GeometrySettings::modInfoTabOrder() const { - if (!obfuscate("APIKEY", apiKey)) { - const auto e = GetLastError(); + QStringList v; - qCritical().nospace() - << "Storing API key failed: " - << formatSystemMessageQ(e); + if (m_Settings.contains("mod_info_tabs")) { + // old byte array from 2.2.0 + QDataStream stream(m_Settings.value("mod_info_tabs").toByteArray()); - return false; + int count = 0; + stream >> count; + + for (int i=0; i<count; ++i) { + QString s; + stream >> s; + v.push_back(s); + } + } else { + // string list since 2.2.1 + QString string = get<QString>(m_Settings, "Widgets", "ModInfoTabOrder", ""); + QTextStream stream(&string); + + while (!stream.atEnd()) { + QString s; + stream >> s; + v.push_back(s); + } } - return true; + return v; } -bool Settings::clearNexusApiKey() +void GeometrySettings::setModInfoTabOrder(const QString& names) { - return setNexusApiKey(""); + set(m_Settings, "Widgets", "ModInfoTabOrder", names); } -bool Settings::hasNexusApiKey() const +bool GeometrySettings::centerDialogs() const { - return !deObfuscate("APIKEY").isEmpty(); + return get<bool>(m_Settings, "Settings", "center_dialogs", false); } -bool Settings::getSteamLogin(QString &username, QString &password) const +void GeometrySettings::setCenterDialogs(bool b) { - if (m_Settings.contains("Settings/steam_username")) { - QString tempPass = deObfuscate("steam_password"); - if (!tempPass.isEmpty()) { - username = m_Settings.value("Settings/steam_username").toString(); - password = tempPass; - return true; + set(m_Settings, "Settings", "center_dialogs", b); +} + +void GeometrySettings::centerOnMainWindowMonitor(QWidget* w) +{ + const auto monitor = getOptional<int>( + m_Settings, "Geometry", "MainWindow_monitor").value_or(-1); + + centerOnMonitor(w, monitor); +} + +void GeometrySettings::centerOnMonitor(QWidget* w, int monitor) +{ + QPoint center; + + if (monitor >= 0 && monitor < QGuiApplication::screens().size()) { + center = QGuiApplication::screens().at(monitor)->geometry().center(); + } else { + center = QGuiApplication::primaryScreen()->geometry().center(); + } + + w->move(center - w->rect().center()); +} + +void GeometrySettings::centerOnParent(QWidget* w, QWidget* parent) +{ + if (!parent) { + parent = w->parentWidget(); + + if (!parent) { + parent = qApp->activeWindow(); + } + } + + if (parent && parent->isVisible()) { + const auto pr = parent->geometry(); + w->move(pr.center() - w->rect().center()); + } +} + +void GeometrySettings::saveMainWindowMonitor(const QMainWindow* w) +{ + if (auto* handle=w->windowHandle()) { + if (auto* screen = handle->screen()) { + const int screenId = QGuiApplication::screens().indexOf(screen); + set(m_Settings, "Geometry", "MainWindow_monitor", screenId); } } - return false; } -bool Settings::compactDownloads() const + +Qt::Orientation dockOrientation(const QMainWindow* mw, const QDockWidget* d) { - return m_Settings.value("Settings/compact_downloads", false).toBool(); + // docks in these areas are horizontal + const auto horizontalAreas = + Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea; + + if (mw->dockWidgetArea(const_cast<QDockWidget*>(d)) & horizontalAreas) { + return Qt::Horizontal; + } else { + return Qt::Vertical; + } } -bool Settings::metaDownloads() const +void GeometrySettings::saveDocks(const QMainWindow* mw) { - return m_Settings.value("Settings/meta_downloads", false).toBool(); + // this attempts to fix https://bugreports.qt.io/browse/QTBUG-46620 where dock + // sizes are not restored when the main window is maximized; it is used in + // MainWindow::readSettings() and MainWindow::storeSettings() + // + // there's also https://stackoverflow.com/questions/44005852, which has what + // seems to be a popular fix, but it breaks the restored size of the window + // by setting it to the desktop's resolution, so that doesn't work + // + // the only fix I could find is to remember the sizes of the docks and manually + // setting them back; saving is straightforward, but restoring is messy + // + // this also depends on the window being visible before the timer in restore() + // is fired and the timer must be processed by application.exec(); therefore, + // the splash screen _must_ be closed before readSettings() is called, because + // it has its own event loop, which seems to interfere with this + // + // all of this should become unnecessary when QTBUG-46620 is fixed + // + + // saves the size of each dock + for (const auto* dock : mw->findChildren<QDockWidget*>()) { + int size = 0; + + // save the width for horizontal docks, or the height for vertical + if (dockOrientation(mw, dock) == Qt::Horizontal) { + size = dock->size().width(); + } else { + size = dock->size().height(); + } + + set(m_Settings, "Geometry", dockSettingName(dock), size); + } } -bool Settings::offlineMode() const +void GeometrySettings::restoreDocks(QMainWindow* mw) const { - return m_Settings.value("Settings/offline_mode", false).toBool(); + struct DockInfo + { + QDockWidget* d; + int size = 0; + Qt::Orientation ori; + }; + + std::vector<DockInfo> dockInfos; + + // for each dock + for (auto* dock : mw->findChildren<QDockWidget*>()) { + if (auto size=getOptional<int>(m_Settings, "Geometry", dockSettingName(dock))) { + // remember this dock, its size and orientation + dockInfos.push_back({dock, *size, dockOrientation(mw, dock)}); + } + } + + // the main window must have had time to process the settings from + // readSettings() or it seems to override whatever is set here + // + // some people said a single processEvents() call is enough, but it doesn't + // look like it + QTimer::singleShot(5, [=] { + for (const auto& info : dockInfos) { + mw->resizeDocks({info.d}, {info.size}, info.ori); + } + }); } -int Settings::logLevel() const + +WidgetSettings::WidgetSettings(QSettings& s) + : m_Settings(s) { - return m_Settings.value("Settings/log_level", static_cast<int>(LogLevel::Info)).toInt(); + MOBase::QuestionBoxMemory::setCallbacks( + [this](auto&& w, auto&& f){ return questionButton(w, f); }, + [this](auto&& w, auto&& b){ setQuestionWindowButton(w, b); }, + [this](auto&& w, auto&& f, auto&& b){ setQuestionFileButton(w, f, b); }); } -int Settings::crashDumpsType() const +std::optional<int> WidgetSettings::index(const QComboBox* cb) const { - return m_Settings.value("Settings/crash_dumps_type", static_cast<int>(CrashDumpsType::Mini)).toInt(); + return getOptional<int>(m_Settings, "Widgets", indexSettingName(cb)); } -int Settings::crashDumpsMax() const +void WidgetSettings::saveIndex(const QComboBox* cb) { - return m_Settings.value("Settings/crash_dumps_max", 5).toInt(); + set(m_Settings, "Widgets", indexSettingName(cb), cb->currentIndex()); } -QColor Settings::modlistOverwrittenLooseColor() const +void WidgetSettings::restoreIndex(QComboBox* cb, std::optional<int> def) const { - return m_Settings.value("Settings/overwrittenLooseFilesColor", QColor(0, 255, 0, 64)).value<QColor>(); + if (auto v=getOptional<int>(m_Settings, "Widgets", indexSettingName(cb), def)) { + cb->setCurrentIndex(*v); + } } -QColor Settings::modlistOverwritingLooseColor() const +std::optional<int> WidgetSettings::index(const QTabWidget* w) const { - return m_Settings.value("Settings/overwritingLooseFilesColor", QColor(255, 0, 0, 64)).value<QColor>(); + return getOptional<int>(m_Settings, "Widgets", indexSettingName(w)); } -QColor Settings::modlistOverwrittenArchiveColor() const +void WidgetSettings::saveIndex(const QTabWidget* w) { - return m_Settings.value("Settings/overwrittenArchiveFilesColor", QColor(0, 255, 255, 64)).value<QColor>(); + set(m_Settings, "Widgets", indexSettingName(w), w->currentIndex()); } -QColor Settings::modlistOverwritingArchiveColor() const +void WidgetSettings::restoreIndex(QTabWidget* w, std::optional<int> def) const { - return m_Settings.value("Settings/overwritingArchiveFilesColor", QColor(255, 0, 255, 64)).value<QColor>(); + if (auto v=getOptional<int>(m_Settings, "Widgets", indexSettingName(w), def)) { + w->setCurrentIndex(*v); + } } -QColor Settings::modlistContainsPluginColor() const +std::optional<bool> WidgetSettings::checked(const QAbstractButton* w) const { - return m_Settings.value("Settings/containsPluginColor", QColor(0, 0, 255, 64)).value<QColor>(); + warnIfNotCheckable(w); + return getOptional<bool>(m_Settings, "Widgets", checkedSettingName(w)); } -QColor Settings::pluginListContainedColor() const +void WidgetSettings::saveChecked(const QAbstractButton* w) { - return m_Settings.value("Settings/containedColor", QColor(0, 0, 255, 64)).value<QColor>(); + warnIfNotCheckable(w); + set(m_Settings, "Widgets", checkedSettingName(w), w->isChecked()); } -QString Settings::executablesBlacklist() const +void WidgetSettings::restoreChecked(QAbstractButton* w, std::optional<bool> def) const { - return m_Settings.value("Settings/executable_blacklist", ( - QStringList() - << "Chrome.exe" - << "Firefox.exe" - << "TSVNCache.exe" - << "TGitCache.exe" - << "Steam.exe" - << "GameOverlayUI.exe" - << "Discord.exe" - << "GalaxyClient.exe" - << "Spotify.exe" - ).join(";") - ).toString(); + warnIfNotCheckable(w); + + if (auto v=getOptional<bool>(m_Settings, "Widgets", checkedSettingName(w), def)) { + w->setChecked(*v); + } } -void Settings::setSteamLogin(QString username, QString password) +QuestionBoxMemory::Button WidgetSettings::questionButton( + const QString& windowName, const QString& filename) const { - if (username == "") { - m_Settings.remove("Settings/steam_username"); - password = ""; - } else { - m_Settings.setValue("Settings/steam_username", username); + const QString sectionName("DialogChoices"); + + if (!filename.isEmpty()) { + const auto fileSetting = windowName + "/" + filename; + if (auto v=getOptional<int>(m_Settings, sectionName, fileSetting)) { + return static_cast<QuestionBoxMemory::Button>(*v); + } } - if (!obfuscate("steam_password", password)) { - const auto e = GetLastError(); - qCritical().nospace() - << "Storing or deleting password failed: " - << formatSystemMessageQ(e); + + if (auto v=getOptional<int>(m_Settings, sectionName, windowName)) { + return static_cast<QuestionBoxMemory::Button>(*v); } + + return QuestionBoxMemory::NoButton; } -LoadMechanism::EMechanism Settings::getLoadMechanism() const +void WidgetSettings::setQuestionWindowButton( + const QString& windowName, QuestionBoxMemory::Button button) { - switch (m_Settings.value("Settings/load_mechanism").toInt()) { - case LoadMechanism::LOAD_MODORGANIZER: return LoadMechanism::LOAD_MODORGANIZER; - case LoadMechanism::LOAD_SCRIPTEXTENDER: return LoadMechanism::LOAD_SCRIPTEXTENDER; - case LoadMechanism::LOAD_PROXYDLL: return LoadMechanism::LOAD_PROXYDLL; + const QString sectionName("DialogChoices"); + + if (button == QuestionBoxMemory::NoButton) { + remove(m_Settings, sectionName, windowName); + } else { + set(m_Settings, sectionName, windowName, button); } - throw std::runtime_error("invalid load mechanism"); } +void WidgetSettings::setQuestionFileButton( + const QString& windowName, const QString& filename, + QuestionBoxMemory::Button button) +{ + const QString sectionName("DialogChoices"); + const QString settingName(windowName + "/" + filename); + + if (button == QuestionBoxMemory::NoButton) { + remove(m_Settings, sectionName, settingName); + } else { + set(m_Settings, sectionName, settingName, button); + } +} -void Settings::setupLoadMechanism() +void WidgetSettings::resetQuestionButtons() { - m_LoadMechanism.activate(getLoadMechanism()); + removeSection(m_Settings, "DialogChoices"); } -bool Settings::useProxy() const +ColorSettings::ColorSettings(QSettings& s) + : m_Settings(s) { - return m_Settings.value("Settings/use_proxy", false).toBool(); } -bool Settings::endorsementIntegration() const +QColor ColorSettings::modlistOverwrittenLoose() const { - return m_Settings.value("Settings/endorsement_integration", true).toBool(); + return get<QColor>( + m_Settings, "Settings", "overwrittenLooseFilesColor", + QColor(0, 255, 0, 64)); } -bool Settings::hideAPICounter() const +void ColorSettings::setModlistOverwrittenLoose(const QColor& c) { - return m_Settings.value("Settings/hide_api_counter", false).toBool(); + set(m_Settings, "Settings", "overwrittenLooseFilesColor", c); } -bool Settings::displayForeign() const +QColor ColorSettings::modlistOverwritingLoose() const { - return m_Settings.value("Settings/display_foreign", true).toBool(); + return get<QColor>( + m_Settings, "Settings", "overwritingLooseFilesColor", + QColor(255, 0, 0, 64)); } -void Settings::setMotDHash(uint hash) +void ColorSettings::setModlistOverwritingLoose(const QColor& c) { - m_Settings.setValue("motd_hash", hash); + set(m_Settings, "Settings", "overwritingLooseFilesColor", c); } -uint Settings::getMotDHash() const +QColor ColorSettings::modlistOverwrittenArchive() const { - return m_Settings.value("motd_hash", 0).toUInt(); + return get<QColor>( + m_Settings, "Settings", "overwrittenArchiveFilesColor", + QColor(0, 255, 255, 64)); } -bool Settings::archiveParsing() const +void ColorSettings::setModlistOverwrittenArchive(const QColor& c) { - return m_Settings.value("Settings/archive_parsing_experimental", false).toBool(); + set(m_Settings, "Settings", "overwrittenArchiveFilesColor", c); } -QVariant Settings::pluginSetting(const QString &pluginName, const QString &key) const +QColor ColorSettings::modlistOverwritingArchive() const +{ + return get<QColor>( + m_Settings, "Settings", "overwritingArchiveFilesColor", + QColor(255, 0, 255, 64)); +} + +void ColorSettings::setModlistOverwritingArchive(const QColor& c) +{ + set(m_Settings, "Settings", "overwritingArchiveFilesColor", c); +} + +QColor ColorSettings::modlistContainsPlugin() const +{ + return get<QColor>( + m_Settings, "Settings", "containsPluginColor", + QColor(0, 0, 255, 64)); +} + +void ColorSettings::setModlistContainsPlugin(const QColor& c) +{ + set(m_Settings, "Settings", "containsPluginColor", c); +} + +QColor ColorSettings::pluginListContained() const +{ + return get<QColor>( + m_Settings, "Settings", "containedColor", + QColor(0, 0, 255, 64)); +} + +void ColorSettings::setPluginListContained(const QColor& c) +{ + set(m_Settings, "Settings", "containedColor", c); +} + +std::optional<QColor> ColorSettings::previousSeparatorColor() const +{ + const auto c = getOptional<QColor>(m_Settings, "General", "previousSeparatorColor"); + if (c && c->isValid()) { + return c; + } + + return {}; +} + +void ColorSettings::setPreviousSeparatorColor(const QColor& c) const +{ + set(m_Settings, "General", "previousSeparatorColor", c); +} + +void ColorSettings::removePreviousSeparatorColor() +{ + remove(m_Settings, "General", "previousSeparatorColor"); +} + +bool ColorSettings::colorSeparatorScrollbar() const +{ + return get<bool>(m_Settings, "Settings", "colorSeparatorScrollbars", true); +} + +void ColorSettings::setColorSeparatorScrollbar(bool b) +{ + set(m_Settings, "Settings", "colorSeparatorScrollbars", b); +} + +QColor ColorSettings::idealTextColor(const QColor& rBackgroundColor) +{ + if (rBackgroundColor.alpha() == 0) + return QColor(Qt::black); + + const int THRESHOLD = 106 * 255.0f / rBackgroundColor.alpha(); + int BackgroundDelta = (rBackgroundColor.red() * 0.299) + (rBackgroundColor.green() * 0.587) + (rBackgroundColor.blue() * 0.114); + return QColor((255 - BackgroundDelta <= THRESHOLD) ? Qt::black : Qt::white); +} + + + +PluginSettings::PluginSettings(QSettings& settings) + : m_Settings(settings) +{ +} + +void PluginSettings::clearPlugins() +{ + m_Plugins.clear(); + m_PluginSettings.clear(); + m_PluginBlacklist.clear(); + + m_PluginBlacklist = readBlacklist(); +} + +void PluginSettings::registerPlugin(IPlugin *plugin) +{ + m_Plugins.push_back(plugin); + m_PluginSettings.insert(plugin->name(), QVariantMap()); + m_PluginDescriptions.insert(plugin->name(), QVariantMap()); + + for (const PluginSetting &setting : plugin->settings()) { + const QString settingName = plugin->name() + "/" + setting.key; + + QVariant temp = get<QVariant>( + m_Settings, "Plugins", settingName, setting.defaultValue); + + if (!temp.convert(setting.defaultValue.type())) { + log::warn( + "failed to interpret \"{}\" as correct type for \"{}\" in plugin \"{}\", using default", + temp.toString(), setting.key, plugin->name()); + + temp = setting.defaultValue; + } + + m_PluginSettings[plugin->name()][setting.key] = temp; + + m_PluginDescriptions[plugin->name()][setting.key] = QString("%1 (default: %2)") + .arg(setting.description) + .arg(setting.defaultValue.toString()); + } +} + +std::vector<MOBase::IPlugin*> PluginSettings::plugins() const +{ + return m_Plugins; +} + +QVariant PluginSettings::setting(const QString &pluginName, const QString &key) const { auto iterPlugin = m_PluginSettings.find(pluginName); if (iterPlugin == m_PluginSettings.end()) { return QVariant(); } + auto iterSetting = iterPlugin->find(key); if (iterSetting == iterPlugin->end()) { return QVariant(); @@ -559,677 +1279,763 @@ QVariant Settings::pluginSetting(const QString &pluginName, const QString &key) return *iterSetting; } -void Settings::setPluginSetting(const QString &pluginName, const QString &key, const QVariant &value) +void PluginSettings::setSetting(const QString &pluginName, const QString &key, const QVariant &value) { auto iterPlugin = m_PluginSettings.find(pluginName); + if (iterPlugin == m_PluginSettings.end()) { - throw MyException(tr("attempt to store setting for unknown plugin \"%1\"").arg(pluginName)); + throw MyException( + QObject::tr("attempt to store setting for unknown plugin \"%1\"") + .arg(pluginName)); } // store the new setting both in memory and in the ini m_PluginSettings[pluginName][key] = value; - m_Settings.setValue("Plugins/" + pluginName + "/" + key, value); + set(m_Settings, "Plugins", pluginName + "/" + key, value); +} + +QVariantMap PluginSettings::settings(const QString &pluginName) const +{ + return m_PluginSettings[pluginName]; +} + +void PluginSettings::setSettings(const QString &pluginName, const QVariantMap& map) +{ + m_PluginSettings[pluginName] = map; +} + +QVariantMap PluginSettings::descriptions(const QString &pluginName) const +{ + return m_PluginDescriptions[pluginName]; +} + +void PluginSettings::setDescriptions(const QString &pluginName, const QVariantMap& map) +{ + m_PluginDescriptions[pluginName] = map; } -QVariant Settings::pluginPersistent(const QString &pluginName, const QString &key, const QVariant &def) const +QVariant PluginSettings::persistent(const QString &pluginName, const QString &key, const QVariant &def) const { if (!m_PluginSettings.contains(pluginName)) { return def; } - return m_Settings.value("PluginPersistance/" + pluginName + "/" + key, def); + + return get<QVariant>(m_Settings, "PluginPersistance", pluginName + "/" + key, def); } -void Settings::setPluginPersistent(const QString &pluginName, const QString &key, const QVariant &value, bool sync) +void PluginSettings::setPersistent( + const QString &pluginName, const QString &key, const QVariant &value, bool sync) { if (!m_PluginSettings.contains(pluginName)) { - throw MyException(tr("attempt to store setting for unknown plugin \"%1\"").arg(pluginName)); + throw MyException( + QObject::tr("attempt to store setting for unknown plugin \"%1\"") + .arg(pluginName)); } - m_Settings.setValue("PluginPersistance/" + pluginName + "/" + key, value); + + set(m_Settings, "PluginPersistance", pluginName + "/" + key, value); + if (sync) { m_Settings.sync(); } } -QString Settings::language() +void PluginSettings::addBlacklist(const QString &fileName) { - QString result = m_Settings.value("Settings/language", "").toString(); - if (result.isEmpty()) { - QStringList languagePreferences = QLocale::system().uiLanguages(); - if (languagePreferences.length() > 0) { - // the users most favoritest language - result = languagePreferences.at(0); - } else { - // fallback system locale - result = QLocale::system().name(); - } - } - return result; + m_PluginBlacklist.insert(fileName); + writeBlacklist(); } -void Settings::updateServers(const QList<ServerInfo> &servers) +bool PluginSettings::blacklisted(const QString &fileName) const { - m_Settings.beginGroup("Servers"); - QStringList oldServerKeys = m_Settings.childKeys(); + return m_PluginBlacklist.contains(fileName); +} - for (const ServerInfo &server : servers) { - if (!oldServerKeys.contains(server.name)) { - // not yet known server - QVariantMap newVal; - newVal["premium"] = server.premium; - newVal["preferred"] = server.preferred ? 1 : 0; - newVal["lastSeen"] = server.lastSeen; - newVal["downloadCount"] = 0; - newVal["downloadSpeed"] = 0.0; +void PluginSettings::setBlacklist(const QStringList& pluginNames) +{ + m_PluginBlacklist.clear(); - m_Settings.setValue(server.name, newVal); - } else { - QVariantMap data = m_Settings.value(server.name).toMap(); - data["lastSeen"] = server.lastSeen; - data["premium"] = server.premium; + for (const auto& name : pluginNames) { + m_PluginBlacklist.insert(name); + } +} + +const QSet<QString>& PluginSettings::blacklist() const +{ + return m_PluginBlacklist; +} - m_Settings.setValue(server.name, data); +void PluginSettings::save() +{ + for (auto iterPlugins=m_PluginSettings.begin(); iterPlugins!=m_PluginSettings.end(); ++iterPlugins) { + for (auto iterSettings=iterPlugins->begin(); iterSettings!=iterPlugins->end(); ++iterSettings) { + const auto key = iterPlugins.key() + "/" + iterSettings.key(); + set(m_Settings, "Plugins", key, iterSettings.value()); } } - // clean up unavailable servers - QDate now = QDate::currentDate(); - for (const QString &key : m_Settings.childKeys()) { - QVariantMap val = m_Settings.value(key).toMap(); - QDate lastSeen = val["lastSeen"].toDate(); - if (lastSeen.daysTo(now) > 30) { - qDebug("removing server %s since it hasn't been available for downloads in over a month", qUtf8Printable(key)); - m_Settings.remove(key); - } + writeBlacklist(); +} + +void PluginSettings::writeBlacklist() +{ + const auto current = readBlacklist(); + + if (current.size() > m_PluginBlacklist.size()) { + // Qt can't remove array elements, the section must be cleared + removeSection(m_Settings, "pluginBlacklist"); } - m_Settings.endGroup(); + ScopedWriteArray swa(m_Settings, "pluginBlacklist", m_PluginBlacklist.size()); - m_Settings.sync(); + for (const QString &plugin : m_PluginBlacklist) { + swa.next(); + swa.set("name", plugin); + } } -void Settings::addBlacklistPlugin(const QString &fileName) +QSet<QString> PluginSettings::readBlacklist() const { - m_PluginBlacklist.insert(fileName); - writePluginBlacklist(); + QSet<QString> set; + + ScopedReadArray sra(m_Settings, "pluginBlacklist"); + sra.for_each([&]{ + set.insert(sra.get<QString>("name")); + }); + + return set; } -void Settings::writePluginBlacklist() -{ - m_Settings.remove("pluginBlacklist"); - m_Settings.beginWriteArray("pluginBlacklist"); - int idx = 0; - for (const QString &plugin : m_PluginBlacklist) { - m_Settings.setArrayIndex(idx++); - m_Settings.setValue("name", plugin); - } - m_Settings.endArray(); +PathSettings::PathSettings(QSettings& settings) + : m_Settings(settings) +{ } -void Settings::addLanguages(QComboBox *languageBox) +std::map<QString, QString> PathSettings::recent() const { - std::vector<std::pair<QString, QString>> languages; + std::map<QString, QString> map; - QDirIterator langIter(QCoreApplication::applicationDirPath() + "/translations", QDir::Files); - QString pattern = ToQString(AppConfig::translationPrefix()) + "_([a-z]{2,3}(_[A-Z]{2,2})?).qm"; - QRegExp exp(pattern); - while (langIter.hasNext()) { - langIter.next(); - QString file = langIter.fileName(); - if (exp.exactMatch(file)) { - QString languageCode = exp.cap(1); - QLocale locale(languageCode); - QString languageString = QString("%1 (%2)").arg(locale.nativeLanguageName()).arg(locale.nativeCountryName()); //QLocale::languageToString(locale.language()); - if (locale.language() == QLocale::Chinese) { - if (languageCode == "zh_TW") { - languageString = "Chinese (traditional)"; - } else { - languageString = "Chinese (simplified)"; - } - } - languages.push_back(std::make_pair(QString("%1").arg(languageString), exp.cap(1))); - //languageBox->addItem(QString("%1").arg(languageString), exp.cap(1)); + ScopedReadArray sra(m_Settings, "recentDirectories"); + + sra.for_each([&] { + const QVariant name = sra.get<QVariant>("name"); + const QVariant dir = sra.get<QVariant>("directory"); + + if (name.isValid() && dir.isValid()) { + map.emplace(name.toString(), dir.toString()); } + }); + + return map; +} + +void PathSettings::setRecent(const std::map<QString, QString>& map) +{ + const auto current = recent(); + + if (current.size() > map.size()) { + // Qt can't remove array elements, the section must be cleared + removeSection(m_Settings, "recentDirectories"); } - if (!languageBox->findText("English")) { - languages.push_back(std::make_pair(QString("English"), QString("en_US"))); - //languageBox->addItem("English", "en_US"); - } - std::sort(languages.begin(), languages.end()); - for (const auto &lang : languages) { - languageBox->addItem(lang.first, lang.second); + + ScopedWriteArray swa(m_Settings, "recentDirectories", map.size()); + + for (auto&& p : map) { + swa.next(); + + swa.set("name", p.first); + swa.set("directory", p.second); } } -void Settings::addStyles(QComboBox *styleBox) +QString PathSettings::getConfigurablePath(const QString &key, + const QString &def, + bool resolve) const { - styleBox->addItem("None", ""); - styleBox->addItem("Fusion", "Fusion"); + QString result = QDir::fromNativeSeparators( + get<QString>(m_Settings, "Settings", key, QString("%BASE_DIR%/") + def)); - QDirIterator langIter(QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::stylesheetsPath()), QStringList("*.qss"), QDir::Files); - while (langIter.hasNext()) { - langIter.next(); - QString style = langIter.fileName(); - styleBox->addItem(style, style); + if (resolve) { + result.replace("%BASE_DIR%", base()); + } + + return result; +} + +void PathSettings::setConfigurablePath(const QString &key, const QString& path) +{ + if (path.isEmpty()) { + remove(m_Settings, "Settings", key); + } else { + set(m_Settings, "Settings", key, path); } } -void Settings::resetDialogs() +QString PathSettings::base() const +{ + return QDir::fromNativeSeparators(get<QString>(m_Settings, + "Settings", "base_directory", qApp->property("dataPath").toString())); +} + +QString PathSettings::downloads(bool resolve) const { - QuestionBoxMemory::resetDialogs(); + return getConfigurablePath( + "download_directory", + ToQString(AppConfig::downloadPath()), + resolve); } -void Settings::query(PluginContainer *pluginContainer, QWidget *parent) +QString PathSettings::cache(bool resolve) const { - SettingsDialog dialog(pluginContainer, this, parent); - connect(&dialog, SIGNAL(resetDialogs()), this, SLOT(resetDialogs())); + return getConfigurablePath( + "cache_directory", + ToQString(AppConfig::cachePath()), + resolve); +} - std::vector<std::unique_ptr<SettingsTab>> tabs; +QString PathSettings::mods(bool resolve) const +{ + return getConfigurablePath( + "mod_directory", + ToQString(AppConfig::modsPath()), + resolve); +} - tabs.push_back(std::unique_ptr<SettingsTab>(new GeneralTab(this, dialog))); - tabs.push_back(std::unique_ptr<SettingsTab>(new PathsTab(this, dialog))); - tabs.push_back(std::unique_ptr<SettingsTab>(new DiagnosticsTab(this, dialog))); - tabs.push_back(std::unique_ptr<SettingsTab>(new NexusTab(this, dialog))); - tabs.push_back(std::unique_ptr<SettingsTab>(new SteamTab(this, dialog))); - tabs.push_back(std::unique_ptr<SettingsTab>(new PluginsTab(this, dialog))); - tabs.push_back(std::unique_ptr<SettingsTab>(new WorkaroundsTab(this, dialog))); +QString PathSettings::profiles(bool resolve) const +{ + return getConfigurablePath( + "profiles_directory", + ToQString(AppConfig::profilesPath()), + resolve); +} +QString PathSettings::overwrite(bool resolve) const +{ + return getConfigurablePath( + "overwrite_directory", + ToQString(AppConfig::overwritePath()), + resolve); +} - QString key = QString("geometry/%1").arg(dialog.objectName()); - if (m_Settings.contains(key)) { - dialog.restoreGeometry(m_Settings.value(key).toByteArray()); +void PathSettings::setBase(const QString& path) +{ + if (path.isEmpty()) { + remove(m_Settings, "Settings", "base_directory"); + } else { + set(m_Settings, "Settings", "base_directory", path); } +} + +void PathSettings::setDownloads(const QString& path) +{ + setConfigurablePath("download_directory", path); +} + +void PathSettings::setMods(const QString& path) +{ + setConfigurablePath("mod_directory", path); +} + +void PathSettings::setCache(const QString& path) +{ + setConfigurablePath("cache_directory", path); +} + +void PathSettings::setProfiles(const QString& path) +{ + setConfigurablePath("profiles_directory", path); +} + +void PathSettings::setOverwrite(const QString& path) +{ + setConfigurablePath("overwrite_directory", path); +} + + +NetworkSettings::NetworkSettings(QSettings& settings) + : m_Settings(settings) +{ +} + +bool NetworkSettings::offlineMode() const +{ + return get<bool>(m_Settings, "Settings", "offline_mode", false); +} + +void NetworkSettings::setOfflineMode(bool b) +{ + set(m_Settings, "Settings", "offline_mode", b); +} + +bool NetworkSettings::useProxy() const +{ + return get<bool>(m_Settings, "Settings", "use_proxy", false); +} - if (dialog.exec() == QDialog::Accepted) { - // remember settings before change - QMap<QString, QString> before; - m_Settings.beginGroup("Settings"); - for (auto k : m_Settings.allKeys()) - before[k] = m_Settings.value(k).toString(); - m_Settings.endGroup(); +void NetworkSettings::setUseProxy(bool b) +{ + set(m_Settings, "Settings", "use_proxy", b); +} - // transfer modified settings to configuration file - for (std::unique_ptr<SettingsTab> const &tab: tabs) { - tab->update(); +void NetworkSettings::setDownloadSpeed(const QString& name, int bytesPerSecond) +{ + auto current = servers(); + + for (auto& server : current) { + if (server.name() == name) { + server.addDownload(bytesPerSecond); + updateServers(current); + return; } + } + + log::error( + "server '{}' not found while trying to add a download with bps {}", + name, bytesPerSecond); +} + +ServerList NetworkSettings::servers() const +{ + ServerList list; + + { + ScopedReadArray sra(m_Settings, "Servers"); + + sra.for_each([&] { + ServerInfo::SpeedList lastDownloads; - // print "changed" settings - m_Settings.beginGroup("Settings"); - bool first_update = true; - for (auto k : m_Settings.allKeys()) - if (m_Settings.value(k).toString() != before[k] && !k.contains("username") && !k.contains("password")) - { - if (first_update) { - qDebug("Changed settings:"); - first_update = false; + const auto lastDownloadsString = sra.get<QString>("lastDownloads", ""); + + for (const auto& s : lastDownloadsString.split(" ")) { + const auto bytesPerSecond = s.toInt(); + if (bytesPerSecond > 0) { + lastDownloads.push_back(bytesPerSecond); } - qDebug(" %s=%s", k.toUtf8().data(), m_Settings.value(k).toString().toUtf8().data()); } - m_Settings.endGroup(); + + ServerInfo server( + sra.get<QString>("name", ""), + sra.get<bool>("premium", false), + QDate::fromString(sra.get<QString>("lastSeen", ""), Qt::ISODate), + sra.get<int>("preferred", 0), + lastDownloads); + + list.add(std::move(server)); + }); } - m_Settings.setValue(key, dialog.saveGeometry()); - // These changes happen regardless of accepted or rejected - bool restartNeeded = false; - if (dialog.getApiKeyChanged()) { - restartNeeded = true; + return list; +} + +void NetworkSettings::updateServers(ServerList newServers) +{ + // clean up unavailable servers + newServers.cleanup(); + + const auto current = servers(); + + if (current.size() > newServers.size()) { + // Qt can't remove array elements, the section must be cleared + removeSection(m_Settings, "Servers"); } - if (dialog.getResetGeometries()) { - restartNeeded = true; - m_Settings.setValue("reset_geometry", true); + + + ScopedWriteArray swa(m_Settings, "Servers", newServers.size()); + + for (const auto& server : newServers) { + swa.next(); + + swa.set("name", server.name()); + swa.set("premium", server.isPremium()); + swa.set("lastSeen", server.lastSeen().toString(Qt::ISODate)); + swa.set("preferred", server.preferred()); + + QString lastDownloads; + for (const auto& speed : server.lastDownloads()) { + if (speed > 0) { + lastDownloads += QString("%1 ").arg(speed); + } + } + + swa.set("lastDownloads", lastDownloads.trimmed()); } - if (restartNeeded) { - if (QMessageBox::question(nullptr, - tr("Restart Mod Organizer?"), - tr("In order to finish configuration changes, MO must be restarted.\n" - "Restart it now?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - qApp->exit(INT_MAX); +} + +void NetworkSettings::updateFromOldMap() +{ + // servers used to be a map of byte arrays until 2.2.1, it's now an array of + // individual values instead + // + // so post 2.2.1, only one key is returned: "size", the size of the arrays; + // in 2.2.1, one key per server is returned + + // sanity check that this is really 2.2.1 + { + const QStringList keys = ScopedGroup(m_Settings, "Servers").keys(); + + for (auto&& k : keys) { + if (k == "size") { + // this looks like an array, so the upgrade was probably already done + return; + } } } + const auto servers = serversFromOldMap(); + removeSection(m_Settings, "Servers"); + updateServers(servers); } -Settings::SettingsTab::SettingsTab(Settings *m_parent, SettingsDialog &m_dialog) - : m_parent(m_parent) - , m_Settings(m_parent->m_Settings) - , m_dialog(m_dialog) +ServerList NetworkSettings::serversFromOldMap() const { -} + // for 2.2.1 and before + + ServerList list; + const ScopedGroup sg(m_Settings, "Servers"); + + sg.for_each([&](auto&& serverKey) { + QVariantMap data = sg.get<QVariantMap>(serverKey); + + ServerInfo server( + serverKey, + data["premium"].toBool(), + data["lastSeen"].toDate(), + data["preferred"].toInt(), + {}); + + // ignoring download count and speed, it's now a list of values instead of + // a total + + list.add(std::move(server)); + }); -Settings::SettingsTab::~SettingsTab() -{} + return list; +} -Settings::GeneralTab::GeneralTab(Settings *m_parent, SettingsDialog &m_dialog) - : Settings::SettingsTab(m_parent, m_dialog) - , m_languageBox(m_dialog.findChild<QComboBox *>("languageBox")) - , m_styleBox(m_dialog.findChild<QComboBox *>("styleBox")) - , m_compactBox(m_dialog.findChild<QCheckBox *>("compactBox")) - , m_showMetaBox(m_dialog.findChild<QCheckBox *>("showMetaBox")) - , m_usePrereleaseBox(m_dialog.findChild<QCheckBox *>("usePrereleaseBox")) - , m_overwritingBtn(m_dialog.findChild<QPushButton *>("overwritingBtn")) - , m_overwrittenBtn(m_dialog.findChild<QPushButton *>("overwrittenBtn")) - , m_overwritingArchiveBtn(m_dialog.findChild<QPushButton *>("overwritingArchiveBtn")) - , m_overwrittenArchiveBtn(m_dialog.findChild<QPushButton *>("overwrittenArchiveBtn")) - , m_containsBtn(m_dialog.findChild<QPushButton *>("containsBtn")) - , m_containedBtn(m_dialog.findChild<QPushButton *>("containedBtn")) - , m_colorSeparatorsBox(m_dialog.findChild<QCheckBox *>("colorSeparatorsBox")) +void NetworkSettings::dump() const { - // FIXME I think 'addLanguages' lives in here not in parent - m_parent->addLanguages(m_languageBox); - { - QString languageCode = m_parent->language(); - int currentID = m_languageBox->findData(languageCode); - // I made a mess. :( Most languages are stored with only the iso country - // code (2 characters like "de") but chinese - // with the exact language variant (zh_TW) so I have to search for both - // variants - if (currentID == -1) { - currentID = m_languageBox->findData(languageCode.mid(0, 2)); - } - if (currentID != -1) { - m_languageBox->setCurrentIndex(currentID); - } - } + log::debug("servers:"); - // FIXME I think addStyles lives in here not in parent - m_parent->addStyles(m_styleBox); - { - int currentID = m_styleBox->findData( - m_Settings.value("Settings/style", "").toString()); - if (currentID != -1) { - m_styleBox->setCurrentIndex(currentID); + for (const auto& server : servers()) { + QString lastDownloads; + for (auto speed : server.lastDownloads()) { + lastDownloads += QString("%1 ").arg(speed); } + + log::debug( + " . {} premium={} lastSeen={} preferred={} lastDownloads={}", + server.name(), + server.isPremium() ? "yes" : "no", + server.lastSeen().toString(Qt::ISODate), + server.preferred(), + lastDownloads.trimmed()); } - /* verision using palette only works with fusion theme for some stupid reason... - m_overwritingBtn->setAutoFillBackground(true); - m_overwrittenBtn->setAutoFillBackground(true); - m_containsBtn->setAutoFillBackground(true); - m_containedBtn->setAutoFillBackground(true); - m_overwritingBtn->setPalette(QPalette(m_parent->modlistOverwritingLooseColor())); - m_overwrittenBtn->setPalette(QPalette(m_parent->modlistOverwrittenLooseColor())); - m_containsBtn->setPalette(QPalette(m_parent->modlistContainsPluginColor())); - m_containedBtn->setPalette(QPalette(m_parent->pluginListContainedColor())); - QPalette palette1 = m_overwritingBtn->palette(); - QPalette palette2 = m_overwrittenBtn->palette(); - QPalette palette3 = m_containsBtn->palette(); - QPalette palette4 = m_containedBtn->palette(); - palette1.setColor(QPalette::Background, m_parent->modlistOverwritingLooseColor()); - palette2.setColor(QPalette::Background, m_parent->modlistOverwrittenLooseColor()); - palette3.setColor(QPalette::Background, m_parent->modlistContainsPluginColor()); - palette4.setColor(QPalette::Background, m_parent->pluginListContainedColor()); - m_overwritingBtn->setPalette(palette1); - m_overwrittenBtn->setPalette(palette2); - m_containsBtn->setPalette(palette3); - m_containedBtn->setPalette(palette4); - */ +} - //version with stylesheet - m_dialog.setButtonColor(m_overwritingBtn, m_parent->modlistOverwritingLooseColor()); - m_dialog.setButtonColor(m_overwrittenBtn, m_parent->modlistOverwrittenLooseColor()); - m_dialog.setButtonColor(m_overwritingArchiveBtn, m_parent->modlistOverwritingArchiveColor()); - m_dialog.setButtonColor(m_overwrittenArchiveBtn, m_parent->modlistOverwrittenArchiveColor()); - m_dialog.setButtonColor(m_containsBtn, m_parent->modlistContainsPluginColor()); - m_dialog.setButtonColor(m_containedBtn, m_parent->pluginListContainedColor()); - m_dialog.setOverwritingColor(m_parent->modlistOverwritingLooseColor()); - m_dialog.setOverwrittenColor(m_parent->modlistOverwrittenLooseColor()); - m_dialog.setOverwritingArchiveColor(m_parent->modlistOverwritingArchiveColor()); - m_dialog.setOverwrittenArchiveColor(m_parent->modlistOverwrittenArchiveColor()); - m_dialog.setContainsColor(m_parent->modlistContainsPluginColor()); - m_dialog.setContainedColor(m_parent->pluginListContainedColor()); +NexusSettings::NexusSettings(Settings& parent, QSettings& settings) + : m_Parent(parent), m_Settings(settings) +{ +} - m_compactBox->setChecked(m_parent->compactDownloads()); - m_showMetaBox->setChecked(m_parent->metaDownloads()); - m_usePrereleaseBox->setChecked(m_parent->usePrereleases()); - m_colorSeparatorsBox->setChecked(m_parent->colorSeparatorScrollbar()); +bool NexusSettings::apiKey(QString& apiKey) const +{ + QString tempKey = getWindowsCredential("APIKEY"); + if (tempKey.isEmpty()) + return false; + + apiKey = tempKey; + return true; } -void Settings::GeneralTab::update() +bool NexusSettings::setApiKey(const QString& apiKey) { - QString oldLanguage = m_parent->language(); - QString newLanguage = m_languageBox->itemData(m_languageBox->currentIndex()).toString(); - if (newLanguage != oldLanguage) { - m_Settings.setValue("Settings/language", newLanguage); - emit m_parent->languageChanged(newLanguage); + if (!setWindowsCredential("APIKEY", apiKey)) { + const auto e = GetLastError(); + log::error("Storing API key failed: {}", formatSystemMessage(e)); + return false; } - QString oldStyle = m_Settings.value("Settings/style", "").toString(); - QString newStyle = m_styleBox->itemData(m_styleBox->currentIndex()).toString(); - if (oldStyle != newStyle) { - m_Settings.setValue("Settings/style", newStyle); - emit m_parent->styleChanged(newStyle); - } + return true; +} + +bool NexusSettings::clearApiKey() +{ + return setApiKey(""); +} + +bool NexusSettings::hasApiKey() const +{ + return !getWindowsCredential("APIKEY").isEmpty(); +} + +bool NexusSettings::endorsementIntegration() const +{ + return get<bool>(m_Settings, "Settings", "endorsement_integration", true); +} - m_Settings.setValue("Settings/overwritingLooseFilesColor", m_dialog.getOverwritingColor()); - m_Settings.setValue("Settings/overwrittenLooseFilesColor", m_dialog.getOverwrittenColor()); - m_Settings.setValue("Settings/overwritingArchiveFilesColor", m_dialog.getOverwritingArchiveColor()); - m_Settings.setValue("Settings/overwrittenArchiveFilesColor", m_dialog.getOverwrittenArchiveColor()); - m_Settings.setValue("Settings/containsPluginColor", m_dialog.getContainsColor()); - m_Settings.setValue("Settings/containedColor", m_dialog.getContainedColor()); - m_Settings.setValue("Settings/compact_downloads", m_compactBox->isChecked()); - m_Settings.setValue("Settings/meta_downloads", m_showMetaBox->isChecked()); - m_Settings.setValue("Settings/use_prereleases", m_usePrereleaseBox->isChecked()); - m_Settings.setValue("Settings/colorSeparatorScrollbars", m_colorSeparatorsBox->isChecked()); +void NexusSettings::setEndorsementIntegration(bool b) const +{ + set(m_Settings, "Settings", "endorsement_integration", b); } -Settings::PathsTab::PathsTab(Settings *parent, SettingsDialog &dialog) - : SettingsTab(parent, dialog) - , m_baseDirEdit(m_dialog.findChild<QLineEdit *>("baseDirEdit")) - , m_downloadDirEdit(m_dialog.findChild<QLineEdit *>("downloadDirEdit")) - , m_modDirEdit(m_dialog.findChild<QLineEdit *>("modDirEdit")) - , m_cacheDirEdit(m_dialog.findChild<QLineEdit *>("cacheDirEdit")) - , m_profilesDirEdit(m_dialog.findChild<QLineEdit *>("profilesDirEdit")) - , m_overwriteDirEdit(m_dialog.findChild<QLineEdit *>("overwriteDirEdit")) - , m_managedGameDirEdit(m_dialog.findChild<QLineEdit *>("managedGameDirEdit")) +EndorsementState NexusSettings::endorsementState() const { - m_baseDirEdit->setText(m_parent->getBaseDirectory()); - m_managedGameDirEdit->setText(m_parent->m_GamePlugin->gameDirectory().absoluteFilePath(m_parent->m_GamePlugin->binaryName())); - QString basePath = parent->getBaseDirectory(); - QDir baseDir(basePath); - for (const auto &dir : { - std::make_pair(m_downloadDirEdit, m_parent->getDownloadDirectory(false)), - std::make_pair(m_modDirEdit, m_parent->getModDirectory(false)), - std::make_pair(m_cacheDirEdit, m_parent->getCacheDirectory(false)), - std::make_pair(m_profilesDirEdit, m_parent->getProfileDirectory(false)), - std::make_pair(m_overwriteDirEdit, m_parent->getOverwriteDirectory(false)) - }) { - QString storePath = baseDir.relativeFilePath(dir.second); - storePath = dir.second; - dir.first->setText(storePath); + return endorsementStateFromString( + get<QString>(m_Settings, "General", "endorse_state", "")); +} + +void NexusSettings::setEndorsementState(EndorsementState s) +{ + const auto v = toString(s); + + if (v.isEmpty()) { + remove(m_Settings, "General", "endorse_state"); + } else { + set(m_Settings, "General", "endorse_state", v); } } -void Settings::PathsTab::update() +void NexusSettings::registerAsNXMHandler(bool force) { - typedef std::tuple<QString, QString, std::wstring> Directory; + const auto nxmPath = QCoreApplication::applicationDirPath() + "/nxmhandler.exe"; + const auto executable = QCoreApplication::applicationFilePath(); + + QString mode = force ? "forcereg" : "reg"; + QString parameters = mode + " " + m_Parent.game().plugin()->gameShortName(); + for (const QString& altGame : m_Parent.game().plugin()->validShortNames()) { + parameters += "," + altGame; + } + parameters += " \"" + executable + "\""; - QString basePath = m_parent->getBaseDirectory(); + const auto r = shell::Execute(nxmPath, parameters); - for (const Directory &dir :{ - Directory{m_downloadDirEdit->text(), "download_directory", AppConfig::downloadPath()}, - Directory{m_cacheDirEdit->text(), "cache_directory", AppConfig::cachePath()}, - Directory{m_modDirEdit->text(), "mod_directory", AppConfig::modsPath()}, - Directory{m_overwriteDirEdit->text(), "overwrite_directory", AppConfig::overwritePath()}, - Directory{m_profilesDirEdit->text(), "profiles_directory", AppConfig::profilesPath()} - }) { - QString path, settingsKey; - std::wstring defaultName; - std::tie(path, settingsKey, defaultName) = dir; + if (!r.success()) { + QMessageBox::critical( + nullptr, QObject::tr("Failed"), + QObject::tr("Failed to start the helper application: %1").arg(r.toString())); + } +} - settingsKey = QString("Settings/%1").arg(settingsKey); - QString realPath = path; - realPath.replace("%BASE_DIR%", m_baseDirEdit->text()); +SteamSettings::SteamSettings(Settings& parent, QSettings& settings) + : m_Parent(parent), m_Settings(settings) +{ +} - if (!QDir(realPath).exists()) { - if (!QDir().mkpath(realPath)) { - QMessageBox::warning(qApp->activeWindow(), tr("Error"), - tr("Failed to create \"%1\", you may not have the " - "necessary permission. path remains unchanged.") - .arg(realPath)); - } - } +QString SteamSettings::appID() const +{ + return get<QString>( + m_Settings, "Settings", "app_id", m_Parent.game().plugin()->steamAPPId()); +} - if (QFileInfo(realPath) - != QFileInfo(basePath + "/" + QString::fromStdWString(defaultName))) { - m_Settings.setValue(settingsKey, path); - } else { - m_Settings.remove(settingsKey); - } +void SteamSettings::setAppID(const QString& id) +{ + if (id.isEmpty()) { + remove(m_Settings, "Settings", "app_id"); + } else { + set(m_Settings, "Settings", "app_id", id); } +} + +bool SteamSettings::login(QString &username, QString &password) const +{ + username = get<QString>(m_Settings, "Settings", "steam_username", ""); + password = getWindowsCredential("steam_password"); - if (QFileInfo(m_baseDirEdit->text()) != - QFileInfo(qApp->property("dataPath").toString())) { - m_Settings.setValue("Settings/base_directory", m_baseDirEdit->text()); + return !username.isEmpty() && !password.isEmpty(); +} + +void SteamSettings::setLogin(QString username, QString password) +{ + if (username == "") { + remove(m_Settings, "Settings", "steam_username"); + password = ""; } else { - m_Settings.remove("Settings/base_directory"); + set(m_Settings, "Settings", "steam_username", username); } - QFileInfo oldGameExe(m_parent->m_GamePlugin->gameDirectory().absoluteFilePath(m_parent->m_GamePlugin->binaryName())); - QFileInfo newGameExe(m_managedGameDirEdit->text()); - if (oldGameExe != newGameExe) { - m_Settings.setValue("gamePath", newGameExe.absolutePath()); + if (!setWindowsCredential("steam_password", password)) { + const auto e = GetLastError(); + log::error("Storing or deleting password failed: {}", formatSystemMessage(e)); } } -Settings::DiagnosticsTab::DiagnosticsTab(Settings *m_parent, SettingsDialog &m_dialog) - : Settings::SettingsTab(m_parent, m_dialog) - , m_logLevelBox(m_dialog.findChild<QComboBox *>("logLevelBox")) - , m_dumpsTypeBox(m_dialog.findChild<QComboBox *>("dumpsTypeBox")) - , m_dumpsMaxEdit(m_dialog.findChild<QSpinBox *>("dumpsMaxEdit")) - , m_diagnosticsExplainedLabel(m_dialog.findChild<QLabel *>("diagnosticsExplainedLabel")) + +InterfaceSettings::InterfaceSettings(QSettings& settings) + : m_Settings(settings) { - m_logLevelBox->setCurrentIndex(m_parent->logLevel()); - m_dumpsTypeBox->setCurrentIndex(m_parent->crashDumpsType()); - m_dumpsMaxEdit->setValue(m_parent->crashDumpsMax()); - QString logsPath = qApp->property("dataPath").toString() - + "/" + QString::fromStdWString(AppConfig::logPath()); - m_diagnosticsExplainedLabel->setText( - m_diagnosticsExplainedLabel->text() - .replace("LOGS_FULL_PATH", logsPath) - .replace("LOGS_DIR", QString::fromStdWString(AppConfig::logPath())) - .replace("DUMPS_FULL_PATH", QString::fromStdWString(OrganizerCore::crashDumpsPath())) - .replace("DUMPS_DIR", QString::fromStdWString(AppConfig::dumpsDir())) - ); } -void Settings::DiagnosticsTab::update() +bool InterfaceSettings::lockGUI() const { - m_Settings.setValue("Settings/log_level", m_logLevelBox->currentIndex()); - m_Settings.setValue("Settings/crash_dumps_type", m_dumpsTypeBox->currentIndex()); - m_Settings.setValue("Settings/crash_dumps_max", m_dumpsMaxEdit->value()); + return get<bool>(m_Settings, "Settings", "lock_gui", true); } -Settings::NexusTab::NexusTab(Settings *parent, SettingsDialog &dialog) - : Settings::SettingsTab(parent, dialog) - , m_offlineBox(dialog.findChild<QCheckBox *>("offlineBox")) - , m_proxyBox(dialog.findChild<QCheckBox *>("proxyBox")) - , m_knownServersList(dialog.findChild<QListWidget *>("knownServersList")) - , m_preferredServersList( - dialog.findChild<QListWidget *>("preferredServersList")) - , m_endorsementBox(dialog.findChild<QCheckBox *>("endorsementBox")) - , m_hideAPICounterBox(dialog.findChild<QCheckBox *>("hideAPICounterBox")) +void InterfaceSettings::setLockGUI(bool b) { - m_offlineBox->setChecked(parent->offlineMode()); - m_proxyBox->setChecked(parent->useProxy()); - m_endorsementBox->setChecked(parent->endorsementIntegration()); - m_hideAPICounterBox->setChecked(parent->hideAPICounter()); + set(m_Settings, "Settings", "lock_gui", b); +} - // display server preferences - m_Settings.beginGroup("Servers"); - for (const QString &key : m_Settings.childKeys()) { - QVariantMap val = m_Settings.value(key).toMap(); - QString descriptor = key; - if (!descriptor.compare("CDN", Qt::CaseInsensitive)) { - descriptor += QStringLiteral(" (automatic)"); - } - if (val.contains("downloadSpeed") && val.contains("downloadCount") && (val["downloadCount"].toInt() > 0)) { - int bps = static_cast<int>(val["downloadSpeed"].toDouble() / val["downloadCount"].toInt()); - descriptor += QString(" (%1 kbps)").arg(bps / 1024); - } +std::optional<QString> InterfaceSettings::styleName() const +{ + return getOptional<QString>(m_Settings, "Settings", "style"); +} - QListWidgetItem *newItem = new QListWidgetItemEx<int>(descriptor, Qt::UserRole + 1); +void InterfaceSettings::setStyleName(const QString& name) +{ + set(m_Settings, "Settings", "style", name); +} - newItem->setData(Qt::UserRole, key); - newItem->setData(Qt::UserRole + 1, val["preferred"].toInt()); - if (val["preferred"].toInt() > 0) { - m_preferredServersList->addItem(newItem); - } else { - m_knownServersList->addItem(newItem); - } - m_preferredServersList->sortItems(Qt::DescendingOrder); - } - m_Settings.endGroup(); +bool InterfaceSettings::compactDownloads() const +{ + return get<bool>(m_Settings, "Settings", "compact_downloads", false); } -void Settings::NexusTab::update() +void InterfaceSettings::setCompactDownloads(bool b) { - /* - if (m_loginCheckBox->isChecked()) { - m_Settings.setValue("Settings/nexus_login", true); - m_Settings.setValue("Settings/nexus_username", m_usernameEdit->text()); - m_Settings.setValue("Settings/nexus_password", obfuscate(m_passwordEdit->text())); - } else { - m_Settings.setValue("Settings/nexus_login", false); - m_Settings.remove("Settings/nexus_username"); - m_Settings.remove("Settings/nexus_password"); - } - */ - m_Settings.setValue("Settings/offline_mode", m_offlineBox->isChecked()); - m_Settings.setValue("Settings/use_proxy", m_proxyBox->isChecked()); - m_Settings.setValue("Settings/endorsement_integration", m_endorsementBox->isChecked()); - m_Settings.setValue("Settings/hide_api_counter", m_hideAPICounterBox->isChecked()); + set(m_Settings, "Settings", "compact_downloads", b); +} - // store server preference - m_Settings.beginGroup("Servers"); - for (int i = 0; i < m_knownServersList->count(); ++i) { - QString key = m_knownServersList->item(i)->data(Qt::UserRole).toString(); - QVariantMap val = m_Settings.value(key).toMap(); - val["preferred"] = 0; - m_Settings.setValue(key, val); - } - int count = m_preferredServersList->count(); - for (int i = 0; i < count; ++i) { - QString key = m_preferredServersList->item(i)->data(Qt::UserRole).toString(); - QVariantMap val = m_Settings.value(key).toMap(); - val["preferred"] = count - i; - m_Settings.setValue(key, val); - } - m_Settings.endGroup(); +bool InterfaceSettings::metaDownloads() const +{ + return get<bool>(m_Settings, "Settings", "meta_downloads", false); } -Settings::SteamTab::SteamTab(Settings *m_parent, SettingsDialog &m_dialog) - : Settings::SettingsTab(m_parent, m_dialog) - , m_steamUserEdit(m_dialog.findChild<QLineEdit *>("steamUserEdit")) - , m_steamPassEdit(m_dialog.findChild<QLineEdit *>("steamPassEdit")) +void InterfaceSettings::setMetaDownloads(bool b) { - if (m_Settings.contains("Settings/steam_username")) { - m_steamUserEdit->setText(m_Settings.value("Settings/steam_username", "").toString()); - QString password = deObfuscate("steam_password"); - if (!password.isEmpty()) { - m_steamPassEdit->setText(password); - } - } + set(m_Settings, "Settings", "meta_downloads", b); } -void Settings::SteamTab::update() +bool InterfaceSettings::hideAPICounter() const { - //FIXME this should be inlined here? - m_parent->setSteamLogin(m_steamUserEdit->text(), m_steamPassEdit->text()); + return get<bool>(m_Settings, "Settings", "hide_api_counter", false); } -Settings::PluginsTab::PluginsTab(Settings *m_parent, SettingsDialog &m_dialog) - : Settings::SettingsTab(m_parent, m_dialog) - , m_pluginsList(m_dialog.findChild<QListWidget *>("pluginsList")) - , m_pluginBlacklistList(m_dialog.findChild<QListWidget *>("pluginBlacklist")) +void InterfaceSettings::setHideAPICounter(bool b) { - // display plugin settings - QSet<QString> handledNames; - for (IPlugin *plugin : m_parent->m_Plugins) { - if (handledNames.contains(plugin->name())) - continue; - QListWidgetItem *listItem = new QListWidgetItem(plugin->name(), m_pluginsList); - listItem->setData(Qt::UserRole, QVariant::fromValue((void*)plugin)); - listItem->setData(Qt::UserRole + 1, m_parent->m_PluginSettings[plugin->name()]); - listItem->setData(Qt::UserRole + 2, m_parent->m_PluginDescriptions[plugin->name()]); - m_pluginsList->addItem(listItem); - handledNames.insert(plugin->name()); - } + set(m_Settings, "Settings", "hide_api_counter", b); +} - // display plugin blacklist - for (const QString &pluginName : m_parent->m_PluginBlacklist) { - m_pluginBlacklistList->addItem(pluginName); - } +bool InterfaceSettings::displayForeign() const +{ + return get<bool>(m_Settings, "Settings", "display_foreign", true); } -void Settings::PluginsTab::update() +void InterfaceSettings::setDisplayForeign(bool b) { - // transfer plugin settings to in-memory structure - for (int i = 0; i < m_pluginsList->count(); ++i) { - QListWidgetItem *item = m_pluginsList->item(i); - m_parent->m_PluginSettings[item->text()] = item->data(Qt::UserRole + 1).toMap(); - } - // store plugin settings on disc - for (auto iterPlugins = m_parent->m_PluginSettings.begin(); iterPlugins != m_parent->m_PluginSettings.end(); ++iterPlugins) { - for (auto iterSettings = iterPlugins->begin(); iterSettings != iterPlugins->end(); ++iterSettings) { - m_Settings.setValue("Plugins/" + iterPlugins.key() + "/" + iterSettings.key(), iterSettings.value()); + set(m_Settings, "Settings", "display_foreign", b); +} + +QString InterfaceSettings::language() +{ + QString result = get<QString>(m_Settings, "Settings", "language", ""); + + if (result.isEmpty()) { + QStringList languagePreferences = QLocale::system().uiLanguages(); + + if (languagePreferences.length() > 0) { + // the users most favoritest language + result = languagePreferences.at(0); + } else { + // fallback system locale + result = QLocale::system().name(); } } - // store plugin blacklist - m_parent->m_PluginBlacklist.clear(); - for (QListWidgetItem *item : m_pluginBlacklistList->findItems("*", Qt::MatchWildcard)) { - m_parent->m_PluginBlacklist.insert(item->text()); - } - m_parent->writePluginBlacklist(); + return result; } -Settings::WorkaroundsTab::WorkaroundsTab(Settings *m_parent, - SettingsDialog &m_dialog) - : Settings::SettingsTab(m_parent, m_dialog) - , m_appIDEdit(m_dialog.findChild<QLineEdit *>("appIDEdit")) - , m_mechanismBox(m_dialog.findChild<QComboBox *>("mechanismBox")) - , m_hideUncheckedBox(m_dialog.findChild<QCheckBox *>("hideUncheckedBox")) - , m_forceEnableBox(m_dialog.findChild<QCheckBox *>("forceEnableBox")) - , m_displayForeignBox(m_dialog.findChild<QCheckBox *>("displayForeignBox")) - , m_lockGUIBox(m_dialog.findChild<QCheckBox *>("lockGUIBox")) - , m_enableArchiveParsingBox(m_dialog.findChild<QCheckBox *>("enableArchiveParsingBox")) - , m_resetGeometriesBtn(m_dialog.findChild<QPushButton *>("resetGeometryBtn")) +void InterfaceSettings::setLanguage(const QString& name) { - m_appIDEdit->setText(m_parent->getSteamAppID()); + set(m_Settings, "Settings", "language", name); +} - LoadMechanism::EMechanism mechanismID = m_parent->getLoadMechanism(); - int index = 0; +bool InterfaceSettings::isTutorialCompleted(const QString& windowName) const +{ + return get<bool>(m_Settings, "CompletedWindowTutorials", windowName, false); +} - if (m_parent->m_LoadMechanism.isDirectLoadingSupported()) { - m_mechanismBox->addItem(QObject::tr("Mod Organizer"), LoadMechanism::LOAD_MODORGANIZER); - if (mechanismID == LoadMechanism::LOAD_MODORGANIZER) { - index = m_mechanismBox->count() - 1; - } - } +void InterfaceSettings::setTutorialCompleted(const QString& windowName, bool b) +{ + set(m_Settings, "CompletedWindowTutorials", windowName, b); +} - if (m_parent->m_LoadMechanism.isScriptExtenderSupported()) { - m_mechanismBox->addItem(QObject::tr("Script Extender"), LoadMechanism::LOAD_SCRIPTEXTENDER); - if (mechanismID == LoadMechanism::LOAD_SCRIPTEXTENDER) { - index = m_mechanismBox->count() - 1; - } - } +bool InterfaceSettings::showChangeGameConfirmation() const +{ + return get<bool>(m_Settings, "Settings", "show_change_game_confirmation", true); +} - if (m_parent->m_LoadMechanism.isProxyDLLSupported()) { - m_mechanismBox->addItem(QObject::tr("Proxy DLL"), LoadMechanism::LOAD_PROXYDLL); - if (mechanismID == LoadMechanism::LOAD_PROXYDLL) { - index = m_mechanismBox->count() - 1; - } - } +void InterfaceSettings::setShowChangeGameConfirmation(bool b) +{ + set(m_Settings, "Settings", "show_change_game_confirmation", b); +} - m_mechanismBox->setCurrentIndex(index); +bool InterfaceSettings::doubleClicksOpenPreviews() const +{ + return get<bool>(m_Settings, "Settings", "double_click_previews", true); +} - m_hideUncheckedBox->setChecked(m_parent->hideUncheckedPlugins()); - m_forceEnableBox->setChecked(m_parent->forceEnableCoreFiles()); - m_displayForeignBox->setChecked(m_parent->displayForeign()); - m_lockGUIBox->setChecked(m_parent->lockGUI()); - m_enableArchiveParsingBox->setChecked(m_parent->archiveParsing()); +void InterfaceSettings::setDoubleClicksOpenPreviews(bool b) +{ + set(m_Settings, "Settings", "double_click_previews", b); +} - m_resetGeometriesBtn->setChecked(m_parent->directInterface().value("reset_geometry", false).toBool()); - m_dialog.setExecutableBlacklist(m_parent->executablesBlacklist()); +DiagnosticsSettings::DiagnosticsSettings(QSettings& settings) + : m_Settings(settings) +{ +} +log::Levels DiagnosticsSettings::logLevel() const +{ + return get<log::Levels>(m_Settings, "Settings", "log_level", log::Levels::Info); } -void Settings::WorkaroundsTab::update() +void DiagnosticsSettings::setLogLevel(log::Levels level) { - if (m_appIDEdit->text() != m_parent->m_GamePlugin->steamAPPId()) { - m_Settings.setValue("Settings/app_id", m_appIDEdit->text()); - } else { - m_Settings.remove("Settings/app_id"); - } - m_Settings.setValue("Settings/load_mechanism", m_mechanismBox->itemData(m_mechanismBox->currentIndex()).toInt()); - m_Settings.setValue("Settings/hide_unchecked_plugins", m_hideUncheckedBox->isChecked()); - m_Settings.setValue("Settings/force_enable_core_files", m_forceEnableBox->isChecked()); - m_Settings.setValue("Settings/display_foreign", m_displayForeignBox->isChecked()); - m_Settings.setValue("Settings/lock_gui", m_lockGUIBox->isChecked()); - m_Settings.setValue("Settings/archive_parsing_experimental", m_enableArchiveParsingBox->isChecked()); + set(m_Settings, "Settings", "log_level", level); +} + +lootcli::LogLevels DiagnosticsSettings::lootLogLevel() const +{ + return get<lootcli::LogLevels>( + m_Settings, "Settings", "loot_log_level", lootcli::LogLevels::Info); +} + +void DiagnosticsSettings::setLootLogLevel(lootcli::LogLevels level) +{ + set(m_Settings, "Settings", "loot_log_level", level); +} + +CrashDumpsType DiagnosticsSettings::crashDumpsType() const +{ + return get<CrashDumpsType>(m_Settings, + "Settings", "crash_dumps_type", CrashDumpsType::Mini); +} + +void DiagnosticsSettings::setCrashDumpsType(CrashDumpsType type) +{ + set(m_Settings, "Settings", "crash_dumps_type", type); +} + +int DiagnosticsSettings::crashDumpsMax() const +{ + return get<int>(m_Settings, "Settings", "crash_dumps_max", 5); +} + +void DiagnosticsSettings::setCrashDumpsMax(int n) +{ + set(m_Settings, "Settings", "crash_dumps_max", n); +} + +std::chrono::seconds DiagnosticsSettings::spawnDelay() const +{ + return std::chrono::seconds( + get<int>(m_Settings, "Settings", "spawn_delay", 0)); +} - m_Settings.setValue("Settings/executable_blacklist", m_dialog.getExecutableBlacklist()); +void DiagnosticsSettings::setSpawnDelay(std::chrono::seconds t) +{ + set(m_Settings, "Settings", "spawn_delay", t.count()); } |
