/* Copyright (C) 2012 Sebastian Herbord. All rights reserved. This file is part of Mod Organizer. Mod Organizer is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Mod Organizer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ #include "settings.h" #include "serverinfo.h" #include "executableslist.h" #include "appconfig.h" #include "expanderwidget.h" #include #include using namespace MOBase; template struct ValueConverter { static const T& convert(const T& t) { return t; } }; template struct ValueConverter>> { static QString convert(const T& t) { return QString("%1").arg(static_cast>(t)); } }; template void logChange( const QString& displayName, std::optional oldValue, const T& newValue) { using VC = ValueConverter; if (oldValue) { log::debug( "setting '{}' changed from '{}' to '{}'", displayName, VC::convert(*oldValue), VC::convert(newValue)); } else { log::debug( "setting '{}' set to '{}'", displayName, VC::convert(newValue)); } } void logRemoval(const QString& name) { log::debug("setting '{}' removed", name); } QString settingName(const QString& section, const QString& key) { if (section.isEmpty()) { return key; } else if (key.isEmpty()) { return section; } else { if (section.compare("General", Qt::CaseInsensitive) == 0) { return key; } else { return section + "/" + key; } } } template void setImpl( QSettings& settings, const QString& displayName, const QString& section, const QString& key, const T& value) { const auto current = getOptional(settings, section, key); if (current && *current == value) { // no change return; } const auto name = settingName(section, key); logChange(displayName, current, value); if constexpr (std::is_enum_v) { settings.setValue( name, static_cast>(value)); } else { settings.setValue(name, value); } } void removeImpl( QSettings& settings, const QString& displayName, const QString& section, const QString& key) { if (key.isEmpty()) { if (!settings.childGroups().contains(section, Qt::CaseInsensitive)) { // not there return; } } else { if (!settings.contains(settingName(section, key))) { // not there return; } } logRemoval(displayName); settings.remove(settingName(section, key)); } template std::optional getOptional( const QSettings& settings, const QString& section, const QString& key, std::optional def={}) { if (settings.contains(settingName(section, key))) { const auto v = settings.value(settingName(section, key)); if constexpr (std::is_enum_v) { return static_cast(v.value>()); } else { return v.value(); } } return def; } template T get( const QSettings& settings, const QString& section, const QString& key, T def={}) { if (auto v=getOptional(settings, section, key)) { return *v; } else { return def; } } template void set( QSettings& settings, const QString& section, const QString& key, const T& value) { setImpl(settings, settingName(section, key), section, key, value); } void remove(QSettings& settings, const QString& section, const QString& key) { removeImpl(settings, settingName(section, key), section, key); } void removeSection(QSettings& settings, const QString& section) { removeImpl(settings, section, section, ""); } class ScopedGroup { public: ScopedGroup(QSettings& s, const QString& name) : m_settings(s), m_name(name) { m_settings.beginGroup(m_name); } ~ScopedGroup() { m_settings.endGroup(); } ScopedGroup(const ScopedGroup&) = delete; ScopedGroup& operator=(const ScopedGroup&) = delete; template void set(const QString& key, const T& value) { setImpl(m_settings, settingName(m_name, key), "", key, value); } void remove(const QString& key) { removeImpl(m_settings, settingName(m_name, key), "", key); } QStringList keys() const { return m_settings.childKeys(); } template void for_each(F&& f) const { for (const QString& key : keys()) { f(key); } } template std::optional getOptional(const QString& key, std::optional def={}) const { return ::getOptional(m_settings, "", key, def); } template T get(const QString& key, T def={}) const { return ::get(m_settings, "", key, def); } private: QSettings& m_settings; QString m_name; }; class ScopedReadArray { public: ScopedReadArray(QSettings& s, const QString& section) : m_settings(s), m_count(0) { m_count = m_settings.beginReadArray(section); } ~ScopedReadArray() { m_settings.endArray(); } ScopedReadArray(const ScopedReadArray&) = delete; ScopedReadArray& operator=(const ScopedReadArray&) = delete; template void for_each(F&& f) const { for (int i=0; i std::optional getOptional(const QString& key, std::optional def={}) const { return ::getOptional(m_settings, "", key, def); } template T get(const QString& key, T def={}) const { return ::get(m_settings, "", key, def); } int count() const { return m_count; } QStringList keys() const { return m_settings.childKeys(); } private: QSettings& m_settings; int m_count; }; class ScopedWriteArray { public: ScopedWriteArray(QSettings& s, const QString& section) : m_settings(s), m_section(section), m_i(0) { m_settings.beginWriteArray(section); } ~ScopedWriteArray() { m_settings.endArray(); } ScopedWriteArray(const ScopedWriteArray&) = delete; ScopedWriteArray& operator=(const ScopedWriteArray&) = delete; void next() { m_settings.setArrayIndex(m_i); ++m_i; } template void set(const QString& key, const T& value) { const auto displayName = QString("%1/%2\\%3") .arg(m_section) .arg(m_i) .arg(key); setImpl(m_settings, displayName, "", key, value); } private: QSettings& m_settings; QString m_section; int m_i; }; EndorsementState endorsementStateFromString(const QString& s) { if (s == "Endorsed") { return EndorsementState::Accepted; } else if (s == "Abstained") { return EndorsementState::Refused; } else { return EndorsementState::NoDecision; } } QString toString(EndorsementState s) { switch (s) { case EndorsementState::Accepted: return "Endorsed"; case EndorsementState::Refused: return "Abstained"; case EndorsementState::NoDecision: // fall-through default: return {}; } } QString widgetNameWithTopLevel(const QWidget* widget) { QStringList components; auto* tl = widget->window(); if (tl == widget) { // this is a top level widget, such as a dialog components.push_back(widget->objectName()); } else { // this is a widget const auto toplevelName = tl->objectName(); if (!toplevelName.isEmpty()) { components.push_back(toplevelName); } const auto widgetName = widget->objectName(); if (!widgetName.isEmpty()) { components.push_back(widgetName); } } if (components.isEmpty()) { // can't do much return "unknown_widget"; } return components.join("_"); } QString widgetName(const QMainWindow* w) { return w->objectName(); } QString widgetName(const QHeaderView* w) { return widgetNameWithTopLevel(w->parentWidget()); } QString widgetName(const ExpanderWidget* w) { return widgetNameWithTopLevel(w->button()); } QString widgetName(const QWidget* w) { return widgetNameWithTopLevel(w); } template QString geoSettingName(const Widget* widget) { return widgetName(widget) + "_geometry"; } template QString stateSettingName(const Widget* widget) { return widgetName(widget) + "_state"; } template QString visibilitySettingName(const Widget* widget) { return widgetName(widget) + "_visibility"; } QString dockSettingName(const QDockWidget* dock) { return "MainWindow_docks_" + dock->objectName() + "_size"; } QString indexSettingName(const QWidget* widget) { return widgetNameWithTopLevel(widget) + "_index"; } QString checkedSettingName(const QAbstractButton* b) { return widgetNameWithTopLevel(b) + "_checked"; } void warnIfNotCheckable(const QAbstractButton* b) { if (!b->isCheckable()) { log::warn( "button '{}' used in the settings as a checkbox or radio button " "but is not checkable", b->objectName()); } } Settings *Settings::s_Instance = nullptr; Settings::Settings(const QString& path) : m_Settings(path, QSettings::IniFormat), m_Geometry(m_Settings), m_Colors(m_Settings), m_Plugins(m_Settings) { if (s_Instance != nullptr) { throw std::runtime_error("second instance of \"Settings\" created"); } else { s_Instance = this; } MOBase::QuestionBoxMemory::setCallbacks( [this](auto&& w, auto&& f){ return getQuestionButton(w, f); }, [this](auto&& w, auto&& b){ setQuestionWindowButton(w, b); }, [this](auto&& w, auto&& f, auto&& b){ setQuestionFileButton(w, f, b); }); } Settings::~Settings() { MOBase::QuestionBoxMemory::setCallbacks({}, {}, {}); s_Instance = nullptr; } Settings &Settings::instance() { if (s_Instance == nullptr) { throw std::runtime_error("no instance of \"Settings\""); } return *s_Instance; } void Settings::processUpdates( const QVersionNumber& currentVersion, const QVersionNumber& lastVersion) { if (getFirstStart()) { return; } if (lastVersion < QVersionNumber(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"); } if (lastVersion < QVersionNumber(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"); } if (lastVersion < QVersionNumber(2, 2, 2)) { // log splitter is gone, it's a dock now remove(m_Settings, "General", "log_split"); } //save version in all case set(m_Settings, "General", "version", currentVersion.toString()); } QString Settings::getFilename() const { return m_Settings.fileName(); } void Settings::registerAsNXMHandler(bool force) { const auto nxmPath = QCoreApplication::applicationDirPath() + "/nxmhandler.exe"; const auto executable = QCoreApplication::applicationFilePath(); QString mode = force ? "forcereg" : "reg"; QString parameters = mode + " " + m_GamePlugin->gameShortName(); for (const QString& altGame : m_GamePlugin->validShortNames()) { parameters += "," + altGame; } parameters += " \"" + executable + "\""; if (!shell::Execute(nxmPath, parameters)) { QMessageBox::critical( nullptr, tr("Failed"), tr("Failed to start the helper application")); } } bool Settings::colorSeparatorScrollbar() const { return get(m_Settings, "Settings", "colorSeparatorScrollbars", true); } void Settings::setColorSeparatorScrollbar(bool b) { set(m_Settings, "Settings", "colorSeparatorScrollbars", b); } void Settings::managedGameChanged(IPluginGame const *gamePlugin) { m_GamePlugin = gamePlugin; } bool Settings::obfuscate(const QString key, const QString data) { 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); 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; result = CredWriteW(&cred, 0); delete[] charData; } delete[] keyData; return result; } QString Settings::deObfuscate(const QString key) { 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) { log::error("Retrieving encrypted data failed: {}", formatSystemMessage(e)); } } delete[] keyData; return result; } QColor Settings::getIdealTextColor(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); } bool Settings::hideUncheckedPlugins() const { return get(m_Settings, "Settings", "hide_unchecked_plugins", false); } void Settings::setHideUncheckedPlugins(bool b) { set(m_Settings, "Settings", "hide_unchecked_plugins", b); } bool Settings::forceEnableCoreFiles() const { return get(m_Settings, "Settings", "force_enable_core_files", true); } void Settings::setForceEnableCoreFiles(bool b) { set(m_Settings, "Settings", "force_enable_core_files", b); } bool Settings::lockGUI() const { return get(m_Settings, "Settings", "lock_gui", true); } void Settings::setLockGUI(bool b) { set(m_Settings, "Settings", "lock_gui", b); } bool Settings::automaticLoginEnabled() const { return get(m_Settings, "Settings", "nexus_login", false); } QString Settings::getSteamAppID() const { return get(m_Settings, "Settings", "app_id", m_GamePlugin->steamAPPId()); } void Settings::setSteamAppID(const QString& id) { if (id.isEmpty()) { remove(m_Settings, "Settings", "app_id"); } else { set(m_Settings, "Settings", "app_id", id); } } bool Settings::usePrereleases() const { return get(m_Settings, "Settings", "use_prereleases", false); } void Settings::setUsePrereleases(bool b) { set(m_Settings, "Settings", "use_prereleases", b); } QString Settings::getConfigurablePath(const QString &key, const QString &def, bool resolve) const { QString result = QDir::fromNativeSeparators( get(m_Settings, "Settings", key, QString("%BASE_DIR%/") + def)); if (resolve) { result.replace("%BASE_DIR%", getBaseDirectory()); } return result; } void Settings::setConfigurablePath(const QString &key, const QString& path) { if (path.isEmpty()) { remove(m_Settings, "Settings", key); } else { set(m_Settings, "Settings", key, path); } } QString Settings::getBaseDirectory() const { return QDir::fromNativeSeparators(get(m_Settings, "Settings", "base_directory", qApp->property("dataPath").toString())); } QString Settings::getDownloadDirectory(bool resolve) const { return getConfigurablePath( "download_directory", ToQString(AppConfig::downloadPath()), resolve); } QString Settings::getCacheDirectory(bool resolve) const { return getConfigurablePath( "cache_directory", ToQString(AppConfig::cachePath()), resolve); } QString Settings::getModDirectory(bool resolve) const { return getConfigurablePath( "mod_directory", ToQString(AppConfig::modsPath()), resolve); } QString Settings::getProfileDirectory(bool resolve) const { return getConfigurablePath( "profiles_directory", ToQString(AppConfig::profilesPath()), resolve); } QString Settings::getOverwriteDirectory(bool resolve) const { return getConfigurablePath( "overwrite_directory", ToQString(AppConfig::overwritePath()), resolve); } void Settings::setBaseDirectory(const QString& path) { if (path.isEmpty()) { remove(m_Settings, "Settings", "base_directory"); } else { set(m_Settings, "Settings", "base_directory", path); } } void Settings::setDownloadDirectory(const QString& path) { setConfigurablePath("download_directory", path); } void Settings::setModDirectory(const QString& path) { setConfigurablePath("mod_directory", path); } void Settings::setCacheDirectory(const QString& path) { setConfigurablePath("cache_directory", path); } void Settings::setProfileDirectory(const QString& path) { setConfigurablePath("profiles_directory", path); } void Settings::setOverwriteDirectory(const QString& path) { setConfigurablePath("overwrite_directory", path); } std::optional Settings::getManagedGameDirectory() const { if (auto v=getOptional(m_Settings, "General", "gamePath")) { return QString::fromUtf8(*v); } return {}; } void Settings::setManagedGameDirectory(const QString& path) { set(m_Settings, "General", "gamePath", QDir::toNativeSeparators(path).toUtf8()); } std::optional Settings::getManagedGameName() const { return getOptional(m_Settings, "General", "gameName"); } void Settings::setManagedGameName(const QString& name) { set(m_Settings, "General", "gameName", name); } std::optional Settings::getManagedGameEdition() const { return getOptional(m_Settings, "General", "game_edition"); } void Settings::setManagedGameEdition(const QString& name) { set(m_Settings, "General", "game_edition", name); } std::optional Settings::getSelectedProfileName() const { if (auto v=getOptional(m_Settings, "General", "selected_profile")) { return QString::fromUtf8(*v); } return {}; } void Settings::setSelectedProfileName(const QString& name) { set(m_Settings, "General", "selected_profile", name.toUtf8()); } std::optional Settings::getStyleName() const { return getOptional(m_Settings, "Settings", "style"); } void Settings::setStyleName(const QString& name) { set(m_Settings, "Settings", "style", name); } bool Settings::getUseProxy() const { return get(m_Settings, "Settings", "use_proxy", false); } void Settings::setUseProxy(bool b) { set(m_Settings, "Settings", "use_proxy", b); } std::optional Settings::getVersion() const { if (auto v=getOptional(m_Settings, "General", "version")) { return QVersionNumber::fromString(*v).normalized(); } return {}; } bool Settings::getFirstStart() const { return get(m_Settings, "General", "first_start", true); } void Settings::setFirstStart(bool b) { set(m_Settings, "General", "first_start", b); } std::optional Settings::getPreviousSeparatorColor() const { const auto c = getOptional(m_Settings, "General", "previousSeparatorColor"); if (c && c->isValid()) { return c; } return {}; } void Settings::setPreviousSeparatorColor(const QColor& c) const { set(m_Settings, "General", "previousSeparatorColor", c); } void Settings::removePreviousSeparatorColor() { remove(m_Settings, "General", "previousSeparatorColor"); } bool Settings::getNexusApiKey(QString &apiKey) const { QString tempKey = deObfuscate("APIKEY"); if (tempKey.isEmpty()) return false; apiKey = tempKey; return true; } bool Settings::setNexusApiKey(const QString& apiKey) { if (!obfuscate("APIKEY", apiKey)) { const auto e = GetLastError(); log::error("Storing API key failed: {}", formatSystemMessage(e)); return false; } return true; } bool Settings::clearNexusApiKey() { return setNexusApiKey(""); } bool Settings::hasNexusApiKey() const { return !deObfuscate("APIKEY").isEmpty(); } bool Settings::getSteamLogin(QString &username, QString &password) const { username = get(m_Settings, "Settings", "steam_username", ""); password = deObfuscate("steam_password"); return !username.isEmpty() && !password.isEmpty(); } bool Settings::compactDownloads() const { return get(m_Settings, "Settings", "compact_downloads", false); } void Settings::setCompactDownloads(bool b) { set(m_Settings, "Settings", "compact_downloads", b); } bool Settings::metaDownloads() const { return get(m_Settings, "Settings", "meta_downloads", false); } void Settings::setMetaDownloads(bool b) { set(m_Settings, "Settings", "meta_downloads", b); } bool Settings::offlineMode() const { return get(m_Settings, "Settings/offline_mode", false); } void Settings::setOfflineMode(bool b) { set(m_Settings, "Settings", "offline_mode", b); } log::Levels Settings::logLevel() const { return get(m_Settings, "Settings", "log_level", log::Levels::Info); } void Settings::setLogLevel(log::Levels level) { set(m_Settings, "Settings", "log_level", level); } CrashDumpsType Settings::crashDumpsType() const { return get(m_Settings, "Settings", "crash_dumps_type", CrashDumpsType::Mini); } void Settings::setCrashDumpsType(CrashDumpsType type) { set(m_Settings, "Settings", "crash_dumps_type", type); } int Settings::crashDumpsMax() const { return get(m_Settings, "Settings", "crash_dumps_max", 5); } void Settings::setCrashDumpsMax(int n) { set(m_Settings, "Settings", "crash_dumps_max", n); } 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(m_Settings, "Settings", "executable_blacklist", def); } void Settings::setExecutablesBlacklist(const QString& s) { set(m_Settings, "Settings", "executable_blacklist", s); } void Settings::setSteamLogin(QString username, QString password) { if (username == "") { remove(m_Settings, "Settings", "steam_username"); password = ""; } else { set(m_Settings, "Settings", "steam_username", username); } if (!obfuscate("steam_password", password)) { const auto e = GetLastError(); log::error("Storing or deleting password failed: {}", formatSystemMessage(e)); } } LoadMechanism::EMechanism Settings::getLoadMechanism() const { const auto def = LoadMechanism::LOAD_MODORGANIZER; const auto i = get(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(i), toString(def)); set(m_Settings, "Settings", "load_mechanism", def); return def; } } return i; } void Settings::setLoadMechanism(LoadMechanism::EMechanism m) { set(m_Settings, "Settings", "load_mechanism", m); } void Settings::setupLoadMechanism() { m_LoadMechanism.activate(getLoadMechanism()); } bool Settings::endorsementIntegration() const { return get(m_Settings, "Settings", "endorsement_integration", true); } void Settings::setEndorsementIntegration(bool b) const { set(m_Settings, "Settings", "endorsement_integration", b); } EndorsementState Settings::endorsementState() const { return endorsementStateFromString( get(m_Settings, "General", "endorse_state", "")); } void Settings::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); } } bool Settings::hideAPICounter() const { return get(m_Settings, "Settings", "hide_api_counter", false); } void Settings::setHideAPICounter(bool b) { set(m_Settings, "Settings", "hide_api_counter", b); } bool Settings::displayForeign() const { return get(m_Settings, "Settings", "display_foreign", true); } void Settings::setDisplayForeign(bool b) { set(m_Settings, "Settings", "display_foreign", b); } void Settings::setMotDHash(uint hash) { set(m_Settings, "General", "motd_hash", hash); } unsigned int Settings::getMotDHash() const { return get(m_Settings, "motd_hash", 0); } bool Settings::archiveParsing() const { return get(m_Settings, "Settings", "archive_parsing_experimental", false); } void Settings::setArchiveParsing(bool b) { set(m_Settings, "Settings", "archive_parsing_experimental", b); } QString Settings::language() { QString result = get(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(); } } return result; } void Settings::setLanguage(const QString& name) { set(m_Settings, "Settings", "language", name); } void Settings::setDownloadSpeed(const QString& name, int bytesPerSecond) { auto servers = getServers(); for (auto& server : servers) { if (server.name() == name) { server.addDownload(bytesPerSecond); updateServers(servers); return; } } log::error( "server '{}' not found while trying to add a download with bps {}", name, bytesPerSecond); } ServerList Settings::getServers() const { // 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 { const QStringList keys = ScopedGroup(m_Settings, "Servers").keys(); if (!keys.empty() && keys[0] != "size") { // old format return getServersFromOldMap(); } } // post 2.2.1 format, array of values ServerList list; { ScopedReadArray sra(m_Settings, "Servers"); sra.for_each([&] { ServerInfo::SpeedList lastDownloads; const auto lastDownloadsString = sra.get("lastDownloads", ""); for (const auto& s : lastDownloadsString.split(" ")) { const auto bytesPerSecond = s.toInt(); if (bytesPerSecond > 0) { lastDownloads.push_back(bytesPerSecond); } } ServerInfo server( sra.get("name", ""), sra.get("premium", false), QDate::fromString(sra.get("lastSeen", ""), Qt::ISODate), sra.get("preferred", 0), lastDownloads); list.add(std::move(server)); }); } return list; } ServerList Settings::getServersFromOldMap() const { // for 2.2.1 and before ServerList list; const ScopedGroup sg(m_Settings, "Servers"); sg.for_each([&](auto&& serverKey) { QVariantMap data = sg.get(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)); }); return list; } void Settings::updateServers(ServerList servers) { // clean up unavailable servers servers.cleanup(); removeSection(m_Settings, "Servers"); { ScopedWriteArray swa(m_Settings, "Servers"); for (const auto& server : servers) { 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()); } } } std::map Settings::getRecentDirectories() const { std::map map; ScopedReadArray sra(m_Settings, "RecentDirectories"); sra.for_each([&] { const QVariant name = sra.get("name"); const QVariant dir = sra.get("directory"); if (name.isValid() && dir.isValid()) { map.emplace(name.toString(), dir.toString()); } }); return map; } void Settings::setRecentDirectories(const std::map& map) { removeSection(m_Settings, "RecentDirectories"); ScopedWriteArray swa(m_Settings, "recentDirectories"); for (auto&& p : map) { swa.next(); swa.set("name", p.first); swa.set("directory", p.second); } } std::vector> Settings::getExecutables() const { ScopedReadArray sra(m_Settings, "customExecutables"); std::vector> v; sra.for_each([&]{ std::map map; for (auto&& key : sra.keys()) { map[key] = m_Settings.value(key); } v.push_back(map); }); return v; } void Settings::setExecutables(const std::vector>& v) { removeSection(m_Settings, "customExecutables"); ScopedWriteArray swa(m_Settings, "customExecutables"); for (const auto& map : v) { swa.next(); for (auto&& p : map) { swa.set(p.first, p.second); } } } bool Settings::isTutorialCompleted(const QString& windowName) const { return get(m_Settings, "CompletedWindowTutorials", windowName, false); } void Settings::setTutorialCompleted(const QString& windowName, bool b) { set(m_Settings, "CompletedWindowTutorials", windowName, b); } bool Settings::keepBackupOnInstall() const { return get(m_Settings, "backup_install", false); } void Settings::setKeepBackupOnInstall(bool b) { set(m_Settings, "General", "backup_install", b); } QuestionBoxMemory::Button Settings::getQuestionButton( const QString& windowName, const QString& filename) const { const QString sectionName("DialogChoices"); if (!filename.isEmpty()) { const auto fileSetting = windowName + "/" + filename; if (auto v=getOptional(m_Settings, sectionName, filename)) { return static_cast(*v); } } if (auto v=getOptional(m_Settings, sectionName, windowName)) { return static_cast(*v); } return QuestionBoxMemory::NoButton; } void Settings::setQuestionWindowButton( const QString& windowName, QuestionBoxMemory::Button button) { const QString sectionName("DialogChoices/"); if (button == QuestionBoxMemory::NoButton) { remove(m_Settings, sectionName, windowName); } else { set(m_Settings, sectionName, windowName, button); } } void Settings::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::resetQuestionButtons() { removeSection(m_Settings, "DialogChoices"); } std::optional Settings::getIndex(const QComboBox* cb) const { return getOptional(m_Settings, "Widgets", indexSettingName(cb)); } void Settings::saveIndex(const QComboBox* cb) { set(m_Settings, "Widgets", indexSettingName(cb), cb->currentIndex()); } void Settings::restoreIndex(QComboBox* cb, std::optional def) const { if (auto v=getOptional(m_Settings, "Widgets", indexSettingName(cb), def)) { cb->setCurrentIndex(*v); } } std::optional Settings::getIndex(const QTabWidget* w) const { return getOptional(m_Settings, "Widgets", indexSettingName(w)); } void Settings::saveIndex(const QTabWidget* w) { set(m_Settings, "Widgets", indexSettingName(w), w->currentIndex()); } void Settings::restoreIndex(QTabWidget* w, std::optional def) const { if (auto v=getOptional(m_Settings, "Widgets", indexSettingName(w), def)) { w->setCurrentIndex(*v); } } std::optional Settings::getChecked(const QAbstractButton* w) const { warnIfNotCheckable(w); return getOptional(m_Settings, "Widgets", checkedSettingName(w)); } void Settings::saveChecked(const QAbstractButton* w) { warnIfNotCheckable(w); set(m_Settings, "Widgets", checkedSettingName(w), w->isChecked()); } void Settings::restoreChecked(QAbstractButton* w, std::optional def) const { warnIfNotCheckable(w); if (auto v=getOptional(m_Settings, "Widgets", checkedSettingName(w), def)) { w->setChecked(*v); } } GeometrySettings& Settings::geometry() { return m_Geometry; } const GeometrySettings& Settings::geometry() const { return m_Geometry; } 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; } 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()); } } log::debug("servers:"); for (const auto& server : getServers()) { 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()); } } GeometrySettings::GeometrySettings(QSettings& s) : m_Settings(s), m_Reset(false) { } void GeometrySettings::requestReset() { m_Reset = true; } void GeometrySettings::resetIfNeeded() { if (!m_Reset) { return; } removeSection(m_Settings, "Geometry"); } void GeometrySettings::saveGeometry(const QWidget* w) { set(m_Settings, "Geometry", geoSettingName(w), w->saveGeometry()); } bool GeometrySettings::restoreGeometry(QWidget* w) const { if (auto v=getOptional(m_Settings, "Geometry", geoSettingName(w))) { w->restoreGeometry(*v); return true; } return false; } void GeometrySettings::saveState(const QMainWindow* w) { set(m_Settings, "Geometry", stateSettingName(w), w->saveState()); } bool GeometrySettings::restoreState(QMainWindow* w) const { if (auto v=getOptional(m_Settings, "Geometry", stateSettingName(w))) { w->restoreState(*v); return true; } return false; } void GeometrySettings::saveState(const QHeaderView* w) { set(m_Settings, "Geometry", stateSettingName(w), w->saveState()); } bool GeometrySettings::restoreState(QHeaderView* w) const { if (auto v=getOptional(m_Settings, "Geometry", stateSettingName(w))) { w->restoreState(*v); return true; } return false; } void GeometrySettings::saveState(const QSplitter* w) { set(m_Settings, "Geometry", stateSettingName(w), w->saveState()); } bool GeometrySettings::restoreState(QSplitter* w) const { if (auto v=getOptional(m_Settings, "Geometry", stateSettingName(w))) { w->restoreState(*v); return true; } return false; } void GeometrySettings::saveState(const ExpanderWidget* expander) { set(m_Settings, "Geometry", stateSettingName(expander), expander->saveState()); } bool GeometrySettings::restoreState(ExpanderWidget* expander) const { if (auto v=getOptional(m_Settings, "Geometry", stateSettingName(expander))) { expander->restoreState(*v); return true; } return false; } void GeometrySettings::saveVisibility(const QWidget* w) { set(m_Settings, "Geometry", visibilitySettingName(w), w->isVisible()); } bool GeometrySettings::restoreVisibility(QWidget* w, std::optional def) const { if (auto v=getOptional(m_Settings, "Geometry", visibilitySettingName(w), def)) { w->setVisible(*v); return true; } return false; } void GeometrySettings::restoreToolbars(QMainWindow* w) const { // all toolbars have the same size and button style settings const auto size = getOptional(m_Settings, "Geometry", "toolbar_size"); const auto style = getOptional(m_Settings, "Geometry", "toolbar_button_style"); for (auto* tb : w->findChildren()) { if (size) { tb->setIconSize(*size); } if (style) { tb->setToolButtonStyle(static_cast(*style)); } restoreVisibility(tb); } } void GeometrySettings::saveToolbars(const QMainWindow* w) { const auto tbs = w->findChildren(); // 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(tb->toolButtonStyle())); } } QStringList GeometrySettings::getModInfoTabOrder() const { QStringList v; if (m_Settings.contains("mod_info_tabs")) { // old byte array from 2.2.0 QDataStream stream(m_Settings.value("mod_info_tabs").toByteArray()); int count = 0; stream >> count; for (int i=0; i> s; v.push_back(s); } } else { // string list since 2.2.1 QString string = m_Settings.value("mod_info_tab_order").toString(); QTextStream stream(&string); while (!stream.atEnd()) { QString s; stream >> s; v.push_back(s); } } return v; } void GeometrySettings::setModInfoTabOrder(const QString& names) { set(m_Settings, "Geometry", "mod_info_tab_order", names); } void GeometrySettings::centerOnMainWindowMonitor(QWidget* w) { const auto monitor = getOptional( m_Settings, "Geometry", "MainWindow_monitor"); QPoint center; if (monitor && QGuiApplication::screens().size() > *monitor) { center = QGuiApplication::screens().at(*monitor)->geometry().center(); } else { center = QGuiApplication::primaryScreen()->geometry().center(); } w->move(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); } } } Qt::Orientation dockOrientation(const QMainWindow* mw, const QDockWidget* d) { // docks in these areas are horizontal const auto horizontalAreas = Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea; if (mw->dockWidgetArea(const_cast(d)) & horizontalAreas) { return Qt::Horizontal; } else { return Qt::Vertical; } } void GeometrySettings::saveDocks(const QMainWindow* mw) { // 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()) { 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); } } void GeometrySettings::restoreDocks(QMainWindow* mw) const { struct DockInfo { QDockWidget* d; int size = 0; Qt::Orientation ori; }; std::vector dockInfos; // for each dock for (auto* dock : mw->findChildren()) { if (auto size=getOptional(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); } }); } ColorSettings::ColorSettings(QSettings& s) : m_Settings(s) { } QColor ColorSettings::modlistOverwrittenLoose() const { return get( m_Settings, "Settings", "overwrittenLooseFilesColor", QColor(0, 255, 0, 64)); } void ColorSettings::setModlistOverwrittenLoose(const QColor& c) { set(m_Settings, "Settings", "overwrittenLooseFilesColor", c); } QColor ColorSettings::modlistOverwritingLoose() const { return get( m_Settings, "Settings", "overwritingLooseFilesColor", QColor(255, 0, 0, 64)); } void ColorSettings::setModlistOverwritingLoose(const QColor& c) { set(m_Settings, "Settings", "overwritingLooseFilesColor", c); } QColor ColorSettings::modlistOverwrittenArchive() const { return get( m_Settings, "Settings", "overwrittenArchiveFilesColor", QColor(0, 255, 255, 64)); } void ColorSettings::setModlistOverwrittenArchive(const QColor& c) { set(m_Settings, "Settings", "overwrittenArchiveFilesColor", c); } QColor ColorSettings::modlistOverwritingArchive() const { return get( 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( 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( m_Settings, "Settings", "containedColor", QColor(0, 0, 255, 64)); } void ColorSettings::setPluginListContained(const QColor& c) { set(m_Settings, "Settings", "containedColor", c); } PluginSettings::PluginSettings(QSettings& settings) : m_Settings(settings) { } void PluginSettings::clearPlugins() { m_Plugins.clear(); m_PluginSettings.clear(); m_PluginBlacklist.clear(); ScopedReadArray sra(m_Settings, "pluginBlacklist"); sra.for_each([&]{ m_PluginBlacklist.insert(sra.get("name")); }); } 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( 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()); } } bool PluginSettings::pluginBlacklisted(const QString &fileName) const { return m_PluginBlacklist.contains(fileName); } QVariant PluginSettings::pluginSetting(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(); } return *iterSetting; } void PluginSettings::setPluginSetting(const QString &pluginName, const QString &key, const QVariant &value) { auto iterPlugin = m_PluginSettings.find(pluginName); if (iterPlugin == m_PluginSettings.end()) { 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; set(m_Settings, "Plugins", pluginName + "/" + key, value); } QVariant PluginSettings::pluginPersistent(const QString &pluginName, const QString &key, const QVariant &def) const { if (!m_PluginSettings.contains(pluginName)) { return def; } return get(m_Settings, "PluginPersistance", pluginName + "/" + key, def); } void PluginSettings::setPluginPersistent( const QString &pluginName, const QString &key, const QVariant &value, bool sync) { if (!m_PluginSettings.contains(pluginName)) { throw MyException( QObject::tr("attempt to store setting for unknown plugin \"%1\"") .arg(pluginName)); } set(m_Settings, "PluginPersistance", pluginName + "/" + key, value); if (sync) { m_Settings.sync(); } } void PluginSettings::addBlacklistPlugin(const QString &fileName) { m_PluginBlacklist.insert(fileName); writePluginBlacklist(); } void PluginSettings::writePluginBlacklist() { removeSection(m_Settings, "PluginBlacklist"); ScopedWriteArray swa(m_Settings, "PluginBlacklist"); for (const QString &plugin : m_PluginBlacklist) { swa.next(); swa.set("name", plugin); } } QVariantMap PluginSettings::pluginSettings(const QString &pluginName) const { return m_PluginSettings[pluginName]; } void PluginSettings::setPluginSettings(const QString &pluginName, const QVariantMap& map) { m_PluginSettings[pluginName] = map; } QVariantMap PluginSettings::pluginDescriptions(const QString &pluginName) const { return m_PluginDescriptions[pluginName]; } void PluginSettings::pluginDescriptions(const QString &pluginName, const QVariantMap& map) { m_PluginDescriptions[pluginName] = map; } const QSet& PluginSettings::pluginBlacklist() const { return m_PluginBlacklist; } void PluginSettings::setPluginBlacklist(const QStringList& pluginNames) { m_PluginBlacklist.clear(); for (const auto& name : pluginNames) { m_PluginBlacklist.insert(name); } } 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()); } } writePluginBlacklist(); } GeometrySaver::GeometrySaver(Settings& s, QDialog* dialog) : m_settings(s), m_dialog(dialog) { m_settings.geometry().restoreGeometry(m_dialog); } GeometrySaver::~GeometrySaver() { m_settings.geometry().saveGeometry(m_dialog); }