From b7cb63ddb1e2b263d5e485c97faea527c7c0af44 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Fri, 6 Nov 2020 11:37:12 -0500 Subject: removed some redundant functions in InstanceManager, made them all non-static documentation --- src/createinstancedialog.cpp | 2 +- src/createinstancedialogpages.cpp | 10 +- src/instancemanager.cpp | 98 +++++++++---------- src/instancemanager.h | 192 +++++++++++++++++++++++++++++++++++--- src/instancemanagerdialog.cpp | 6 +- src/main.cpp | 18 ++-- 6 files changed, 250 insertions(+), 76 deletions(-) (limited to 'src') diff --git a/src/createinstancedialog.cpp b/src/createinstancedialog.cpp index 2ff8adf3..13dd200c 100644 --- a/src/createinstancedialog.cpp +++ b/src/createinstancedialog.cpp @@ -443,7 +443,7 @@ QString CreateInstanceDialog::dataPath() const QString s; if (instanceType() == Portable) { - s = QDir(InstanceManager::portablePath()).absolutePath(); + s = QDir(InstanceManager::singleton().portablePath()).absolutePath(); } else { s = InstanceManager::singleton().instancePath(instanceName()); } diff --git a/src/createinstancedialogpages.cpp b/src/createinstancedialogpages.cpp index 26f2f61d..f2a7ab36 100644 --- a/src/createinstancedialogpages.cpp +++ b/src/createinstancedialogpages.cpp @@ -136,11 +136,11 @@ TypePage::TypePage(CreateInstanceDialog& dlg) { ui->createGlobal->setDescription( ui->createGlobal->description() - .arg(InstanceManager::singleton().instancesPath())); + .arg(InstanceManager::singleton().globalInstancesRootPath())); ui->createPortable->setDescription( ui->createPortable->description() - .arg(InstanceManager::portablePath())); + .arg(InstanceManager::singleton().portablePath())); if (InstanceManager::singleton().portableInstanceExists()) { ui->createPortable->setEnabled(false); @@ -748,7 +748,7 @@ void NamePage::onChanged() void NamePage::updateWarnings() { - const auto root = InstanceManager::singleton().instancesPath(); + const auto root = InstanceManager::singleton().globalInstancesRootPath(); m_okay = checkName(root, ui->instanceName->text()); updateNavigation(); @@ -899,9 +899,9 @@ void PathsPage::setPaths(const QString& name, bool force) QString path; if (m_dlg.instanceType() == CreateInstanceDialog::Portable) { - path = InstanceManager::portablePath(); + path = InstanceManager::singleton().portablePath(); } else { - const auto root = InstanceManager::singleton().instancesPath(); + const auto root = InstanceManager::singleton().globalInstancesRootPath(); path = root + "/" + name; } diff --git a/src/instancemanager.cpp b/src/instancemanager.cpp index 8163149b..e7c2a611 100644 --- a/src/instancemanager.cpp +++ b/src/instancemanager.cpp @@ -79,7 +79,7 @@ QString Instance::profileName() const QString Instance::iniPath() const { - return InstanceManager::iniPath(m_dir); + return InstanceManager::singleton().iniPath(m_dir); } bool Instance::isPortable() const @@ -96,6 +96,7 @@ Instance::SetupResults Instance::setup(PluginContainer& plugins) return SetupResults::BadIni; } + // game name and directory are from ini unless overridden by setGame() if (m_gameName.isEmpty()) { if (auto v=s.game().name()) m_gameName = *v; @@ -106,11 +107,13 @@ Instance::SetupResults Instance::setup(PluginContainer& plugins) m_gameDir = *v; } + // getting game plugin const auto r = getGamePlugin(plugins); if (r != SetupResults::Ok) { return r; } + // getting game variant, error if it's missing and required by the plugin if (m_gameVariant.isEmpty()) { if (auto v=s.game().edition()) { m_gameVariant = *v; @@ -123,15 +126,22 @@ Instance::SetupResults Instance::setup(PluginContainer& plugins) m_plugin->setGameVariant(m_gameVariant); } + // figuring out profile from ini if it's missing getProfile(s); + // updating the settings since some of these values might have been missing s.game().setName(m_gameName); s.game().setDirectory(m_gameDir); s.game().setSelectedProfileName(m_profile); - if (!m_gameVariant.isEmpty()) + if (!m_gameVariant.isEmpty()) { + // don't write a variant to the ini if the plugin doesn't require one s.game().setEdition(m_gameVariant); + } + // the game directory may be different than what the plugin detected, the user + // can change it in the settings and might have multiple versions of the game + // installed m_plugin->setGamePath(m_gameDir); return SetupResults::Ok; @@ -299,31 +309,27 @@ InstanceManager &InstanceManager::singleton() void InstanceManager::overrideInstance(const QString& instanceName) { m_overrideInstanceName = instanceName; - m_overrideInstance = true; } void InstanceManager::overrideProfile(const QString& profileName) { m_overrideProfileName = profileName; - m_overrideProfile = true; } std::optional InstanceManager::currentInstance() const { - const QString profile = m_overrideProfile ? m_overrideProfileName : ""; + const QString profile = m_overrideProfileName ? + *m_overrideProfileName : ""; + + const QString name = m_overrideInstanceName ? + *m_overrideInstanceName : GlobalSettings::currentInstance(); - if (portableInstallIsLocked()) { + + if (!allowedToChangeInstance()) { // force portable instance return Instance(QDir(portablePath()), true, profile); } - QString name; - - if (m_overrideInstance) - name = m_overrideInstanceName; - else - name = GlobalSettings::currentInstance(); - if (name.isEmpty()) { if (portableInstanceExists()) { // use portable @@ -340,7 +346,7 @@ std::optional InstanceManager::currentInstance() const void InstanceManager::clearCurrentInstance() { setCurrentInstance(""); - m_overrideInstance = false; + m_overrideInstanceName = {}; } void InstanceManager::setCurrentInstance(const QString &name) @@ -350,27 +356,27 @@ void InstanceManager::setCurrentInstance(const QString &name) QString InstanceManager::instancePath(const QString& instanceName) const { - return QDir::fromNativeSeparators(instancesPath() + "/" + instanceName); + return QDir::fromNativeSeparators(globalInstancesRootPath() + "/" + instanceName); } -QString InstanceManager::instancesPath() const +QString InstanceManager::globalInstancesRootPath() const { return QDir::fromNativeSeparators( QStandardPaths::writableLocation(QStandardPaths::DataLocation)); } -QString InstanceManager::iniPath(const QDir& instanceDir) +QString InstanceManager::iniPath(const QDir& instanceDir) const { return instanceDir.filePath(QString::fromStdWString(AppConfig::iniFileName())); } -std::vector InstanceManager::instancePaths() const +std::vector InstanceManager::globalInstancePaths() const { const std::set ignore = { "cache", "qtwebengine", }; - const QDir root(instancesPath()); + const QDir root(globalInstancesRootPath()); const auto dirs = root.entryList(QDir::Dirs | QDir::NoDotAndDotDot); std::vector list; @@ -384,24 +390,12 @@ std::vector InstanceManager::instancePaths() const return list; } -QStringList InstanceManager::instanceNames() const -{ - QStringList list; - - for (auto&& d : instancePaths()) { - list.push_back(d.dirName()); - } - - return list; -} - - -bool InstanceManager::isPortablePath(const QString& dataPath) +bool InstanceManager::hasAnyInstances() const { - return (dataPath == portablePath()); + return portableInstanceExists() || !globalInstancePaths().empty(); } -QString InstanceManager::portablePath() +QString InstanceManager::portablePath() const { return qApp->applicationDirPath(); } @@ -412,16 +406,13 @@ bool InstanceManager::portableInstanceExists() const QString::fromStdWString(AppConfig::iniFileName())); } - -bool InstanceManager::portableInstallIsLocked() const -{ - return QFile::exists(qApp->applicationDirPath() + "/" + - QString::fromStdWString(AppConfig::portableLockFileName())); -} - bool InstanceManager::allowedToChangeInstance() const { - return !portableInstallIsLocked(); + const auto lockFile = + qApp->applicationDirPath() + "/" + + QString::fromStdWString(AppConfig::portableLockFileName()); + + return !QFile::exists(lockFile); } const MOBase::IPluginGame* InstanceManager::gamePluginForDirectory( @@ -429,6 +420,7 @@ const MOBase::IPluginGame* InstanceManager::gamePluginForDirectory( { const QString ini = iniPath(instanceDir); + // reading ini Settings s(ini); if (s.iniStatus() != QSettings::NoError) @@ -437,6 +429,7 @@ const MOBase::IPluginGame* InstanceManager::gamePluginForDirectory( return nullptr; } + // using game name from ini, if available const auto instanceGameName = s.game().name(); if (instanceGameName && !instanceGameName->isEmpty()) @@ -448,7 +441,7 @@ const MOBase::IPluginGame* InstanceManager::gamePluginForDirectory( } log::error( - "no plugin reports game name '{}' found in ini {}", + "no plugin has game name '{}' that was found in ini {}", *instanceGameName, ini); } else @@ -457,8 +450,7 @@ const MOBase::IPluginGame* InstanceManager::gamePluginForDirectory( } - log::error("falling back on looksValid check"); - + // using game directory from ini, if available const auto gameDir = s.game().directory(); if (gameDir && !gameDir->isEmpty()) @@ -470,7 +462,7 @@ const MOBase::IPluginGame* InstanceManager::gamePluginForDirectory( } log::error( - "no plugins appear to support game directory '{}' from ini {}", + "no plugin appears to support game directory '{}' from ini {}", *gameDir, ini); } else @@ -478,6 +470,17 @@ const MOBase::IPluginGame* InstanceManager::gamePluginForDirectory( log::error("no game directory found in ini {}", ini); } + + // looking for a plugin that can handle the directory + log::debug("falling back on looksValid check"); + + for (const IPluginGame* game : plugins.plugins()) { + if (game->looksValid(instanceDir)) { + return game; + } + } + + return nullptr; } @@ -485,6 +488,7 @@ QString InstanceManager::makeUniqueName(const QString& instanceName) const { const QString sanitized = sanitizeInstanceName(instanceName); + // trying "name (N)" QString name = sanitized; for (int i=2; i<100; ++i) { if (!instanceExists(name)) { @@ -499,7 +503,7 @@ QString InstanceManager::makeUniqueName(const QString& instanceName) const bool InstanceManager::instanceExists(const QString& instanceName) const { - const QDir root = instancesPath(); + const QDir root = globalInstancesRootPath(); return root.exists(instanceName); } diff --git a/src/instancemanager.h b/src/instancemanager.h index 79f1f30b..e2ceb743 100644 --- a/src/instancemanager.h +++ b/src/instancemanager.h @@ -10,33 +10,122 @@ class Settings; class PluginContainer; +// represents an instance, either global or portable +// +// if setup() is not called, the game plugin is not available and the INI is +// not processed at all, so name(), directory() and isPortable() really are the +// only meaningful functions +// +// setup() must be called when MO wants to use the instance, it will read the +// INI, figure out the game plugin to use and set it up by calling +// setGameVaraint(), setGamePath(), etc. on it +// +// when setup() fails because the game name/directory or variant are missing, +// setGame() and setVariant() can be called before retrying setup(); this +// happens on startup if that information is missing +// class Instance { public: + // returned by setup() + // enum class SetupResults { + // instance is ready to be used Ok, + + // error while reading the INI BadIni, + + // both the game name and directory are missing from the ini; setup() will + // attempt to recover if either are missing, but not when both are IniMissingGame, + + // either: + // 1) there is no plugin with the given name, or + // 2) if the name is missing, no plugin can handle the game directory PluginGone, + + // the selected plugin does not consider the game directory as being valid GameGone, + + // there is no game variant specified in the INI, but the plugin requires + // one MissingVariant }; + + // an instance that lives in the given directory; `portable` must be `true` + // if this is a portable instance + // + // `profileName` can be given to override what's in the INI; this typically + // happens when the profile is overriden on the command line + // Instance(QDir dir, bool portable, QString profileName={}); + // finds the appropriate game plugin and sets it up so MO can use it + // + // setup() tries to recover from some errors, but can fail for a variety of + // reasons, see SetupResults + // SetupResults setup(PluginContainer& plugins); + + // overrides the game name and directory + // void setGame(const QString& name, const QString& dir); + + // overrides the game variant + // void setVariant(const QString& name); + + // returns the instance name; this is the directory name or "Portable" for + // portable instances + // + // can be called without setup() + // QString name() const; + + // returns either: + // 1) the game name from the INI, + // 2) gameName() from the game plugin if it was missing, or + // 3) whatever was given in setGame() + // QString gameName() const; + + // returns either: + // 1) the game directory from the INI, + // 2) gameDirectory() from the game plugin if it was missing, or + // 3) whatever was given in setGame() + // QString gameDirectory() const; + + // returns the instance directory; can be called without setup() + // QDir directory() const; + + // returns the selected game plugin; will return null if setup() hasn't been + // called, or if it failed + // MOBase::IPluginGame* gamePlugin() const; + + // returns either: + // 1) the profile name given in the constructor, + // 2) the profile name from the INI, or + // 3) the default profile name if it's missing (see + // AppConfig::defaultProfileName()) + // QString profileName() const; + + // returns the path to the INI file for this instance; the file may not + // exist + // QString iniPath() const; + + // returns whether this is a portable instance; this is the flag given in the + // constructor + // bool isPortable() const; private: @@ -46,51 +135,132 @@ private: MOBase::IPluginGame* m_plugin; QString m_profile; + // figures out the game plugin for this instance + // SetupResults getGamePlugin(PluginContainer& plugins); + + // figures out the profile name for this instance + // void getProfile(const Settings& s); }; +// manages global and portable instances +// class InstanceManager { public: + // there is only one manager; this isn't called instance() because it's hella + // confusing + // static InstanceManager& singleton(); + // overrides instance name found in registry + // void overrideInstance(const QString& instanceName); + + // overrides profile name from INI for currentInstance() + // void overrideProfile(const QString& profileName); + // returns a game plugin that considers the given directory valid + // + // this will check for an INI file in the directory and use its game name + // and directory if available + // + // if there is no INI, if it's missing these values or if there are no game + // plugins that can handle these values, this returns the first plugin that + // considers the given directory valid + // + // returns null if all of this fails + // const MOBase::IPluginGame* gamePluginForDirectory( const QDir& dir, const PluginContainer& plugins) const; + // clears the instance name from the registry; on restart, this will make MO + // either select the portable instance if it exists, or display the instance + // selection/creation dialog + // void clearCurrentInstance(); + + // returns the current instance from the registry; this may be empty if the + // instance name in the registry is empty or non-existent and there is no + // portable instance set up + // std::optional currentInstance() const; + + // sets the instance name in the registry so the same instance is opened next + // time MO runs + // void setCurrentInstance(const QString &name); + // whether MO should allow the user to change the current instance from the + // user interface + // bool allowedToChangeInstance() const; - static bool isPortablePath(const QString& dataPath); - static QString portablePath(); + + // whether a portable instance exists; this basically checks for an INI in + // the application directory + // bool portableInstanceExists() const; - QString instancesPath() const; - QStringList instanceNames() const; - std::vector instancePaths() const; + // whether any instance exists, whether global or portable + // + bool hasAnyInstances() const; + + // returns the absolute path to the portable instance, regardless of whether + // one exists + // + QString portablePath() const; + // returns the absolute path to the directory that contains global instances + // (typically AppData/Local/ModOrganizer) + // + QString globalInstancesRootPath() const; + + // returns the list of absolute path to all existing global instances; this + // does not include the portable instance + // + std::vector globalInstancePaths() const; + + // returns `name` modified so that it is a valid instance name + // QString sanitizeInstanceName(const QString &name) const; + + // sanitizes the given instance name and either + // 1) returns it if there is no instance with this name + // 2) tries to add " (N)" at the end until it works + // + // may return an empty string if no unique name can be found + // QString makeUniqueName(const QString& instanceName) const; + + // returns whether a global instance with this name already exists + // bool instanceExists(const QString& instanceName) const; + + // returns whether the given instance name would be a valid name; this does + // not check whether the instance already exists, it's basiscally just a check + // against what sanitizeInstanceName() returns + // bool validInstanceName(const QString& instanceName) const; + + // returns the absolute path of a global instance with the given name; this + // does not check if the name is valid or if exists + // QString instancePath(const QString& instanceName) const; - static QString iniPath(const QDir& instanceDir); + + // returns the absolute path to the INI file for the given instance directory; + // the file may not exist + // + QString iniPath(const QDir& instanceDir) const; private: InstanceManager(); - bool portableInstallIsLocked() const; private: - bool m_overrideInstance{false}; - QString m_overrideInstanceName; - bool m_overrideProfile{false}; - QString m_overrideProfileName; + std::optional m_overrideInstanceName; + std::optional m_overrideProfileName; }; #endif // MODORGANIZER_INSTANCEMANAGER_INCLUDED diff --git a/src/instancemanagerdialog.cpp b/src/instancemanagerdialog.cpp index 3b6daeb8..c2108f9d 100644 --- a/src/instancemanagerdialog.cpp +++ b/src/instancemanagerdialog.cpp @@ -48,7 +48,7 @@ public: void setDir(const QDir& dir) { m_dir = dir; - m_settings.reset(new Settings(InstanceManager::iniPath(dir))); + m_settings.reset(new Settings(InstanceManager::singleton().iniPath(dir))); } QString name() const @@ -96,7 +96,7 @@ public: QString iniFile() const { - return InstanceManager::iniPath(m_dir); + return InstanceManager::singleton().iniPath(m_dir); } QIcon icon(const PluginContainer& plugins) const @@ -351,7 +351,7 @@ void InstanceManagerDialog::updateInstances() m_instances.clear(); - for (auto&& d : m.instancePaths()) { + for (auto&& d : m.globalInstancePaths()) { m_instances.push_back(std::make_unique(d, false)); } diff --git a/src/main.cpp b/src/main.cpp index e56197c9..7f0886f6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -280,7 +280,7 @@ std::optional selectInstance() PluginContainer pc(nullptr); pc.loadPlugins(); - if (m.instancePaths().empty() && !m.portableInstanceExists()) { + if (!m.hasAnyInstances()) { // no instances configured CreateInstanceDialog dlg(pc, nullptr); if (dlg.exec() != QDialog::Accepted) { @@ -454,10 +454,6 @@ int runApplication( log::info("data path: {}", dataPath); - if (InstanceManager::isPortablePath(dataPath)) { - log::debug("this is a portable instance"); - } - log::info("working directory: {}", QDir::currentPath()); if (!instance.secondary()) { @@ -529,6 +525,10 @@ int runApplication( } } + if (currentInstance.isPortable()) { + log::debug("this is a portable instance"); + } + checkPathsForSanity(*currentInstance.gamePlugin(), settings); organizer.setManagedGame(currentInstance.gamePlugin()); @@ -665,13 +665,13 @@ int doOneRun( if (!currentInstance->directory().exists()) { // the previously used instance doesn't exist anymore - if (m.instanceNames().empty() && !m.portableInstanceExists()) { + if (m.hasAnyInstances()) { criticalOnTop(QObject::tr( - "Instance at '%1' not found. You must create a new instance") - .arg(currentInstance->directory().absolutePath())); + "Instance at '%1' not found. Select another instance.") + .arg(currentInstance->directory().absolutePath())); } else { criticalOnTop(QObject::tr( - "Instance at '%1' not found. Select another instance.") + "Instance at '%1' not found. You must create a new instance") .arg(currentInstance->directory().absolutePath())); } -- cgit v1.3.1 From dc2ada2a4249917f538938298deb193ed1dd6bb9 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Fri, 6 Nov 2020 13:56:04 -0500 Subject: added ... to the "Delete Instance" button to make it less scary merged InstanceInfo into Instance, most of it was redundant added logging before deleting instance added Instance::readFromIni(), contains stuff that used to be in setup(), used by instance dialog replaced QDir with QString in a few places, I hate QDir --- src/instancemanager.cpp | 278 +++++++++++++++++++++++++++++---- src/instancemanager.h | 109 ++++++++++--- src/instancemanagerdialog.cpp | 347 +++++------------------------------------- src/instancemanagerdialog.h | 80 +++++++++- src/instancemanagerdialog.ui | 2 +- src/main.cpp | 8 +- 6 files changed, 456 insertions(+), 368 deletions(-) (limited to 'src') diff --git a/src/instancemanager.cpp b/src/instancemanager.cpp index e7c2a611..fbafc6e8 100644 --- a/src/instancemanager.cpp +++ b/src/instancemanager.cpp @@ -38,7 +38,7 @@ along with Mod Organizer. If not, see . using namespace MOBase; -Instance::Instance(QDir dir, bool portable, QString profileName) : +Instance::Instance(QString dir, bool portable, QString profileName) : m_dir(std::move(dir)), m_portable(portable), m_plugin(nullptr), m_profile(std::move(profileName)) { @@ -49,7 +49,7 @@ QString Instance::name() const if (isPortable()) return QObject::tr("Portable"); else - return m_dir.dirName(); + return QDir(m_dir).dirName(); } QString Instance::gameName() const @@ -62,11 +62,16 @@ QString Instance::gameDirectory() const return m_gameDir; } -QDir Instance::directory() const +QString Instance::directory() const { return m_dir; } +QString Instance::baseDirectory() const +{ + return m_baseDir; +} + MOBase::IPluginGame* Instance::gamePlugin() const { return m_plugin; @@ -87,13 +92,29 @@ bool Instance::isPortable() const return m_portable; } -Instance::SetupResults Instance::setup(PluginContainer& plugins) +bool Instance::isActive() const +{ + auto& m = InstanceManager::singleton(); + + if (auto i=m.currentInstance()) + { + if (m_portable) { + return i->isPortable(); + } else { + return (i->name() == name()); + } + } + + return false; +} + +bool Instance::readFromIni() { Settings s(iniPath()); if (s.iniStatus() != QSettings::NoError) { log::error("can't read ini {}", iniPath()); - return SetupResults::BadIni; + return false; } // game name and directory are from ini unless overridden by setGame() @@ -107,27 +128,61 @@ Instance::SetupResults Instance::setup(PluginContainer& plugins) m_gameDir = *v; } - // getting game plugin - const auto r = getGamePlugin(plugins); - if (r != SetupResults::Ok) { - return r; - } - - // getting game variant, error if it's missing and required by the plugin if (m_gameVariant.isEmpty()) { if (auto v=s.game().edition()) { m_gameVariant = *v; } } + if (m_baseDir.isEmpty()) { + m_baseDir = s.paths().base(); + } + + // figuring out profile from ini if it's missing + getProfile(s); + + return true; +} + +Instance::SetupResults Instance::setup(PluginContainer& plugins) +{ + // read initial values from the ini + if (!readFromIni()) { + return SetupResults::BadIni; + } + + // getting game plugin + const auto r = getGamePlugin(plugins); + if (r != SetupResults::Ok) { + return r; + } + + // error if the variant missing and required by the plugin if (m_gameVariant.isEmpty() && m_plugin->gameVariants().size() > 1) { return SetupResults::MissingVariant; } else { m_plugin->setGameVariant(m_gameVariant); } - // figuring out profile from ini if it's missing - getProfile(s); + // update the ini in case anything was missing + updateIni(); + + // the game directory may be different than what the plugin detected, the user + // can change it in the settings and might have multiple versions of the game + // installed + m_plugin->setGamePath(m_gameDir); + + return SetupResults::Ok; +} + +void Instance::updateIni() +{ + Settings s(iniPath()); + + if (s.iniStatus() != QSettings::NoError) { + log::error("can't open ini {}", iniPath()); + return; + } // updating the settings since some of these values might have been missing s.game().setName(m_gameName); @@ -138,13 +193,6 @@ Instance::SetupResults Instance::setup(PluginContainer& plugins) // don't write a variant to the ini if the plugin doesn't require one s.game().setEdition(m_gameVariant); } - - // the game directory may be different than what the plugin detected, the user - // can change it in the settings and might have multiple versions of the game - // installed - m_plugin->setGamePath(m_gameDir); - - return SetupResults::Ok; } void Instance::setGame(const QString& name, const QString& dir) @@ -272,7 +320,6 @@ Instance::SetupResults Instance::getGamePlugin(PluginContainer& plugins) } } - void Instance::getProfile(const Settings& s) { if (!m_profile.isEmpty()) { @@ -294,6 +341,176 @@ void Instance::getProfile(const Settings& s) iniPath(), m_profile); } +// returns a list of files and folders that must be deleted when deleting +// this instance +// +std::vector Instance::objectsForDeletion() const +{ + // native separators and ending slash + auto prettyDir = [](auto s) { + if (!s.endsWith("/") || !s.endsWith("\\")) { + s += "/"; + } + + return QDir::toNativeSeparators(s); + }; + + // native separators + auto prettyFile = [](auto s) { + return QDir::toNativeSeparators(s); + }; + + + // lowercase, native separators and ending slash + auto canonicalDir = [](QString s) { + s = s.toLower(); + + if (!s.endsWith("/") || !s.endsWith("\\")) { + s += "/"; + } + + return QDir::toNativeSeparators(s); + }; + + // lower and native separators + auto canonicalFile = [](auto s) { + return QDir::toNativeSeparators(s.toLower()); + }; + + + + // whether the given directory is contained in the root + auto dirInRoot = [&](const QString& root, const QString& dir) { + return canonicalDir(dir).startsWith(canonicalDir(root)); + }; + + // whether the given file is contained in the root + auto fileInRoot = [&](const QString& root, const QString& file) { + return canonicalFile(file).startsWith(canonicalDir(root)); + }; + + + Settings settings(iniPath()); + + if (settings.iniStatus() != QSettings::NoError) { + log::error("can't read ini {}", iniPath()); + return {}; + } + + + + const QString loc = directory(); + const QString base = settings.paths().base(); + + + // directories that might contain the individual files and directories + // set in the path settings + std::vector roots; + + // a portable instance has its location in the installation directory, + // don't delete that + if (!isPortable()) { + if (QDir(loc).exists()) { + roots.push_back({loc, true}); + } + } + + // the base directory is the location directory by default, don't add it + // if it's the same + if (canonicalDir(base) != canonicalDir(loc)) { + if (QDir(base).exists()) { + roots.push_back({base, false}); + } + } + + + // all the directories that are part of an instance; none of them are + // mandatory for deletion + const std::vector dirs = { + settings.paths().downloads(), + settings.paths().mods(), + settings.paths().cache(), + settings.paths().profiles(), + settings.paths().overwrite(), + QDir(m_dir).filePath(QString::fromStdWString(AppConfig::dumpsDir())), + QDir(m_dir).filePath(QString::fromStdWString(AppConfig::logPath())), + }; + + // all the files that are part of an instance + const std::vector files = { + {iniPath(), true}, // the ini file must be deleted + }; + + + // this will contain the root directories, plus all the individual + // directories that are not inside these roots + std::vector cleanDirs; + + for (const auto& f : dirs) { + bool inRoots = false; + + for (const auto& root : roots) { + if (dirInRoot(root.path, f.path)) { + inRoots = true; + break; + } + } + + if (!inRoots) { + // not in roots, this is a path that was changed by the user; make + // sure it exists + if (QDir(f.path).exists()) { + cleanDirs.push_back({prettyDir(f.path), f.mandatoryDelete}); + } + } + } + + // prepending the roots + for (auto itor=roots.rbegin(); itor!=roots.rend(); ++itor) { + cleanDirs.insert( + cleanDirs.begin(), + {prettyDir(itor->path), itor->mandatoryDelete}); + } + + + + // this will contain the individual files that are not inside the roots; + // not that this only contains the INI file for now, so most of this is + // useless + std::vector cleanFiles; + + for (const auto& f : files) { + bool inRoots = false; + + for (const auto& root : roots) { + if (fileInRoot(root.path, f.path)) { + inRoots = true; + break; + } + } + + if (!inRoots) { + // not in roots, this is a path that was changed by the user; make + // sure it exists + if (QFileInfo(f.path).exists()) { + cleanFiles.push_back({prettyFile(f.path), f.mandatoryDelete}); + } + } + } + + + // contains all the directories and files to be deleted + std::vector all; + all.insert(all.end(), cleanDirs.begin(), cleanDirs.end()); + all.insert(all.end(), cleanFiles.begin(), cleanFiles.end()); + + // mandatory on top + std::stable_sort(all.begin(), all.end()); + + return all; +} + + InstanceManager::InstanceManager() { @@ -327,20 +544,20 @@ std::optional InstanceManager::currentInstance() const if (!allowedToChangeInstance()) { // force portable instance - return Instance(QDir(portablePath()), true, profile); + return Instance(portablePath(), true, profile); } if (name.isEmpty()) { if (portableInstanceExists()) { // use portable - return Instance(QDir(portablePath()), true, profile); + return Instance(portablePath(), true, profile); } else { // no instance set return {}; } } - return Instance(QDir(instancePath(name)), false, profile); + return Instance(instancePath(name), false, profile); } void InstanceManager::clearCurrentInstance() @@ -365,12 +582,13 @@ QString InstanceManager::globalInstancesRootPath() const QStandardPaths::writableLocation(QStandardPaths::DataLocation)); } -QString InstanceManager::iniPath(const QDir& instanceDir) const +QString InstanceManager::iniPath(const QString& instanceDir) const { - return instanceDir.filePath(QString::fromStdWString(AppConfig::iniFileName())); + return QDir(instanceDir).filePath( + QString::fromStdWString(AppConfig::iniFileName())); } -std::vector InstanceManager::globalInstancePaths() const +std::vector InstanceManager::globalInstancePaths() const { const std::set ignore = { "cache", "qtwebengine", @@ -379,7 +597,7 @@ std::vector InstanceManager::globalInstancePaths() const const QDir root(globalInstancesRootPath()); const auto dirs = root.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - std::vector list; + std::vector list; for (auto&& d : dirs) { if (!ignore.contains(QFileInfo(d).fileName().toLower())) { @@ -416,7 +634,7 @@ bool InstanceManager::allowedToChangeInstance() const } const MOBase::IPluginGame* InstanceManager::gamePluginForDirectory( - const QDir& instanceDir, const PluginContainer& plugins) const + const QString& instanceDir, const PluginContainer& plugins) const { const QString ini = iniPath(instanceDir); diff --git a/src/instancemanager.h b/src/instancemanager.h index e2ceb743..161df739 100644 --- a/src/instancemanager.h +++ b/src/instancemanager.h @@ -55,15 +55,64 @@ public: }; + // a file or directory owned by this instance, used by objectsForDeletion() + // + struct Object + { + // path to the file or directory + QString path; + + // whether this object must be deleted to properly delete the instance; + // typically true only for the instance directory itself, but not for the + // base directory, etc. + bool mandatoryDelete; + + + Object(QString p, bool d=false) + : path(std::move(p)), mandatoryDelete(d) + { + } + + // puts mandatory delete on top + // + bool operator<(const Object& o) const + { + if (mandatoryDelete && !o.mandatoryDelete) { + return true; + } else if (!mandatoryDelete && o.mandatoryDelete) { + return false; + } + + return false; + } + }; + + + // an instance that lives in the given directory; `portable` must be `true` // if this is a portable instance // // `profileName` can be given to override what's in the INI; this typically // happens when the profile is overriden on the command line // - Instance(QDir dir, bool portable, QString profileName={}); + Instance(QString dir, bool portable, QString profileName={}); + + + // reads in values from the INI if they were not given yet: + // - game name + // - game directory + // - game variant + // - profile name + // + // note that setup() already calls this + // + // returns false if the ini couldn't be read from + // + bool readFromIni(); + - // finds the appropriate game plugin and sets it up so MO can use it + // finds the appropriate game plugin and sets it up so MO can use it; this + // calls readFromIni() first // // setup() tries to recover from some errors, but can fail for a variety of // reasons, see SetupResults @@ -88,22 +137,32 @@ public: QString name() const; // returns either: - // 1) the game name from the INI, - // 2) gameName() from the game plugin if it was missing, or - // 3) whatever was given in setGame() + // 1) the game name from the INI, if readFromIni() was called; + // 2) gameName() from the game plugin if it was missing and setup() was + // called; or + // 3) whatever was given in setGame() // QString gameName() const; // returns either: - // 1) the game directory from the INI, - // 2) gameDirectory() from the game plugin if it was missing, or - // 3) whatever was given in setGame() + // 1) the game directory from the INI, if readFromIni() was called; + // 2) gameDirectory() from the game plugin if it was missing and setup() + // was called; or + // 3) whatever was given in setGame() // QString gameDirectory() const; - // returns the instance directory; can be called without setup() + // returns the instance directory as given in the constructor // - QDir directory() const; + QString directory() const; + + // returns the base directory; empty if readFromIni() hasn't been called + // + QString baseDirectory() const; + + // returns whether this is a portable instance, as given in the constructor + // + bool isPortable() const; // returns the selected game plugin; will return null if setup() hasn't been // called, or if it failed @@ -111,8 +170,8 @@ public: MOBase::IPluginGame* gamePlugin() const; // returns either: - // 1) the profile name given in the constructor, - // 2) the profile name from the INI, or + // 1) the profile name given in the constructor if not empty; + // 2) the profile name from the INI if readFromIni() was called, or // 3) the default profile name if it's missing (see // AppConfig::defaultProfileName()) // @@ -123,15 +182,21 @@ public: // QString iniPath() const; - // returns whether this is a portable instance; this is the flag given in the - // constructor + // whether this is the currently active instance in MO // - bool isPortable() const; + bool isActive() const; + + // returns a list of files and directories that must be deleted when deleting + // this instance; this will read the INI and fail if it's not accessible + // + // returns an empty list on failure + // + std::vector objectsForDeletion() const; private: - QDir m_dir; + QString m_dir; bool m_portable; - QString m_gameName, m_gameDir, m_gameVariant; + QString m_gameName, m_gameDir, m_gameVariant, m_baseDir; MOBase::IPluginGame* m_plugin; QString m_profile; @@ -142,6 +207,10 @@ private: // figures out the profile name for this instance // void getProfile(const Settings& s); + + // updates the ini with the given values and the ones found by setup() + // + void updateIni(); }; @@ -175,7 +244,7 @@ public: // returns null if all of this fails // const MOBase::IPluginGame* gamePluginForDirectory( - const QDir& dir, const PluginContainer& plugins) const; + const QString& dir, const PluginContainer& plugins) const; // clears the instance name from the registry; on restart, this will make MO // either select the portable instance if it exists, or display the instance @@ -221,7 +290,7 @@ public: // returns the list of absolute path to all existing global instances; this // does not include the portable instance // - std::vector globalInstancePaths() const; + std::vector globalInstancePaths() const; // returns `name` modified so that it is a valid instance name // @@ -253,7 +322,7 @@ public: // returns the absolute path to the INI file for the given instance directory; // the file may not exist // - QString iniPath(const QDir& instanceDir) const; + QString iniPath(const QString& instanceDir) const; private: InstanceManager(); diff --git a/src/instancemanagerdialog.cpp b/src/instancemanagerdialog.cpp index c2108f9d..c199fa7b 100644 --- a/src/instancemanagerdialog.cpp +++ b/src/instancemanagerdialog.cpp @@ -13,292 +13,19 @@ using namespace MOBase; -class InstanceInfo -{ -public: - struct Object - { - QString path; - bool mandatoryDelete; - - Object(QString p, bool d=false) - : path(std::move(p)), mandatoryDelete(d) - { - } - - bool operator<(const Object& o) const - { - if (mandatoryDelete && !o.mandatoryDelete) { - return true; - } else if (!mandatoryDelete && o.mandatoryDelete) { - return false; - } - - return false; - } - }; - - - InstanceInfo(QDir dir, bool isPortable) - : m_portable(isPortable) - { - setDir(dir); - } - - void setDir(const QDir& dir) - { - m_dir = dir; - m_settings.reset(new Settings(InstanceManager::singleton().iniPath(dir))); - } - - QString name() const - { - if (m_portable) { - return QObject::tr("Portable"); - } else { - return m_dir.dirName(); - } - } - - QString gameName() const - { - if (auto n=m_settings->game().name()) { - if (auto e=m_settings->game().edition()) { - if (!e->isEmpty()) { - return *n + " (" + *e + ")"; - } - } - - return *n; - } else { - return {}; - } - } - - QString gamePath() const - { - if (auto n=m_settings->game().directory()) { - return QDir::toNativeSeparators(*n); - } else { - return {}; - } - } - - QString location() const - { - return QDir::toNativeSeparators(m_dir.path()); - } - - QString baseDirectory() const - { - return QDir::toNativeSeparators(m_settings->paths().base()); - } - - QString iniFile() const - { - return InstanceManager::singleton().iniPath(m_dir); - } - - QIcon icon(const PluginContainer& plugins) const - { - const auto* game = InstanceManager::singleton().gamePluginForDirectory( - m_dir, plugins); - - if (game) - return game->gameIcon(); - - QPixmap empty(32, 32); - empty.fill(QColor(0, 0, 0, 0)); - return QIcon(empty); - } - - bool isPortable() const - { - return m_portable; - } - - bool isActive() const - { - auto& m = InstanceManager::singleton(); - - if (auto i=m.currentInstance()) - { - if (m_portable) { - return i->isPortable(); - } else { - return (i->name() == name()); - } - } - - return false; - } - - // returns a list of files and folders that must be deleted when deleting - // this instance - // - std::vector objectsForDeletion() const - { - // native separators and ending slash - auto prettyDir = [](auto s) { - if (!s.endsWith("/") || !s.endsWith("\\")) { - s += "/"; - } - - return QDir::toNativeSeparators(s); - }; - - // native separators - auto prettyFile = [](auto s) { - return QDir::toNativeSeparators(s); - }; - - - // lowercase, native separators and ending slash - auto canonicalDir = [](auto s) { - s = s.toLower(); - if (!s.endsWith("/") || !s.endsWith("\\")) { - s += "/"; - } - - return QDir::toNativeSeparators(s); - }; - - // lower and native separators - auto canonicalFile = [](auto s) { - return QDir::toNativeSeparators(s.toLower()); - }; - - - - // whether the given directory is contained in the root - auto dirInRoot = [&](auto root, auto dir) { - return canonicalDir(dir).startsWith(canonicalDir(root)); - }; - - // whether the given file is contained in the root - auto fileInRoot = [&](auto root, auto file) { - return canonicalFile(file).startsWith(canonicalDir(root)); - }; - +QIcon instanceIcon(const PluginContainer& pc, const Instance& i) +{ + const auto* game = InstanceManager::singleton() + .gamePluginForDirectory(i.directory(), pc); + if (game) + return game->gameIcon(); - const auto loc = location(); - const auto base = m_settings->paths().base(); - - - // directories that might contain the individual files and directories - // set in the path settings - std::vector roots; - - // a portable instance has its location in the installation directory, - // don't delete that - if (!isPortable()) { - if (QDir(loc).exists()) { - roots.push_back({loc, true}); - } - } - - // the base directory is the location directory by default, don't add it - // if it's the same - if (canonicalDir(base) != canonicalDir(loc)) { - if (QDir(base).exists()) { - roots.push_back({base, false}); - } - } - - - // all the directories that are part of an instance; none of them are - // mandatory for deletion - const std::vector dirs = { - m_settings->paths().downloads(), - m_settings->paths().mods(), - m_settings->paths().cache(), - m_settings->paths().profiles(), - m_settings->paths().overwrite(), - m_dir.filePath(QString::fromStdWString(AppConfig::dumpsDir())), - m_dir.filePath(QString::fromStdWString(AppConfig::logPath())), - }; - - // all the files that are part of an instance - const std::vector files = { - {iniFile(), true}, // the ini file must be deleted - }; - - - // this will contain the root directories, plus all the individual - // directories that are not inside these roots - std::vector cleanDirs; - - for (const auto& f : dirs) { - bool inRoots = false; - - for (const auto& root : roots) { - if (dirInRoot(root.path, f.path)) { - inRoots = true; - break; - } - } - - if (!inRoots) { - // not in roots, this is a path that was changed by the user; make - // sure it exists - if (QDir(f.path).exists()) { - cleanDirs.push_back({prettyDir(f.path), f.mandatoryDelete}); - } - } - } - - // prepending the roots - for (auto itor=roots.rbegin(); itor!=roots.rend(); ++itor) { - cleanDirs.insert( - cleanDirs.begin(), - {prettyDir(itor->path), itor->mandatoryDelete}); - } - - - - // this will contain the individual files that are not inside the roots; - // not that this only contains the INI file for now, so most of this is - // useless - std::vector cleanFiles; - - for (const auto& f : files) { - bool inRoots = false; - - for (const auto& root : roots) { - if (fileInRoot(root.path, f.path)) { - inRoots = true; - break; - } - } - - if (!inRoots) { - // not in roots, this is a path that was changed by the user; make - // sure it exists - if (QFileInfo(f.path).exists()) { - cleanFiles.push_back({prettyFile(f.path), f.mandatoryDelete}); - } - } - } - - - // contains all the directories and files to be deleted - std::vector all; - all.insert(all.end(), cleanDirs.begin(), cleanDirs.end()); - all.insert(all.end(), cleanFiles.begin(), cleanFiles.end()); - - // mandatory on top - std::stable_sort(all.begin(), all.end()); - - return all; - } - -private: - const bool m_portable; - QDir m_dir; - std::unique_ptr m_settings; -}; + QPixmap empty(32, 32); + empty.fill(QColor(0, 0, 0, 0)); + return QIcon(empty); +} InstanceManagerDialog::~InstanceManagerDialog() = default; @@ -352,7 +79,7 @@ void InstanceManagerDialog::updateInstances() m_instances.clear(); for (auto&& d : m.globalInstancePaths()) { - m_instances.push_back(std::make_unique(d, false)); + m_instances.push_back(std::make_unique(d, false)); } // sort first, prepend portable after so it's always on top @@ -363,7 +90,12 @@ void InstanceManagerDialog::updateInstances() if (m.portableInstanceExists()) { m_instances.insert( m_instances.begin(), - std::make_unique(m.portablePath(), true)); + std::make_unique(m.portablePath(), true)); + } + + // read all inis, ignore errors + for (auto&& i : m_instances) { + i->readFromIni(); } } @@ -381,7 +113,7 @@ void InstanceManagerDialog::updateList() const auto& ii = *m_instances[i]; auto* item = new QStandardItem(ii.name()); - item->setIcon(ii.icon(m_pc)); + item->setIcon(instanceIcon(m_pc, ii)); m_model->appendRow(item); @@ -568,9 +300,9 @@ void InstanceManagerDialog::rename() return; } - const QString src = i->location(); + const QString src = i->directory(); const QString dest = QDir::toNativeSeparators( - QFileInfo(i->location()).dir().path() + "/" + newName); + QFileInfo(src).dir().path() + "/" + newName); const auto r = shell::Rename(src, dest, false); if (!r) { @@ -582,15 +314,19 @@ void InstanceManagerDialog::rename() return; } + auto newInstance = std::make_unique(dest, false); + i = newInstance.get(); + m_model->item(selIndex)->setText(newName); - i->setDir(dest); + m_instances[selIndex] = std::move(newInstance); + fillData(*i); } void InstanceManagerDialog::exploreLocation() { if (const auto* i=singleSelection()) { - shell::Explore(i->location()); + shell::Explore(i->directory()); } } @@ -604,14 +340,14 @@ void InstanceManagerDialog::exploreBaseDirectory() void InstanceManagerDialog::exploreGame() { if (const auto* i=singleSelection()) { - shell::Explore(i->gamePath()); + shell::Explore(i->gameDirectory()); } } void InstanceManagerDialog::openINI() { if (const auto* i=singleSelection()) { - shell::Open(i->iniFile()); + shell::Open(i->iniPath()); } } @@ -707,6 +443,15 @@ void InstanceManagerDialog::setRestartOnSelect(bool b) bool InstanceManagerDialog::doDelete(const QStringList& files, bool recycle) { + // logging + for (auto&& f : files) { + if (recycle) { + log::info("will recycle {}", f); + } else { + log::info("will delete {}", f); + } + } + if (MOBase::shellDelete(files, recycle, this)) { return true; } @@ -771,17 +516,7 @@ std::size_t InstanceManagerDialog::singleSelectionIndex() const return static_cast(sel.indexes()[0].row()); } -InstanceInfo* InstanceManagerDialog::singleSelection() -{ - const auto i = singleSelectionIndex(); - if (i == NoSelection) { - return nullptr; - } - - return m_instances[i].get(); -} - -const InstanceInfo* InstanceManagerDialog::singleSelection() const +const Instance* InstanceManagerDialog::singleSelection() const { const auto i = singleSelectionIndex(); if (i == NoSelection) { @@ -791,13 +526,13 @@ const InstanceInfo* InstanceManagerDialog::singleSelection() const return m_instances[i].get(); } -void InstanceManagerDialog::fillData(const InstanceInfo& ii) +void InstanceManagerDialog::fillData(const Instance& ii) { ui->name->setText(ii.name()); - ui->location->setText(ii.location()); + ui->location->setText(ii.directory()); ui->baseDirectory->setText(ii.baseDirectory()); ui->gameName->setText(ii.gameName()); - ui->gameDir->setText(ii.gamePath()); + ui->gameDir->setText(ii.gameDirectory()); setButtonsEnabled(true); const auto& m = InstanceManager::singleton(); diff --git a/src/instancemanagerdialog.h b/src/instancemanagerdialog.h index 5b08ffc2..94f379ca 100644 --- a/src/instancemanagerdialog.h +++ b/src/instancemanagerdialog.h @@ -6,9 +6,11 @@ namespace Ui { class InstanceManagerDialog; }; -class InstanceInfo; +class Instance; class PluginContainer; +// a dialog to manage existing instances +// class InstanceManagerDialog : public QDialog { Q_OBJECT @@ -19,21 +21,61 @@ public: ~InstanceManagerDialog(); + // selects the instance having the given index in the list + // void select(std::size_t i); + + // selects the instance by name + // void select(const QString& name); + + // select the instance that is currently in use in MO + // void selectActiveInstance(); + + // switches to the selected instance; restarts MO, unless + // was called setRestartOnSelect(false) + // void openSelectedInstance(); + + // renames the currently selected instance + // void rename(); + + // explores the directory of the selected instance + // void exploreLocation(); + + // explores the base directory of the selected instance + // void exploreBaseDirectory(); + + // explores the game directory of the selected instance + // void exploreGame(); + + // converts the selected, portable instance to a global one; not implemented + // void convertToGlobal(); + + // converts the selected, global instance to a portable one; not implemented + // void convertToPortable(); + + // opens the ini of the selected instance in the shell + // void openINI(); + + // deletes the selected instance + // void deleteInstance(); + + // sets whether the dialog should restart MO when selecting an instance; this + // is false on startup when no instances exist + // void setRestartOnSelect(bool b); private: @@ -41,27 +83,51 @@ private: std::unique_ptr ui; const PluginContainer& m_pc; - std::vector> m_instances; + std::vector> m_instances; MOBase::FilterWidget m_filter; QStandardItemModel* m_model; bool m_restartOnSelect; + // refreshes the list instances from disk + // void updateInstances(); + // updates the ui for the selected instance + // void onSelection(); + + // opens the create instance dialog + // void createNew(); + + // returns the index of selected instance, NoSelection if none + // std::size_t singleSelectionIndex() const; - InstanceInfo* singleSelection(); - const InstanceInfo* singleSelection() const; + // returns the InstanceInfo associated with the selected instance, null if + // none + // + const Instance* singleSelection() const; + + // fills the instance list on the ui + // void updateList(); - void fillData(const InstanceInfo& ii); + + // fills the ui for the selected instance + // + void fillData(const Instance& ii); + + // clears the ui when there's no selection + // void clearData(); + + // enables/disables buttons like rename, explore... + // void setButtonsEnabled(bool b); - bool deletePortable(const InstanceInfo& ii); - bool deleteGlobal(const InstanceInfo& ii); + // deletes the given files, returns false on error + // bool doDelete(const QStringList& files, bool recycle); }; diff --git a/src/instancemanagerdialog.ui b/src/instancemanagerdialog.ui index 410b58c1..45b99e21 100644 --- a/src/instancemanagerdialog.ui +++ b/src/instancemanagerdialog.ui @@ -330,7 +330,7 @@ - Delete instance + Delete instance... diff --git a/src/main.cpp b/src/main.cpp index 7f0886f6..a2653685 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -662,17 +662,17 @@ int doOneRun( } else { - if (!currentInstance->directory().exists()) { + if (!QDir(currentInstance->directory()).exists()) { // the previously used instance doesn't exist anymore if (m.hasAnyInstances()) { criticalOnTop(QObject::tr( "Instance at '%1' not found. Select another instance.") - .arg(currentInstance->directory().absolutePath())); + .arg(currentInstance->directory())); } else { criticalOnTop(QObject::tr( "Instance at '%1' not found. You must create a new instance") - .arg(currentInstance->directory().absolutePath())); + .arg(currentInstance->directory())); } currentInstance = selectInstance(); @@ -682,7 +682,7 @@ int doOneRun( } } - const QString dataPath = currentInstance->directory().path(); + const QString dataPath = currentInstance->directory(); application.setProperty("dataPath", dataPath); setExceptionHandlers(); -- cgit v1.3.1 From bc157095a56596e697d40371688926953b2c5533 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Fri, 6 Nov 2020 14:43:33 -0500 Subject: moved getInstanceName() up removed individual getters in CreateInstanceDialog, they were duplicating the work in creationInfo() only VariantsPage needs access to the game plugin during creationInfo(), so pass it instead selecting an undetected game, choosing a valid path and going back to the games page would not display the selected path in the list --- src/createinstancedialog.cpp | 65 +++++--------- src/createinstancedialog.h | 99 ++++++++++++++++++---- src/createinstancedialogpages.cpp | 76 +++++++++-------- src/createinstancedialogpages.h | 4 +- src/instancemanagerdialog.cpp | 172 +++++++++++++++++++++----------------- 5 files changed, 243 insertions(+), 173 deletions(-) (limited to 'src') diff --git a/src/createinstancedialog.cpp b/src/createinstancedialog.cpp index 13dd200c..c2cdde49 100644 --- a/src/createinstancedialog.cpp +++ b/src/createinstancedialog.cpp @@ -413,49 +413,6 @@ bool CreateInstanceDialog::canBack() const return false; } -CreateInstanceDialog::Types CreateInstanceDialog::instanceType() const -{ - return getSelected(&cid::Page::selectedInstanceType); -} - -MOBase::IPluginGame* CreateInstanceDialog::game() const -{ - return getSelected(&cid::Page::selectedGame); -} - -QString CreateInstanceDialog::gameLocation() const -{ - return getSelected(&cid::Page::selectedGameLocation); -} - -QString CreateInstanceDialog::gameVariant() const -{ - return getSelected(&cid::Page::selectedGameVariant); -} - -QString CreateInstanceDialog::instanceName() const -{ - return getSelected(&cid::Page::selectedInstanceName); -} - -QString CreateInstanceDialog::dataPath() const -{ - QString s; - - if (instanceType() == Portable) { - s = QDir(InstanceManager::singleton().portablePath()).absolutePath(); - } else { - s = InstanceManager::singleton().instancePath(instanceName()); - } - - return QDir::toNativeSeparators(s); -} - -CreateInstanceDialog::Paths CreateInstanceDialog::paths() const -{ - return getSelected(&cid::Page::selectedPaths); -} - bool CreateInstanceDialog::switching() const { return m_switching; @@ -482,7 +439,8 @@ void fixFilePath(QString& path) path = QDir::toNativeSeparators(QFileInfo(path).absolutePath()); } -CreateInstanceDialog::CreationInfo CreateInstanceDialog::creationInfo() const +CreateInstanceDialog::CreationInfo +CreateInstanceDialog::rawCreationInfo() const { const auto iniFilename = QString::fromStdWString(AppConfig::iniFileName()); @@ -491,12 +449,27 @@ CreateInstanceDialog::CreationInfo CreateInstanceDialog::creationInfo() const ci.type = getSelected(&cid::Page::selectedInstanceType); ci.game = getSelected(&cid::Page::selectedGame); ci.gameLocation = getSelected(&cid::Page::selectedGameLocation); - ci.gameVariant = getSelected(&cid::Page::selectedGameVariant); + ci.gameVariant = getSelected(&cid::Page::selectedGameVariant, ci.game); ci.instanceName = getSelected(&cid::Page::selectedInstanceName); ci.paths = getSelected(&cid::Page::selectedPaths); - ci.dataPath = dataPath(); + + if (ci.type == Portable) { + ci.dataPath = QDir(InstanceManager::singleton().portablePath()).absolutePath(); + } else { + ci.dataPath = InstanceManager::singleton().instancePath(ci.instanceName); + } + + ci.dataPath = QDir::toNativeSeparators(ci.dataPath); ci.iniPath = ci.dataPath + "/" + iniFilename; + return ci; +} + +CreateInstanceDialog::CreationInfo +CreateInstanceDialog::creationInfo() const +{ + auto ci = rawCreationInfo(); + fixDirPath(ci.paths.base); fixFilePath(ci.paths.ini); diff --git a/src/createinstancedialog.h b/src/createinstancedialog.h index 25e383eb..d541f45e 100644 --- a/src/createinstancedialog.h +++ b/src/createinstancedialog.h @@ -10,11 +10,25 @@ namespace cid { class Page; } class PluginContainer; class Settings; +// this is a wizard for creating a new instance, it is made out of Page objects, +// see createinstancedialogpages.h +// +// each page can give back one or more pieces of information that is collected +// in creationInfo() and used by finish() to do the actual creation +// +// pages can be disabled if they return true in skip(), which happens globally +// for some (IntroPage has a setting in the registry), depending on context +// (NexusPage is skipped if the API key already exists) or explicitly (when +// only some info about the instance is missing on startup, such as a game +// variant) +// class CreateInstanceDialog : public QDialog { Q_OBJECT public: + // instance type + // enum Types { NoType = 0, @@ -22,6 +36,10 @@ public: Portable }; + // all the paths required by the instance, some may be empty, such as + // basically all of them except for `base` when the user doesn't use the + // "Advanced" part of the paths page + // struct Paths { QString base; @@ -34,6 +52,8 @@ public: auto operator<=>(const Paths&) const = default; }; + // all the info filled in the various pages + // struct CreationInfo { Types type; @@ -53,10 +73,11 @@ public: ~CreateInstanceDialog(); Ui::CreateInstanceDialog* getUI(); - const PluginContainer& pluginContainer(); Settings* settings(); + // disables all the pages except for the given one, used on startup when some + // specific info is missing template void setSinglePage(const QString& instanceName) { @@ -71,6 +92,8 @@ public: setSinglePageImpl(instanceName); } + // returns the page having the give path, or null + // template Page* getPage() { @@ -83,24 +106,59 @@ public: return nullptr; } + + // moves to the next page calls finish() if on the last one + // void next(); + + // moves to the previous page, if any + // void back(); + + // whether the current page reports that it is ready; if this is the last + // page, next() would call finish() + // + bool canNext() const; + + // whether the current page is not the first one and there is an enabled page + // prior + // + bool canBack() const; + + // selects the given page by index; this doesn't check if the page should be + // skipped + // void selectPage(std::size_t i); + + // moves by `d` pages, can be negative to move back + // void changePage(int d); + + // creates the instance and closes the dialog + // void finish(); + + // updates the navigation buttons based on the current page + // void updateNavigation(); + + // whether this is the last enabled page + // bool isOnLastPage() const; - Types instanceType() const; - MOBase::IPluginGame* game() const; - QString gameLocation() const; - QString gameVariant() const; - QString instanceName() const; - QString dataPath() const; - Paths paths() const; + // returns whether the user has requested to switch to the new instance + // bool switching() const; + // gathers the info from all the pages as it appears, paths are not fixed; + // see creationInfo() + // + CreationInfo rawCreationInfo() const; + + // gathers the info from all the pages: paths are converted to absolute and + // the base dir variable is expanded everywhere; see rawCreationInfo() + // CreationInfo creationInfo() const; private: @@ -113,13 +171,26 @@ private: bool m_singlePage; + // called from setSinglePage(), does whatever doesn't need the T + // void setSinglePageImpl(const QString& instanceName); - template - T getSelected(T (cid::Page::*mf)() const) const + // adds a line to the creation log + // + void logCreation(const QString& s); + void logCreation(const std::wstring& s); + + // calls the given member function on all pages until one returns an object + // that's not empty; used by gatherInfo() + // + template + auto getSelected(MF mf, Args&&... args) const { + // return type + using T = decltype((std::declval().*mf)(std::forward(args)...)); + for (auto&& p : m_pages) { - const auto t = (p.get()->*mf)(); + const auto t = (p.get()->*mf)(std::forward(args)...); if (t != T()) { return t; } @@ -127,12 +198,6 @@ private: return T(); } - - void logCreation(const QString& s); - void logCreation(const std::wstring& s); - - bool canNext() const; - bool canBack() const; }; #endif // MODORGANIZER_CREATEINSTANCEDIALOG_INCLUDED diff --git a/src/createinstancedialogpages.cpp b/src/createinstancedialogpages.cpp index f2a7ab36..a08bf5a1 100644 --- a/src/createinstancedialogpages.cpp +++ b/src/createinstancedialogpages.cpp @@ -101,7 +101,7 @@ QString Page::selectedGameLocation() const return {}; } -QString Page::selectedGameVariant() const +QString Page::selectedGameVariant(MOBase::IPluginGame*) const { // no-op return {}; @@ -246,6 +246,12 @@ void GamePage::select(IPluginGame* game, const QString& dir) checked = nullptr; } else { checked = checkInstallation(path, checked); + + if (checked) { + // remember this path + checked->dir = path; + checked->installed = true; + } } } else { checked->dir = dir; @@ -255,7 +261,9 @@ void GamePage::select(IPluginGame* game, const QString& dir) } m_selection = checked; + selectButton(checked); + updateButton(checked); updateNavigation(); if (checked) { @@ -611,7 +619,7 @@ bool VariantsPage::ready() const bool VariantsPage::doSkip() const { - auto* g = m_dlg.game(); + auto* g = m_dlg.rawCreationInfo().game; if (!g) { // shouldn't happen return true; @@ -623,7 +631,7 @@ bool VariantsPage::doSkip() const void VariantsPage::activated() { - auto* g = m_dlg.game(); + auto* g = m_dlg.rawCreationInfo().game; if (m_previousGame != g) { m_previousGame = g; @@ -650,15 +658,13 @@ void VariantsPage::select(const QString& variant) } } -QString VariantsPage::selectedGameVariant() const +QString VariantsPage::selectedGameVariant(MOBase::IPluginGame* game) const { - auto* g = m_dlg.game(); - if (!g) { - // shouldn't happen + if (!game) { return {}; } - const auto variants = g->gameVariants(); + const auto variants = game->gameVariants(); if (variants.size() < 2) { return {}; } else { @@ -671,7 +677,7 @@ void VariantsPage::fillList() ui->editions->clear(); m_buttons.clear(); - auto* g = m_dlg.game(); + auto* g = m_dlg.rawCreationInfo().game; if (!g) { // shouldn't happen return; @@ -708,12 +714,12 @@ bool NamePage::ready() const bool NamePage::doSkip() const { - return (m_dlg.instanceType() == CreateInstanceDialog::Portable); + return (m_dlg.rawCreationInfo().type == CreateInstanceDialog::Portable); } void NamePage::activated() { - auto* g = m_dlg.game(); + auto* g = m_dlg.rawCreationInfo().game; if (!g) { // shouldn't happen, next should be disabled return; @@ -820,8 +826,8 @@ bool PathsPage::ready() const void PathsPage::activated() { - const auto name = m_dlg.instanceName(); - const auto type = m_dlg.instanceType(); + const auto name = m_dlg.rawCreationInfo().instanceName; + const auto type = m_dlg.rawCreationInfo().type; const bool changed = (m_lastInstanceName != name) || (m_lastType != type); @@ -829,7 +835,7 @@ void PathsPage::activated() checkPaths(); updateNavigation(); - m_label.setText(m_dlg.game()->gameName()); + m_label.setText(m_dlg.rawCreationInfo().game->gameName()); m_lastInstanceName = name; m_lastType = type; } @@ -898,7 +904,7 @@ void PathsPage::setPaths(const QString& name, bool force) { QString path; - if (m_dlg.instanceType() == CreateInstanceDialog::Portable) { + if (m_dlg.rawCreationInfo().type == CreateInstanceDialog::Portable) { path = InstanceManager::singleton().portablePath(); } else { const auto root = InstanceManager::singleton().globalInstancesRootPath(); @@ -939,7 +945,7 @@ bool PathsPage::checkPath( const QDir d(path); if (InstanceManager::singleton().validInstanceName(d.dirName())) { - if (m_dlg.instanceType() == CreateInstanceDialog::Portable) { + if (m_dlg.rawCreationInfo().type == CreateInstanceDialog::Portable) { // the default data path for a portable instance is the application // directory, so it's not an error if it exists if (QDir(path) != InstanceManager::singleton().portablePath()) { @@ -1041,37 +1047,41 @@ QString ConfirmationPage::toLocalizedString(CreateInstanceDialog::Types t) const QString ConfirmationPage::makeReview() const { QStringList lines; - const auto paths = m_dlg.paths(); - lines.push_back(QObject::tr("Instance type: %1").arg(toLocalizedString(m_dlg.instanceType()))); - lines.push_back(QObject::tr("Instance location: %1").arg(m_dlg.dataPath())); + const auto ci = m_dlg.rawCreationInfo(); + + lines.push_back(QObject::tr("Instance type: %1") + .arg(toLocalizedString(ci.type))); + + lines.push_back(QObject::tr("Instance location: %1") + .arg(ci.dataPath)); - if (m_dlg.instanceType() != CreateInstanceDialog::Portable) { - lines.push_back(QObject::tr("Instance name: %1").arg(m_dlg.instanceName())); + if (ci.type != CreateInstanceDialog::Portable) { + lines.push_back(QObject::tr("Instance name: %1").arg(ci.instanceName)); } - if (paths.downloads.isEmpty()) { + if (ci.paths.downloads.isEmpty()) { // simple settings - if (paths.base != m_dlg.dataPath()) { - lines.push_back(QObject::tr("Base directory: %1").arg(paths.base)); + if (ci.paths.base != ci.dataPath) { + lines.push_back(QObject::tr("Base directory: %1").arg(ci.paths.base)); } } else { // advanced settings - lines.push_back(QObject::tr("Base directory: %1").arg(paths.base)); - lines.push_back(dirLine(QObject::tr("Downloads"), paths.downloads)); - lines.push_back(dirLine(QObject::tr("Mods"), paths.mods)); - lines.push_back(dirLine(QObject::tr("Profiles"), paths.profiles)); - lines.push_back(dirLine(QObject::tr("Overwrite"), paths.overwrite)); + lines.push_back(QObject::tr("Base directory: %1").arg(ci.paths.base)); + lines.push_back(dirLine(QObject::tr("Downloads"), ci.paths.downloads)); + lines.push_back(dirLine(QObject::tr("Mods"), ci.paths.mods)); + lines.push_back(dirLine(QObject::tr("Profiles"), ci.paths.profiles)); + lines.push_back(dirLine(QObject::tr("Overwrite"), ci.paths.overwrite)); } // game - QString name = m_dlg.game()->gameName(); - if (!m_dlg.gameVariant().isEmpty()) { - name += " (" + m_dlg.gameVariant() + ")"; + QString name = ci.game->gameName(); + if (!ci.gameVariant.isEmpty()) { + name += " (" + ci.gameVariant + ")"; } lines.push_back(QObject::tr("Game: %1").arg(name)); - lines.push_back(QObject::tr("Game location: %1").arg(m_dlg.gameLocation())); + lines.push_back(QObject::tr("Game location: %1").arg(ci.gameLocation)); return lines.join("\n"); } diff --git a/src/createinstancedialogpages.h b/src/createinstancedialogpages.h index e239c196..099071a2 100644 --- a/src/createinstancedialogpages.h +++ b/src/createinstancedialogpages.h @@ -48,7 +48,7 @@ public: virtual CreateInstanceDialog::Types selectedInstanceType() const; virtual MOBase::IPluginGame* selectedGame() const; virtual QString selectedGameLocation() const; - virtual QString selectedGameVariant() const; + virtual QString selectedGameVariant(MOBase::IPluginGame* game) const; virtual QString selectedInstanceName() const; virtual CreateInstanceDialog::Paths selectedPaths() const; @@ -146,7 +146,7 @@ public: bool ready() const override; void activated() override; - QString selectedGameVariant() const override; + QString selectedGameVariant(MOBase::IPluginGame* game) const override; void select(const QString& variant); diff --git a/src/instancemanagerdialog.cpp b/src/instancemanagerdialog.cpp index c199fa7b..71fed78d 100644 --- a/src/instancemanagerdialog.cpp +++ b/src/instancemanagerdialog.cpp @@ -13,7 +13,9 @@ using namespace MOBase; - +// returns the icon for the given instance or an empty 32x32 icon if the game +// plugin couldn't be found +// QIcon instanceIcon(const PluginContainer& pc, const Instance& i) { const auto* game = InstanceManager::singleton() @@ -27,6 +29,78 @@ QIcon instanceIcon(const PluginContainer& pc, const Instance& i) return QIcon(empty); } +// pops up a dialog to ask for an instance name when renaming +// +QString getInstanceName( + QWidget* parent, const QString& title, const QString& moreText, + const QString& label, const QString& oldName={}) +{ + auto& m = InstanceManager::singleton(); + + QDialog dlg(parent); + dlg.setWindowTitle(title); + + auto* ly = new QVBoxLayout(&dlg); + + auto* bb = new QDialogButtonBox( + QDialogButtonBox::Cancel | QDialogButtonBox::Ok); + + auto* text = new QLineEdit(oldName); + text->selectAll(); + + auto* error = new QLabel; + + if (!moreText.isEmpty()) { + auto* lb = new QLabel(moreText); + lb->setWordWrap(true); + ly->addWidget(lb); + ly->addSpacing(10); + } + + auto* lb = new QLabel(label); + lb->setWordWrap(true); + ly->addWidget(lb); + + ly->addWidget(text); + ly->addWidget(error); + ly->addStretch(); + ly->addWidget(bb); + + auto check = [&] { + bool okay = false; + + if (text->text().isEmpty()) { + error->setText(""); + } else if (!m.validInstanceName(text->text())) { + error->setText(QObject::tr("The instance name must be a valid folder name.")); + } else { + const auto name = m.sanitizeInstanceName(text->text()); + + if ((name != oldName) && m.instanceExists(text->text())) { + error->setText(QObject::tr("An instance with this name already exists.")); + } else { + okay = true; + } + } + + error->setVisible(!okay); + bb->button(QDialogButtonBox::Ok)->setEnabled(okay); + }; + + QObject::connect(text, &QLineEdit::textChanged, [&] { check(); }); + QObject::connect(bb, &QDialogButtonBox::accepted, [&]{ dlg.accept(); }); + QObject::connect(bb, &QDialogButtonBox::rejected, [&]{ dlg.reject(); }); + + check(); + + dlg.resize({400, 120}); + if (dlg.exec() != QDialog::Accepted) { + return {}; + } + + return m.sanitizeInstanceName(text->text()); +} + InstanceManagerDialog::~InstanceManagerDialog() = default; @@ -106,9 +180,9 @@ void InstanceManagerDialog::updateList() m_model->clear(); - const std::size_t NoSel = -1; - std::size_t sel = NoSel; + std::size_t sel = NoSelection; + // creating items for instances for (std::size_t i=0; i= m_instances.size()) { sel = m_instances.size() - 1; } else { @@ -207,76 +281,6 @@ void InstanceManagerDialog::openSelectedInstance() accept(); } -QString getInstanceName( - QWidget* parent, const QString& title, const QString& moreText, - const QString& label, const QString& oldName={}) -{ - auto& m = InstanceManager::singleton(); - - QDialog dlg(parent); - dlg.setWindowTitle(title); - - auto* ly = new QVBoxLayout(&dlg); - - auto* bb = new QDialogButtonBox( - QDialogButtonBox::Cancel | QDialogButtonBox::Ok); - - auto* text = new QLineEdit(oldName); - text->selectAll(); - - auto* error = new QLabel; - - if (!moreText.isEmpty()) { - auto* lb = new QLabel(moreText); - lb->setWordWrap(true); - ly->addWidget(lb); - ly->addSpacing(10); - } - - auto* lb = new QLabel(label); - lb->setWordWrap(true); - ly->addWidget(lb); - - ly->addWidget(text); - ly->addWidget(error); - ly->addStretch(); - ly->addWidget(bb); - - auto check = [&] { - bool okay = false; - - if (text->text().isEmpty()) { - error->setText(""); - } else if (!m.validInstanceName(text->text())) { - error->setText(QObject::tr("The instance name must be a valid folder name.")); - } else { - const auto name = m.sanitizeInstanceName(text->text()); - - if ((name != oldName) && m.instanceExists(text->text())) { - error->setText(QObject::tr("An instance with this name already exists.")); - } else { - okay = true; - } - } - - error->setVisible(!okay); - bb->button(QDialogButtonBox::Ok)->setEnabled(okay); - }; - - QObject::connect(text, &QLineEdit::textChanged, [&] { check(); }); - QObject::connect(bb, &QDialogButtonBox::accepted, [&]{ dlg.accept(); }); - QObject::connect(bb, &QDialogButtonBox::rejected, [&]{ dlg.reject(); }); - - check(); - - dlg.resize({400, 120}); - if (dlg.exec() != QDialog::Accepted) { - return {}; - } - - return m.sanitizeInstanceName(text->text()); -} - void InstanceManagerDialog::rename() { auto* i = singleSelection(); @@ -293,6 +297,8 @@ void InstanceManagerDialog::rename() return; } + + // getting new name const auto newName = getInstanceName( this, tr("Rename instance"), "", tr("Instance name"), i->name()); @@ -300,11 +306,16 @@ void InstanceManagerDialog::rename() return; } + + // renaming const QString src = i->directory(); const QString dest = QDir::toNativeSeparators( QFileInfo(src).dir().path() + "/" + newName); + log::info("renaming {} to {}", src, dest); + const auto r = shell::Rename(src, dest, false); + if (!r) { QMessageBox::critical( this, tr("Error"), @@ -314,6 +325,8 @@ void InstanceManagerDialog::rename() return; } + + // updating ui auto newInstance = std::make_unique(dest, false); i = newInstance.get(); @@ -365,6 +378,8 @@ void InstanceManagerDialog::deleteInstance() return; } + // creating dialog + const auto Recycle = QMessageBox::Save; const auto Delete = QMessageBox::Yes; const auto Cancel = QMessageBox::Cancel; @@ -388,6 +403,8 @@ void InstanceManagerDialog::deleteInstance() list->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); list->setMaximumHeight(160); + + // filling the list for (const auto& f : files) { auto* item = new QListWidgetItem(f.path); @@ -412,6 +429,7 @@ void InstanceManagerDialog::deleteInstance() } + // gathering all the selected items QStringList selected; for (int i=0; icount(); ++i) { @@ -427,10 +445,14 @@ void InstanceManagerDialog::deleteInstance() return; } + + // deleting if (!doDelete(selected, (r == Recycle))) { return; } + + // updating ui updateInstances(); updateList(); } @@ -501,7 +523,7 @@ void InstanceManagerDialog::createNew() updateInstances(); updateList(); - select(dlg.instanceName()); + select(dlg.creationInfo().instanceName); } std::size_t InstanceManagerDialog::singleSelectionIndex() const -- cgit v1.3.1 From b962d8081624ffdd7ab5cecbb6b2701b000b6b73 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Fri, 6 Nov 2020 14:53:24 -0500 Subject: moved DirectoryCreator up, moved fix functions as lambdas --- src/createinstancedialog.cpp | 240 +++++++++++++++++++++++-------------------- 1 file changed, 129 insertions(+), 111 deletions(-) (limited to 'src') diff --git a/src/createinstancedialog.cpp b/src/createinstancedialog.cpp index c2cdde49..f52d3ebd 100644 --- a/src/createinstancedialog.cpp +++ b/src/createinstancedialog.cpp @@ -10,6 +10,95 @@ using namespace MOBase; + +class Failed {}; + +class DirectoryCreator +{ +public: + DirectoryCreator(const DirectoryCreator&) = delete; + DirectoryCreator& operator=(const DirectoryCreator&) = delete; + + static std::unique_ptr create( + const QDir& target, std::function log) + { + return std::unique_ptr(new DirectoryCreator(target, log)); + } + + ~DirectoryCreator() + { + rollback(); + } + + void commit() + { + m_created.clear(); + } + + void rollback() noexcept + { + try + { + for (auto itor=m_created.rbegin(); itor!=m_created.rend(); ++itor) { + const auto r = shell::DeleteDirectoryRecursive(*itor); + if (!r) { + m_logger(r.toString()); + } + } + + m_created.clear(); + } + catch(...) + { + // eat it + } + } + +private: + std::function m_logger; + + DirectoryCreator(const QDir& target, std::function log) + : m_logger(log) + { + try + { + const QString s = QDir::toNativeSeparators(target.absolutePath()); + const QStringList cs = s.split("\\"); + + if (cs.empty()) { + return; + } + + QDir d(cs[0]); + + for (int i=1; i m_created; +}; + + CreateInstanceDialog::CreateInstanceDialog( const PluginContainer& pc, Settings* s, QWidget *parent) : QDialog(parent), ui(new Ui::CreateInstanceDialog), m_pc(pc), m_settings(s), @@ -121,6 +210,7 @@ void CreateInstanceDialog::changePage(int d) // first/last page if (d > 0) { + // forwards for (;;) { ++i; @@ -133,6 +223,7 @@ void CreateInstanceDialog::changePage(int d) } } } else { + // backwards for (;;) { if (i == 0) { break; @@ -151,94 +242,6 @@ void CreateInstanceDialog::changePage(int d) } } - -class Failed {}; - -class DirectoryCreator -{ -public: - DirectoryCreator(const DirectoryCreator&) = delete; - DirectoryCreator& operator=(const DirectoryCreator&) = delete; - - static std::unique_ptr create( - const QDir& target, std::function log) - { - return std::unique_ptr(new DirectoryCreator(target, log)); - } - - ~DirectoryCreator() - { - rollback(); - } - - void commit() - { - m_created.clear(); - } - - void rollback() noexcept - { - try - { - for (auto itor=m_created.rbegin(); itor!=m_created.rend(); ++itor) { - const auto r = shell::DeleteDirectoryRecursive(*itor); - if (!r) { - m_logger(r.toString()); - } - } - - m_created.clear(); - } - catch(...) - { - // eat it - } - } - -private: - std::function m_logger; - - DirectoryCreator(const QDir& target, std::function log) - : m_logger(log) - { - try - { - const QString s = QDir::toNativeSeparators(target.absolutePath()); - const QStringList cs = s.split("\\"); - - if (cs.empty()) { - return; - } - - QDir d(cs[0]); - - for (int i=1; i m_created; -}; - void CreateInstanceDialog::finish() { ui->creationLog->clear(); @@ -260,6 +263,8 @@ void CreateInstanceDialog::finish() { std::vector> dirs; + // creating all these directories; if any of them fail, this throws and + // any newly created directory will be deleted in DirectoryCreator's dtor dirs.push_back(createDir(ci.dataPath)); dirs.push_back(createDir(ci.paths.base)); dirs.push_back(createDir(PathSettings::resolve(ci.paths.downloads, ci.paths.base))); @@ -268,6 +273,7 @@ void CreateInstanceDialog::finish() dirs.push_back(createDir(PathSettings::resolve(ci.paths.overwrite, ci.paths.base))); + // creating ini Settings s(ci.iniPath); s.game().setName(ci.game->gameName()); s.game().setDirectory(ci.gameLocation); @@ -301,7 +307,9 @@ void CreateInstanceDialog::finish() logCreation(tr("Writing %1...").arg(ci.iniPath)); + // writing ini const auto r = s.sync(); + if (r != QSettings::NoError) { switch (r) { @@ -321,16 +329,20 @@ void CreateInstanceDialog::finish() throw Failed(); } + + // committing all the directories so they don't get deleted for (auto& d : dirs) { d->commit(); } logCreation(tr("Done.")); + // remember settings if (ui->hideIntro->isChecked()) { GlobalSettings::setHideCreateInstanceIntro(true); } + // launch the new instance if (ui->launch->isChecked()) { InstanceManager::singleton().setCurrentInstance(ci.instanceName); @@ -338,15 +350,16 @@ void CreateInstanceDialog::finish() // don't restart without settings, it happens on startup when there are // no instances ExitModOrganizer(Exit::Restart); + m_switching = true; } - - m_switching = true; } + // close the dialog accept(); } catch(Failed&) { + // if Failed was thrown, all the directories have been deleted } } @@ -418,27 +431,6 @@ bool CreateInstanceDialog::switching() const return m_switching; } -void fixVarDir(QString& path, const std::wstring& defaultDir) -{ - if (path.isEmpty()) { - path = cid::makeDefaultPath(defaultDir); - } else if (!path.contains(PathSettings::BaseDirVariable)) { - path = QDir(path).absolutePath(); - } - - path = QDir::toNativeSeparators(path); -} - -void fixDirPath(QString& path) -{ - path = QDir::toNativeSeparators(QDir(path).absolutePath()); -} - -void fixFilePath(QString& path) -{ - path = QDir::toNativeSeparators(QFileInfo(path).absolutePath()); -} - CreateInstanceDialog::CreationInfo CreateInstanceDialog::rawCreationInfo() const { @@ -468,6 +460,32 @@ CreateInstanceDialog::rawCreationInfo() const CreateInstanceDialog::CreationInfo CreateInstanceDialog::creationInfo() const { + auto fixVarDir = [](QString& path, const std::wstring& defaultDir) + { + // if the path is empty, it wasn't filled by the user, probably because + // the "Advanced" checkbox wasn't checked, so use the base dir variable + // with the default dir + + if (path.isEmpty()) { + path = cid::makeDefaultPath(defaultDir); + } else if (!path.contains(PathSettings::BaseDirVariable)) { + path = QDir(path).absolutePath(); + } + + path = QDir::toNativeSeparators(path); + }; + + auto fixDirPath = [](QString& path) + { + path = QDir::toNativeSeparators(QDir(path).absolutePath()); + }; + + auto fixFilePath = [](QString& path) + { + path = QDir::toNativeSeparators(QFileInfo(path).absolutePath()); + }; + + auto ci = rawCreationInfo(); fixDirPath(ci.paths.base); -- cgit v1.3.1 From baa5c3abee4960046e4fe1e2240dd1e73253c43c Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Fri, 6 Nov 2020 16:13:06 -0500 Subject: documentation use an okay flag in PathsPage to avoid calling checkPaths() in ready() and make stuff mutable moved a few things around --- src/createinstancedialog.cpp | 10 ++ src/createinstancedialogpages.cpp | 173 +++++++++--------- src/createinstancedialogpages.h | 368 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 448 insertions(+), 103 deletions(-) (limited to 'src') diff --git a/src/createinstancedialog.cpp b/src/createinstancedialog.cpp index f52d3ebd..1c1b62d0 100644 --- a/src/createinstancedialog.cpp +++ b/src/createinstancedialog.cpp @@ -13,6 +13,12 @@ using namespace MOBase; class Failed {}; +// create() will create all the directories in `target`; if any path component +// fails to create, it will throw Failed +// +// unless commit() is called, all the created directories will be deleted in +// the destructor +// class DirectoryCreator { public: @@ -39,6 +45,7 @@ public: { try { + // delete each directory starting from the end for (auto itor=m_created.rbegin(); itor!=m_created.rend(); ++itor) { const auto r = shell::DeleteDirectoryRecursive(*itor); if (!r) { @@ -62,6 +69,7 @@ private: { try { + // split on separators const QString s = QDir::toNativeSeparators(target.absolutePath()); const QStringList cs = s.split("\\"); @@ -69,8 +77,10 @@ private: return; } + // root directory QDir d(cs[0]); + // for each directory after the root for (int i=1; i GamePage::sortedGamePlugins() const return v; } +void GamePage::createGames() +{ + m_games.clear(); + + for (auto* game : sortedGamePlugins()) { + m_games.push_back(std::make_unique(game)); + } +} + GamePage::Game* GamePage::findGame(IPluginGame* game) { for (auto& g : m_games) { @@ -344,13 +368,24 @@ GamePage::Game* GamePage::findGame(IPluginGame* game) return nullptr; } -void GamePage::createGames() +void GamePage::createGameButton(Game* g) { - m_games.clear(); + g->button = new QCommandLinkButton; + g->button->setCheckable(true); - for (auto* game : sortedGamePlugins()) { - m_games.push_back(std::make_unique(game)); - } + updateButton(g); + + QObject::connect(g->button, &QAbstractButton::clicked, [g, this] { + select(g->game); + }); +} + +void GamePage::addButton(QAbstractButton* b) +{ + auto* ly = static_cast(ui->games->layout()); + + // insert before the stretch + ly->insertWidget(ly->count() - 1, b); } void GamePage::updateButton(Game* g) @@ -407,6 +442,26 @@ void GamePage::selectButton(Game* g) } } +void GamePage::clearButtons() +{ + auto* ly = static_cast(ui->games->layout()); + + ui->games->setUpdatesEnabled(false); + + // delete all children + qDeleteAll(ui->games->findChildren("", Qt::FindDirectChildrenOnly)); + + // stretch widgets added with addStretch() are not in the parent widget, + // they have to be deleted from the layout itself + while (auto* child=ly->takeAt(0)) + delete child; + + // add a stretch, buttons will be added before + ly->addStretch(); + + ui->games->setUpdatesEnabled(true); +} + QCommandLinkButton* GamePage::createCustomButton() { auto* b = new QCommandLinkButton; @@ -422,18 +477,6 @@ QCommandLinkButton* GamePage::createCustomButton() return b; } -void GamePage::createGameButton(Game* g) -{ - g->button = new QCommandLinkButton; - g->button->setCheckable(true); - - updateButton(g); - - QObject::connect(g->button, &QAbstractButton::clicked, [g, this] { - select(g->game); - }); -} - void GamePage::fillList() { const bool showAll = ui->showAllGames->isChecked(); @@ -459,34 +502,6 @@ void GamePage::fillList() addButton(createCustomButton()); } -void GamePage::clearButtons() -{ - auto* ly = static_cast(ui->games->layout()); - - ui->games->setUpdatesEnabled(false); - - // delete all children - qDeleteAll(ui->games->findChildren("", Qt::FindDirectChildrenOnly)); - - // stretch widgets added with addStretch() are not in the parent widget, - // they have to be deleted from the layout itself - while (auto* child=ly->takeAt(0)) - delete child; - - // add a stretch, buttons will be added before - ly->addStretch(); - - ui->games->setUpdatesEnabled(true); -} - -void GamePage::addButton(QAbstractButton* b) -{ - auto* ly = static_cast(ui->games->layout()); - - // insert before the stretch - ly->insertWidget(ly->count() - 1, b); -} - GamePage::Game* GamePage::checkInstallation(const QString& path, Game* g) { if (g->game->looksValid(path)) { @@ -495,7 +510,15 @@ GamePage::Game* GamePage::checkInstallation(const QString& path, Game* g) } // the selected game can't use that folder, find another one - auto* otherGame = findAnotherGame(path); + IPluginGame* otherGame = nullptr; + + for (auto* gg : m_pc.plugins()) { + if (gg->looksValid(path)) { + otherGame = gg; + break; + } + } + if (otherGame == g->game) { // shouldn't happen, but okay return g; @@ -531,17 +554,6 @@ GamePage::Game* GamePage::checkInstallation(const QString& path, Game* g) return g; } -IPluginGame* GamePage::findAnotherGame(const QString& path) -{ - for (auto* otherGame : m_pc.plugins()) { - if (otherGame->looksValid(path)) { - return otherGame; - } - } - - return nullptr; -} - bool GamePage::confirmUnknown(const QString& path, IPluginGame* game) { const auto r = TaskDialog(&m_dlg) @@ -733,7 +745,7 @@ void NamePage::activated() m_modified = false; } - updateWarnings(); + verify(); } QString NamePage::selectedInstanceName() const @@ -749,13 +761,12 @@ QString NamePage::selectedInstanceName() const void NamePage::onChanged() { m_modified = true; - updateWarnings(); + verify(); } -void NamePage::updateWarnings() +void NamePage::verify() { const auto root = InstanceManager::singleton().globalInstancesRootPath(); - m_okay = checkName(root, ui->instanceName->text()); updateNavigation(); } @@ -803,7 +814,8 @@ PathsPage::PathsPage(CreateInstanceDialog& dlg) : m_label(ui->pathsLabel), m_simpleExists(ui->locationExists), m_simpleInvalid(ui->locationInvalid), m_advancedExists(ui->advancedDirExists), - m_advancedInvalid(ui->advancedDirInvalid) + m_advancedInvalid(ui->advancedDirInvalid), + m_okay(false) { QObject::connect(ui->location, &QLineEdit::textEdited, [&]{ onChanged(); }); QObject::connect(ui->base, &QLineEdit::textEdited, [&]{ onChanged(); }); @@ -821,7 +833,7 @@ PathsPage::PathsPage(CreateInstanceDialog& dlg) : bool PathsPage::ready() const { - return checkPaths(); + return m_okay; } void PathsPage::activated() @@ -863,21 +875,27 @@ void PathsPage::onChanged() updateNavigation(); } -bool PathsPage::checkPaths() const +void PathsPage::checkPaths() { if (ui->advancedPathOptions->isChecked()) { - return + m_okay = checkAdvancedPath(ui->base->text()) && checkAdvancedPath(resolve(ui->downloads->text())) && checkAdvancedPath(resolve(ui->mods->text())) && checkAdvancedPath(resolve(ui->profiles->text())) && checkAdvancedPath(resolve(ui->overwrite->text())); } else { - return checkPath(ui->location->text(), m_simpleExists, m_simpleInvalid); + m_okay = + checkSimplePath(ui->location->text()); } } -bool PathsPage::checkAdvancedPath(const QString& path) const +bool PathsPage::checkSimplePath(const QString& path) +{ + return checkPath(path, m_simpleExists, m_simpleInvalid); +} + +bool PathsPage::checkAdvancedPath(const QString& path) { return checkPath(path, m_advancedExists, m_advancedInvalid); } @@ -931,7 +949,7 @@ void PathsPage::setIfEmpty(QLineEdit* e, const QString& path, bool force) bool PathsPage::checkPath( QString path, - PlaceholderLabel& existsLabel, PlaceholderLabel& invalidLabel) const + PlaceholderLabel& existsLabel, PlaceholderLabel& invalidLabel) { bool exists = false; bool invalid = false; @@ -1013,10 +1031,6 @@ bool NexusPage::doSkip() const return m_skip; } -void NexusPage::activated() -{ -} - ConfirmationPage::ConfirmationPage(CreateInstanceDialog& dlg) : Page(dlg) @@ -1029,21 +1043,6 @@ void ConfirmationPage::activated() ui->creationLog->clear(); } -QString ConfirmationPage::toLocalizedString(CreateInstanceDialog::Types t) const -{ - switch (t) - { - case CreateInstanceDialog::Global: - return QObject::tr("Global"); - - case CreateInstanceDialog::Portable: - return QObject::tr("Portable"); - - default: - return QObject::tr("Instance type: %1").arg(QObject::tr("?")); - } -} - QString ConfirmationPage::makeReview() const { QStringList lines; diff --git a/src/createinstancedialogpages.h b/src/createinstancedialogpages.h index 099071a2..e2eaf0fb 100644 --- a/src/createinstancedialogpages.h +++ b/src/createinstancedialogpages.h @@ -15,14 +15,26 @@ class NexusConnectionUI; namespace cid { +// returns "%base_dir%/dir" +// QString makeDefaultPath(const std::wstring& dir); +// remembers the original text of the given label and, if it contains a %1, +// sets it in setText() +// class PlaceholderLabel { public: PlaceholderLabel(QLabel* label); + + // if the original label text contained a %1, replaces it by the arg and + // sets that as the new label text + // void setText(const QString& arg); + + // whether the label is visible + // void setVisible(bool b); private: @@ -31,25 +43,67 @@ private: }; +// one page in the wizard +// +// each page can implement one or more selected*() below; those are called +// by CreateInstanceDialog to gather data from all pages +// class Page { public: Page(CreateInstanceDialog& dlg); + // whether this page has been filled and is valid; used by the dialog to + // determine if it can move to the next page + // virtual bool ready() const; + + // called every time a page is shown in the screen + // virtual void activated(); + // overrides whether this page should be skipped; this is used by + // CreateInstanceDialog::setSinglePage() to disable all other pages + // void setSkip(bool b); + + // whether this page should be skipped + // bool skip() const; + // asks the dialog to update its navigation buttons, typically used when a + // page changes its ready state without moving to a different page + // void updateNavigation(); + + // asks the dialog to move to the next page; some pages will automatically + // advance once the user has made the proper selection + // void next(); + + // returns the instance type + // virtual CreateInstanceDialog::Types selectedInstanceType() const; + + // returns the game plugin + // virtual MOBase::IPluginGame* selectedGame() const; + + // returns the game directory + // virtual QString selectedGameLocation() const; + + // returns the game variant + // virtual QString selectedGameVariant(MOBase::IPluginGame* game) const; + + // returns the instance name + // virtual QString selectedInstanceName() const; + + // returns the various paths + // virtual CreateInstanceDialog::Paths selectedPaths() const; protected: @@ -58,10 +112,15 @@ protected: const PluginContainer& m_pc; bool m_skip; + + // implemented by derived classes, overridden by setSkip(true) + // virtual bool doSkip() const; }; +// introduction page, can be disabled by a global setting +// class IntroPage : public Page { public: @@ -72,15 +131,27 @@ protected: }; +// instance type page +// class TypePage : public Page { public: TypePage(CreateInstanceDialog& dlg); + // whether a type has been been selected + // bool ready() const override; + + // returns the selected type + // CreateInstanceDialog::Types selectedInstanceType() const override; + // selects a global instance + // void global(); + + // selects a portable instance + // void portable(); private: @@ -88,160 +159,425 @@ private: }; +// game plugin page, displays a list of command buttons for each game, along +// with a "browse" button for custom directories and filtering stuff +// +// the game list initially only shows plugins that report isInstalled(), and the +// user has two ways of specifying paths for games that were not found: +// +// 1) by clicking the "Browse..." button and selecting an arbitrary directory +// +// all plugins are checked until one returns true for looksValid(); if none +// of them do, this is an error +// +// 2) by checking the "Show all supported games" checkbox and clicking one +// of the games on the list +// +// if the selected plugin doesn't recognize the directory, the user is +// warned, but is allowed to continue; there's also some logic to try to +// find another plugin that can manage this directory and suggest it +// instead +// class GamePage : public Page { public: GamePage(CreateInstanceDialog& dlg); + // whether a game has been selected + // bool ready() const override; + + // returns the selected game + // MOBase::IPluginGame* selectedGame() const override; + + // returns the selected game directory QString selectedGameLocation() const override; + + // selects the given game and toggles its associated button; the game + // directory can be overridden + // + // pops up a directory selection dialog if `dir` is empty and the plugin + // hasn't detected the game + // void select(MOBase::IPluginGame* game, const QString& dir={}); + + // pops up a directory selection dialog and looks for a plugin to manage + // it + // void selectCustom(); + // pops up a warning dialog that the game at the given path is not supported + // by any plugin, includes a list of all game plugins in the details section + // of the dialog + // void warnUnrecognized(const QString& path); private: + // a single game, with its button and custom directory, if any + // struct Game { + // game plugin MOBase::IPluginGame* game = nullptr; + + // button on the ui QCommandLinkButton* button = nullptr; + + // game directory; set in ctor if the plugin has detected the game, or + // set later when the user selects a directory QString dir; + + // whether a directory has been set for this game, either auto detected + // or by the user bool installed = false; + Game(MOBase::IPluginGame* g); Game(const Game&) = delete; Game& operator=(const Game&) = delete; }; + // list of all game plugins, even if they're not installed; those are filtered + // from the ui if the checkbox isn't checked std::vector> m_games; + + // current selection Game* m_selection; + + // filter MOBase::FilterWidget m_filter; + + // returns a list of all the game plugins sorted with natsort + // std::vector sortedGamePlugins() const; - Game* findGame(MOBase::IPluginGame* game); + + // creates the m_games list + // void createGames(); + + // finds the game struct associated with the given game + // + Game* findGame(MOBase::IPluginGame* game); + + + // creates the ui for the given game button + // + void createGameButton(Game* g); + + // adds the given button to the ui + // + void addButton(QAbstractButton* b); + + // updates the given button on the ui, sets the text, icon, etc. + // void updateButton(Game* g); + + // called when a button has been clicked; selects the game or asks the user + // for directory, depending + // void selectButton(Game* g); + + // removes all buttons from the ui + // void clearButtons(); - void addButton(QAbstractButton* b); + + // creates the "Browse" button + // QCommandLinkButton* createCustomButton(); - void createGameButton(Game* g); + + + // clears the button list and adds all the buttons to it, depending on + // filtering and stuff + // void fillList(); - void onFilter(); + + // checks whether the given path looks valid to the given game plugin + // + // if the plugin doesn't like the path, allows the user to override and + // accept, but also attempts to find another plugin that wants it and + // propose that as an alternative, if there's one + // + // returns: + // - if the user selects the alternative plugin, returns that plugin + // instead; + // - if the path is bad but the user overrides, returns the given plugin + // - if the user cancels or if no plugins can manage the directory, returns + // null + // Game* checkInstallation(const QString& path, Game* g); - MOBase::IPluginGame* findAnotherGame(const QString& path); + + // tells the user that the path cannot be handled by any game plugin, returns + // true if the user decides to accept anyway + // bool confirmUnknown(const QString& path, MOBase::IPluginGame* game); + + // tells the user that the path can be handled by a different plugin than the + // selected one and allows them to either + // 1) use the alternative, guessedGame is returned; + // 2) use the selection anyway, selectedGame is returned; or + // 3) cancel, null is returned + // MOBase::IPluginGame* confirmOtherGame( const QString& path, MOBase::IPluginGame* selectedGame, MOBase::IPluginGame* guessedGame); }; +// game variants page; displays a list of command buttons for game variants, as +// reported by the game plugin +// +// this page is always skipped if the game plugin reports no variants +// class VariantsPage : public Page { public: VariantsPage(CreateInstanceDialog& dlg); + // whether a variant has been selected or the game plugin reports no variants + // bool ready() const override; + + // uses the game selected in the previous page to fill the list, this must be + // called every time because the user may go back in forth in the wizard + // void activated() override; + + // returns the selected variant, if any + // QString selectedGameVariant(MOBase::IPluginGame* game) const override; + // selects the given variant + // void select(const QString& variant); protected: + // returns true if the game has no variants + // bool doSkip() const override; private: + // game that was selected the last time this page was active MOBase::IPluginGame* m_previousGame; + + // buttons std::vector m_buttons; + + // selected variant QString m_selection; + + // fills the list with buttons void fillList(); }; +// instance name page; displays a textbox where the user can enter a name and +// does basic checks to make sure the name is valid and not a duplicate +// +// skipped for portable instances +// class NamePage : public Page { public: NamePage(CreateInstanceDialog& dlg); + // whether a valid name has been entered + // bool ready() const override; + + // uses the selected game to generate an instance name + // + // as long as the user hasn't modified the textbox, this will regenerate a new + // instance name every time the selected game changes + // void activated() override; + + // returns the instance name + // QString selectedInstanceName() const override; protected: + // returns true for portable instances + // bool doSkip() const override; private: - mutable PlaceholderLabel m_label, m_exists, m_invalid; + // game label, replaces %1 with the game name + PlaceholderLabel m_label; + + // "instance already exists" label, replaces %1 with instance name + PlaceholderLabel m_exists; + + // "instance name invalid" label, replaces %1 with instance name + PlaceholderLabel m_invalid; + + // whether the user has modified the text, prevents auto generation when the + // selected game changes bool m_modified; + + // whether the instance name is valid bool m_okay; + + // called when the user modifies the textbox, remember that it has changed and + // calls verify() + // void onChanged(); - void updateWarnings(); + + // check if the entered name is valid, sets m_okay and calls checkName() + // + void verify(); + + // updates the ui depending on whether the given instance name is valid in + // the given directory; returns false if the name is invalid + // bool checkName(QString parentDir, QString name); }; +// instance paths page; shows a single textbox for the base directory, or a +// series of textboxes for all the configurable paths if the advanced checkbox +// is checked +// class PathsPage : public Page { public: PathsPage(CreateInstanceDialog& dlg); + // whether all paths make sense + // bool ready() const override; + + // resets all the paths if the instance type or instance name have changed, + // the current values are kept as long as these don't change; also updates the + // game name in the ui + // void activated() override; + // returns the selected paths + // CreateInstanceDialog::Paths selectedPaths() const override; private: + // instance name the last time this page was active QString m_lastInstanceName; + + // instance type the last time this page was active CreateInstanceDialog::Types m_lastType; + + // help label, replaces %1 by the game name PlaceholderLabel m_label; - mutable PlaceholderLabel m_simpleExists, m_simpleInvalid; - mutable PlaceholderLabel m_advancedExists, m_advancedInvalid; + // path exists/is invalid labels for the simple page, replaces %1 with the + // path + PlaceholderLabel m_simpleExists, m_simpleInvalid; + + // path exists/is invalid labels for the advanced page, replaces %1 with the + // path + PlaceholderLabel m_advancedExists, m_advancedInvalid; + + // whether the paths are valid + bool m_okay; + + + // called when the user changes any textbox, checks the path and updates nav + // void onChanged(); - bool checkPaths() const; - bool checkAdvancedPath(const QString& path) const; + + // checks the simple or advanced paths, sets m_okay + // + void checkPaths(); + + // checks a simple path, forwards to checkPath() with the simple labels + // + bool checkSimplePath(const QString& path); + + // checks an advanced path, forwards to checkPath() with the advanced labels + // + bool checkAdvancedPath(const QString& path); + + // returns false if the path is invalid or already exists, sets the given + // labels accordingly + // + bool checkPath( + QString path, + PlaceholderLabel& existsLabel, PlaceholderLabel& invalidLabel); + + // replaces %base_dir% in the given path by whatever's in the base path + // textbox + // QString resolve(const QString& path) const; + + // called when the advanced checkbox is toggled, switches the active page + // and checks the paths + // void onAdvanced(); + + // called whenever the page becomes active + // + // this normally doesn't change the textboxes unless they're empty, but if the + // instance name or type have changed, `force` is true, which forces all paths + // to reset + // void setPaths(const QString& name, bool force); + + // sets the given textbox to the path if it's empty or if `force` is true + // void setIfEmpty(QLineEdit* e, const QString& path, bool force); - bool checkPath( - QString path, - PlaceholderLabel& existsLabel, PlaceholderLabel& invalidLabel) const; }; +// nexus connection page; this reuses the ui found in the settings dialog and +// is skipped if there's already an api key in the credentials manager +// class NexusPage : public Page { public: NexusPage(CreateInstanceDialog& dlg); ~NexusPage(); + // always returns true, this is an optional page + // bool ready() const override; - void activated() override; protected: + // returns true if the api key was already detected + // bool doSkip() const override; private: + // connection ui std::unique_ptr m_connectionUI; + + // set to true only if the api key was detected when opening the dialog, or + // going back and forth would skip the page after the process is completed, + // which would be unexpected bool m_skip; }; +// shows a text log of all the creation parameters +// class ConfirmationPage : public Page { public: ConfirmationPage(CreateInstanceDialog& dlg); + // recreates the log with the latest settings + // void activated() override; - QString toLocalizedString(CreateInstanceDialog::Types t) const; + // returns the text for the log + // QString makeReview() const; + +private: + // returns a log line with the given caption and path, something like + // " - caption: path" + // QString dirLine(const QString& caption, const QString& path) const; }; -- cgit v1.3.1 From 7614a147495d631c376aac2da5049b5ae4c9cc48 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Fri, 6 Nov 2020 16:40:01 -0500 Subject: documentation --- src/createinstancedialogpages.cpp | 101 +++++++++++++++++++++++++++++++------- 1 file changed, 82 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/createinstancedialogpages.cpp b/src/createinstancedialogpages.cpp index 8431abd1..0f39f6c1 100644 --- a/src/createinstancedialogpages.cpp +++ b/src/createinstancedialogpages.cpp @@ -15,6 +15,8 @@ namespace cid using namespace MOBase; using MOBase::TaskDialog; +// returns %base_dir%/dir +// QString makeDefaultPath(const std::wstring& dir) { return QDir::toNativeSeparators(PathSettings::makeDefaultPath( @@ -69,6 +71,7 @@ bool Page::ready() const bool Page::skip() const { + // setSkip() overrides this if it's true return m_skip || doSkip(); } @@ -149,6 +152,7 @@ bool IntroPage::doSkip() const TypePage::TypePage(CreateInstanceDialog& dlg) : Page(dlg), m_type(CreateInstanceDialog::NoType) { + // replace placeholders with actual paths ui->createGlobal->setDescription( ui->createGlobal->description() .arg(InstanceManager::singleton().globalInstancesRootPath())); @@ -157,6 +161,7 @@ TypePage::TypePage(CreateInstanceDialog& dlg) ui->createPortable->description() .arg(InstanceManager::singleton().portablePath())); + // disable portable button if it already exists if (InstanceManager::singleton().portableInstanceExists()) { ui->createPortable->setEnabled(false); ui->portableExistsLabel->setVisible(true); @@ -254,34 +259,47 @@ void GamePage::select(IPluginGame* game, const QString& dir) if (checked) { if (!checked->installed) { if (dir.isEmpty()) { + // the selected game has no installation directory and none was given, + // ask the user + const auto path = QFileDialog::getExistingDirectory( &m_dlg, QObject::tr("Find game installation")); if (path.isEmpty()) { + // cancelled checked = nullptr; } else { + // check whether a plugin supports the given directory; this can + // return the same plugin, a different one, or null checked = checkInstallation(path, checked); if (checked) { - // remember this path + // plugin was found, remember this path checked->dir = path; checked->installed = true; } } } else { + // the selected game didn't detect anything, but a directory was given, + // so use that checked->dir = dir; checked->installed = true; } } } - m_selection = checked; + // select this plugin, if any + m_selection = checked; selectButton(checked); + + // update the button associated with it in case the paths have changed updateButton(checked); + updateNavigation(); if (checked) { + // automatically move to the next page when a game is selected next(); } } @@ -297,22 +315,33 @@ void GamePage::selectCustom() return; } + // try to find a plugin that likes this directory for (auto& g : m_games) { if (g->game->looksValid(path)) { + // found one g->dir = path; g->installed = true; + + // select it select(g->game); + + // update the button because the path has changed updateButton(g.get()); + return; } } + // warning to the user warnUnrecognized(path); + + // reselect the previous button selectButton(m_selection); } void GamePage::warnUnrecognized(const QString& path) { + // put the list of supported games in the details textbox QString supportedGames; for (auto* game : sortedGamePlugins()) { supportedGames += game->gameName() + "\n"; @@ -337,10 +366,12 @@ std::vector GamePage::sortedGamePlugins() const { std::vector v; + // all game plugins for (auto* game : m_pc.plugins()) { v.push_back(game); } + // natsort std::sort(v.begin(), v.end(), [](auto* a, auto* b) { return (naturalCompare(a->gameName(), b->gameName()) < 0); }); @@ -460,6 +491,11 @@ void GamePage::clearButtons() ly->addStretch(); ui->games->setUpdatesEnabled(true); + + for (auto& g : m_games) { + // all buttons have been deleted + g->button = nullptr; + } } QCommandLinkButton* GamePage::createCustomButton() @@ -484,14 +520,13 @@ void GamePage::fillList() clearButtons(); for (auto& g : m_games) { - g->button = nullptr; - if (!showAll && !g->installed) { // not installed continue; } if (!m_filter.matches(g->game->gameName())) { + // filtered out continue; } @@ -499,6 +534,7 @@ void GamePage::fillList() addButton(g->button); } + // browse button addButton(createCustomButton()); } @@ -525,6 +561,7 @@ GamePage::Game* GamePage::checkInstallation(const QString& path, Game* g) } if (otherGame) { + // an alternative was found, ask the user about it auto* confirmedGame = confirmOtherGame(path, g->game, otherGame); if (!confirmedGame) { @@ -626,6 +663,9 @@ VariantsPage::VariantsPage(CreateInstanceDialog& dlg) bool VariantsPage::ready() const { + // note that this isn't called when doSkip() is true, which happens when + // the game has no variants + return !m_selection.isEmpty(); } @@ -637,8 +677,7 @@ bool VariantsPage::doSkip() const return true; } - const auto variants = g->gameVariants(); - return (variants.size() < 2); + return (g->gameVariants().size() < 2); } void VariantsPage::activated() @@ -646,6 +685,7 @@ void VariantsPage::activated() auto* g = m_dlg.rawCreationInfo().game; if (m_previousGame != g) { + // recreate the list, the game has changed m_previousGame = g; m_selection = ""; fillList(); @@ -654,9 +694,11 @@ void VariantsPage::activated() void VariantsPage::select(const QString& variant) { + m_selection = variant; + + // find the button, set it checked for (auto* b : m_buttons) { if (b->text() == variant) { - m_selection = variant; b->setChecked(true); } else { b->setChecked(false); @@ -666,6 +708,7 @@ void VariantsPage::select(const QString& variant) updateNavigation(); if (!m_selection.isEmpty()) { + // automatically move to the next page when a variant is selected next(); } } @@ -676,8 +719,7 @@ QString VariantsPage::selectedGameVariant(MOBase::IPluginGame* game) const return {}; } - const auto variants = game->gameVariants(); - if (variants.size() < 2) { + if (game->gameVariants().size() < 2) { return {}; } else { return m_selection; @@ -695,8 +737,8 @@ void VariantsPage::fillList() return; } - const auto variants = g->gameVariants(); - for (auto& v : variants) { + // for each variant, create a checkable button and add it + for (auto& v : g->gameVariants()) { auto* b = new QCommandLinkButton(v); b->setCheckable(true); @@ -721,11 +763,13 @@ NamePage::NamePage(CreateInstanceDialog& dlg) : bool NamePage::ready() const { + // checked when textboxes change or when the page is activated return m_okay; } bool NamePage::doSkip() const { + // portable instances have no name return (m_dlg.rawCreationInfo().type == CreateInstanceDialog::Portable); } @@ -739,6 +783,8 @@ void NamePage::activated() m_label.setText(g->gameName()); + // generate a name if the user hasn't changed the text in case the game + // changed, or if it's empty if (!m_modified || ui->instanceName->text().isEmpty()) { const auto n = InstanceManager::singleton().makeUniqueName(g->gameName()); ui->instanceName->setText(n); @@ -833,6 +879,8 @@ PathsPage::PathsPage(CreateInstanceDialog& dlg) : bool PathsPage::ready() const { + // set when the page is activated, textboxes are changed or the advanced + // checkbox is toggled return m_okay; } @@ -841,10 +889,15 @@ void PathsPage::activated() const auto name = m_dlg.rawCreationInfo().instanceName; const auto type = m_dlg.rawCreationInfo().type; + // if the instance name or type have changed, all the paths must be + // regenerated const bool changed = (m_lastInstanceName != name) || (m_lastType != type); + + // generating and paths setPaths(name, changed); checkPaths(); + updateNavigation(); m_label.setText(m_dlg.rawCreationInfo().game->gameName()); @@ -878,6 +931,7 @@ void PathsPage::onChanged() void PathsPage::checkPaths() { if (ui->advancedPathOptions->isChecked()) { + // checking advanced paths m_okay = checkAdvancedPath(ui->base->text()) && checkAdvancedPath(resolve(ui->downloads->text())) && @@ -885,6 +939,7 @@ void PathsPage::checkPaths() checkAdvancedPath(resolve(ui->profiles->text())) && checkAdvancedPath(resolve(ui->overwrite->text())); } else { + // checking simple path m_okay = checkSimplePath(ui->location->text()); } @@ -907,6 +962,9 @@ QString PathsPage::resolve(const QString& path) const void PathsPage::onAdvanced() { + // the base/location textboxes are different widgets but they represent the + // same base path value, so they're synced between pages + if (ui->advancedPathOptions->isChecked()) { ui->base->setText(ui->location->text()); ui->pathPages->setCurrentIndex(1); @@ -920,19 +978,21 @@ void PathsPage::onAdvanced() void PathsPage::setPaths(const QString& name, bool force) { - QString path; + QString basePath; if (m_dlg.rawCreationInfo().type == CreateInstanceDialog::Portable) { - path = InstanceManager::singleton().portablePath(); + basePath = InstanceManager::singleton().portablePath(); } else { const auto root = InstanceManager::singleton().globalInstancesRootPath(); - path = root + "/" + name; + basePath = root + "/" + name; } - path = QDir::toNativeSeparators(QDir::cleanPath(path)); + basePath = QDir::toNativeSeparators(QDir::cleanPath(basePath)); - setIfEmpty(ui->location, path, force); - setIfEmpty(ui->base, path, force); + // all paths are set regardless of advanced checkbox + + setIfEmpty(ui->location, basePath, force); + setIfEmpty(ui->base, basePath, force); setIfEmpty(ui->downloads, makeDefaultPath(AppConfig::downloadPath()), force); setIfEmpty(ui->mods, makeDefaultPath(AppConfig::modsPath()), force); @@ -951,6 +1011,8 @@ bool PathsPage::checkPath( QString path, PlaceholderLabel& existsLabel, PlaceholderLabel& invalidLabel) { + auto& m = InstanceManager::singleton(); + bool exists = false; bool invalid = false; bool empty = false; @@ -962,11 +1024,11 @@ bool PathsPage::checkPath( } else { const QDir d(path); - if (InstanceManager::singleton().validInstanceName(d.dirName())) { + if (m.validInstanceName(d.dirName())) { if (m_dlg.rawCreationInfo().type == CreateInstanceDialog::Portable) { // the default data path for a portable instance is the application // directory, so it's not an error if it exists - if (QDir(path) != InstanceManager::singleton().portablePath()) { + if (QDir(path) != m.portablePath()) { exists = QDir(path).exists(); } } else { @@ -1023,6 +1085,7 @@ NexusPage::~NexusPage() = default; bool NexusPage::ready() const { + // this page is optional return true; } -- cgit v1.3.1 From 6ca1ebc349528d5f6fc9194c590a17ecbe9fa444 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Fri, 6 Nov 2020 17:28:01 -0500 Subject: comments interpret a first argument starting with -- as an error instead of an exe name/binary --- src/commandline.cpp | 55 +++++++++++++++++-------- src/commandline.h | 114 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 150 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/commandline.cpp b/src/commandline.cpp index 3f8a6b1a..ff1be764 100644 --- a/src/commandline.cpp +++ b/src/commandline.cpp @@ -13,6 +13,8 @@ std::string pad_right(std::string s, std::size_t n, char c=' ') return s; } +// formats the list of pairs in two columns +// std::string table( const std::vector>& v, std::size_t indent, std::size_t spacing) @@ -60,6 +62,9 @@ std::optional CommandLine::run(const std::wstring& line) args.erase(args.begin()); } + // parsing the first part of the command line, including global options and + // command name, but not the rest, which will be collected below + auto parsed = po::wcommand_line_parser(args) .options(m_allOptions) .positional(m_positional) @@ -69,20 +74,29 @@ std::optional CommandLine::run(const std::wstring& line) po::store(parsed, m_vm); po::notify(m_vm); + // collect options past the command name auto opts = po::collect_unrecognized( parsed.options, po::include_positional); if (m_vm.count("command")) { + // there's a word as the first argument; this may be a command name or + // an old style exe name/binary + const auto commandName = m_vm["command"].as(); + // look for the command by name first for (auto&& c : m_commands) { if (c->name() == commandName) { + // this is a command + // remove the command name itself opts.erase(opts.begin()); try { + // parse the the remainder of the command line according to the + // command's options po::wcommand_line_parser parser(opts); auto co = c->allOptions(); @@ -106,6 +120,7 @@ std::optional CommandLine::run(const std::wstring& line) return 0; } + // run the command return c->run(line, m_vm, opts); } catch(po::error& e) @@ -122,6 +137,11 @@ std::optional CommandLine::run(const std::wstring& line) } } + + // the first word on the command line is not a valid command, try the other + // stuff; this is handled in main.cpp + + // look for help if (m_vm.count("help")) { env::Console console; std::cout << usage() << "\n"; @@ -130,12 +150,25 @@ std::optional CommandLine::run(const std::wstring& line) if (!opts.empty()) { const auto qs = QString::fromStdWString(opts[0]); + + if (qs.startsWith("--")) { + // assume that for something like `ModOrganizer.exe --bleh`, it's just + // a bad option instead of an executable that starts with "--" + env::Console console; + std::cerr << "\nUnrecognized option " << qs.toStdString() << "\n"; + + return 1; + } + + // try as an moshorcut:// m_shortcut = qs; if (!m_shortcut.isValid()) { + // not a shortcut, try a link if (isNxmLink(qs)) { m_nxmLink = qs; } else { + // assume an executable name/binary m_executable = qs; } } @@ -182,6 +215,7 @@ void CommandLine::createOptions() ("command", po::value(), "command") ("subargs", po::value >(), "args"); + // one command name, followed by any arguments for that command m_positional .add("command", 1) .add("subargs", -1); @@ -210,6 +244,7 @@ std::string CommandLine::usage(const Command* c) const << "\n" << "Commands:\n"; + // name and description for all commands std::vector> v; for (auto&& c : m_commands) { v.push_back({c->name(), c->description()}); @@ -224,6 +259,7 @@ std::string CommandLine::usage(const Command* c) const << "Global options:\n" << m_visibleOptions << "\n"; + // show the more text unless this is usage for a specific command if (!c) { oss << "\n" << more() << "\n"; } @@ -247,6 +283,8 @@ std::optional CommandLine::profile() const std::optional CommandLine::instance() const { + // note that moshortcut:// overrides -i + if (m_shortcut.isValid() && m_shortcut.hasInstance()) { return m_shortcut.instance(); } else if (m_vm.count("instance")) { @@ -369,21 +407,6 @@ po::positional_options_description Command::getPositional() const } -std::string Command::usage() const -{ - std::ostringstream oss; - - oss - << "\n" - << "Usage:\n" - << " ModOrganizer.exe [options] [[command] [command-options]]\n" - << "\n" - << "Options:\n" - << visibleOptions() << "\n"; - - return oss.str(); -} - std::optional Command::run( const std::wstring& originalLine, po::variables_map vm, @@ -569,8 +592,6 @@ std::optional ExeCommand::doRun() const auto args = vm()["arguments"].as(); const auto cwd = vm()["cwd"].as(); - - return 0; } diff --git a/src/commandline.h b/src/commandline.h index 72018ba3..478ee8ea 100644 --- a/src/commandline.h +++ b/src/commandline.h @@ -9,44 +9,96 @@ namespace cl namespace po = boost::program_options; - +// base class for all commands +// class Command { public: virtual ~Command() = default; + // command name, used on the command line to invoke it; this is meta().name + // std::string name() const; + + // command short description, shown in general help; this is + // meta().description + // std::string description() const; + + // usage line, puts together the name and whatever getUsageLine() returns + // std::string usageLine() const; + + // returns all options for this command, including hidden ones; used to parse + // the command line + // po::options_description allOptions() const; + + // returns visible options, used to display usage + // po::options_description visibleOptions() const; + + // returns positional arguments + // po::positional_options_description positional() const; - std::string usage() const; + // whether the command allows for unregistered options; this is only used for + // the old `launch` command and shouldn't be used anywhere else + // virtual bool allow_unregistered() const; + // runs this command, eventually calls doRun() + // std::optional run( const std::wstring& originalLine, po::variables_map vm, std::vector untouched); protected: + // meta information about this command, returned by derived classes + // struct Meta { std::string name, description; }; + // returns the usage line for this command, not including the name + // virtual std::string getUsageLine() const; + + // returns visible options specific to this command + // virtual po::options_description getVisibleOptions() const; + + // returns hidden options specific to this command + // virtual po::options_description getInternalOptions() const; + + // returns positional arguments specific to this command + // virtual po::positional_options_description getPositional() const; + + // meta + // virtual Meta meta() const = 0; + + // runs the command + // virtual std::optional doRun() = 0; + + // returns the original command line + // const std::wstring& originalCmd() const; + + // variables + // const po::variables_map& vm() const; + + // returns unparsed options, only used by launch + // const std::vector& untouched() const; private: @@ -56,6 +108,8 @@ private: }; +// generates a crash dump for another MO process +// class CrashDumpCommand : public Command { protected: @@ -93,6 +147,8 @@ protected: }; +// runs a configured executable +// class ExeCommand : public Command { protected: @@ -105,6 +161,8 @@ protected: }; +// runs an arbitrary executable +// class RunCommand : public Command { protected: @@ -114,24 +172,76 @@ protected: }; +// parses the command line and runs any given command +// +// the command line used to support a few commands but with no real conventions; +// those are mostly preserved for backwards compatibility, but deprecated: +// +// - moshortcut:// for desktop/taskbar shortcuts, may contain an instance name +// and a configured executable or arbitrary binary (still used in MO for +// shortcuts) +// +// - nxm:// links +// +// - the name of a configured executable or path to binary, followed by +// arbitrary parameters, forwarded to the program +// +// any command added CommandLine will unfortunately break any executable with +// the same name: `ModOrganizer.exe run` used to launch a program named "run" +// but will now execute the command "run" +// +// if moshortcut:// is detected and has an instance, it will override -i if both +// are given +// class CommandLine { public: CommandLine(); + // parses the given command line and executes the appropriate command, if + // any + // + // returns an empty optional if execution should continue, or a return code + // if MO must quit + // std::optional run(const std::wstring& line); + + // clears parsed options, used when MO is "restarted" so the options aren't + // processed again + // void clear(); + // global usage string plus usage for the given command, if any + // std::string usage(const Command* c=nullptr) const; + + // whether --multiple was given + // bool multiple() const; + + // profile override (-p) + // std::optional profile() const; + + // instance override (-i) + // std::optional instance() const; + // returns the data parsed from an moshortcut:// option, if any + // const MOShortcut& shortcut() const; + + // returns the nxm:// link, if any + // std::optional nxmLink() const; + + // returns the executable/binary, if any std::optional executable() const; + // returns the list of arguments, excluding moshortcut or executable name; + // deprecated, only use with executable() + // const QStringList& untouched() const; private: -- cgit v1.3.1 From 90ea45328d72f9664ace3cfbdce700dbe7a1d016 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 7 Nov 2020 16:16:47 -0500 Subject: moved splash stuff to MOSplash comments --- src/commandline.cpp | 3 +++ src/main.cpp | 71 ++----------------------------------------------- src/moapplication.cpp | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/moapplication.h | 19 ++++++++++++++ src/settings.h | 18 +++++++++++++ 5 files changed, 115 insertions(+), 69 deletions(-) (limited to 'src') diff --git a/src/commandline.cpp b/src/commandline.cpp index ff1be764..dc8cf53f 100644 --- a/src/commandline.cpp +++ b/src/commandline.cpp @@ -592,6 +592,8 @@ std::optional ExeCommand::doRun() const auto args = vm()["arguments"].as(); const auto cwd = vm()["cwd"].as(); + std::cout << "not implemented\n"; + return 0; } @@ -608,6 +610,7 @@ Command::Meta RunCommand::meta() const std::optional RunCommand::doRun() { + std::cout << "not implemented\n"; return {}; } diff --git a/src/main.cpp b/src/main.cpp index a2653685..9d6ed7b0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -160,69 +160,6 @@ void addDllsToPath() env::prependToPath(dllsPath); } -QString getSplashPath( - const Settings& settings, const QString& dataPath, - const MOBase::IPluginGame* game) -{ - if (!settings.useSplash()) { - return {}; - } - - // try splash from instance directory - const QString splashPath = dataPath + "/splash.png"; - if (QFile::exists(dataPath + "/splash.png")) { - QImage image(splashPath); - if (!image.isNull()) { - return splashPath; - } - } - - // try splash from plugin - QString pluginSplash = QString(":/%1/splash").arg(game->gameShortName()); - if (QFile::exists(pluginSplash)) { - QImage image(pluginSplash); - if (!image.isNull()) { - image.save(splashPath); - return pluginSplash; - } - } - - // try default splash from resource - QString defaultSplash = ":/MO/gui/splash"; - if (QFile::exists(defaultSplash)) { - QImage image(defaultSplash); - if (!image.isNull()) { - return defaultSplash; - } - } - - return splashPath; -} - -std::unique_ptr createSplash( - const Settings& settings, const QString& dataPath, - const MOBase::IPluginGame* game) -{ - const auto splashPath = getSplashPath(settings, dataPath, game); - if (splashPath.isEmpty()) { - return {}; - } - - QPixmap image(splashPath); - if (image.isNull()) { - log::error("failed to load splash from {}", splashPath); - return {}; - } - - auto splash = std::make_unique(image); - settings.geometry().centerOnMainWindowMonitor(splash.get()); - - splash->show(); - splash->activateWindow(); - - return splash; -} - std::optional handleCommandLine( const cl::CommandLine& cl, OrganizerCore& organizer) { @@ -554,7 +491,7 @@ int runApplication( return *r; } - auto splash = createSplash(settings, dataPath, currentInstance.gamePlugin()); + MOSplash splash(settings, dataPath, currentInstance.gamePlugin()); QString apiKey; if (GlobalSettings::nexusApiKey(apiKey)) { @@ -589,11 +526,7 @@ int runApplication( mainWindow.show(); mainWindow.activateWindow(); - if (splash) { - // don't pass mainwindow as it just waits half a second for it - // instead of proceding - splash->finish(nullptr); - } + splash.close(); tt.stop(); diff --git a/src/moapplication.cpp b/src/moapplication.cpp index 290666af..659b5a12 100644 --- a/src/moapplication.cpp +++ b/src/moapplication.cpp @@ -18,6 +18,8 @@ along with Mod Organizer. If not, see . */ #include "moapplication.h" +#include "settings.h" +#include #include #include #include @@ -147,3 +149,74 @@ void MOApplication::updateStyle(const QString& fileName) } } } + + +MOSplash::MOSplash( + const Settings& settings, const QString& dataPath, + const MOBase::IPluginGame* game) +{ + const auto splashPath = getSplashPath(settings, dataPath, game); + if (splashPath.isEmpty()) { + return; + } + + QPixmap image(splashPath); + if (image.isNull()) { + log::error("failed to load splash from {}", splashPath); + return; + } + + ss_.reset(new QSplashScreen(image)); + settings.geometry().centerOnMainWindowMonitor(ss_.get()); + + ss_->show(); + ss_->activateWindow(); +} + +void MOSplash::close() +{ + if (ss_) { + // don't pass mainwindow as it just waits half a second for it + // instead of proceding + ss_->finish(nullptr); + } +} + +QString MOSplash::getSplashPath( + const Settings& settings, const QString& dataPath, + const MOBase::IPluginGame* game) const +{ + if (!settings.useSplash()) { + return {}; + } + + // try splash from instance directory + const QString splashPath = dataPath + "/splash.png"; + if (QFile::exists(dataPath + "/splash.png")) { + QImage image(splashPath); + if (!image.isNull()) { + return splashPath; + } + } + + // try splash from plugin + QString pluginSplash = QString(":/%1/splash").arg(game->gameShortName()); + if (QFile::exists(pluginSplash)) { + QImage image(pluginSplash); + if (!image.isNull()) { + image.save(splashPath); + return pluginSplash; + } + } + + // try default splash from resource + QString defaultSplash = ":/MO/gui/splash"; + if (QFile::exists(defaultSplash)) { + QImage image(defaultSplash); + if (!image.isNull()) { + return defaultSplash; + } + } + + return splashPath; +} diff --git a/src/moapplication.h b/src/moapplication.h index c67c4622..0ee04492 100644 --- a/src/moapplication.h +++ b/src/moapplication.h @@ -23,6 +23,8 @@ along with Mod Organizer. If not, see . #include #include +class Settings; +namespace MOBase { class IPluginGame; } class MOApplication : public QApplication { @@ -46,4 +48,21 @@ private: }; +class MOSplash +{ +public: + MOSplash( + const Settings& settings, const QString& dataPath, + const MOBase::IPluginGame* game); + + void close(); + +private: + std::unique_ptr ss_; + + QString getSplashPath( + const Settings& settings, const QString& dataPath, + const MOBase::IPluginGame* game) const; +}; + #endif // MOAPPLICATION_H diff --git a/src/settings.h b/src/settings.h index c8325ba2..fc7789f0 100644 --- a/src/settings.h +++ b/src/settings.h @@ -195,6 +195,10 @@ private: class WidgetSettings { public: + // globalInstance is forwarded from the Settings constructor; WidgetSettings + // has the callbacks used by QuestionBoxMemory and those are global, so they + // should only be set by the global instance + // WidgetSettings(QSettings& s, bool globalInstance); // selected index for a combobox @@ -671,6 +675,20 @@ class Settings : public QObject Q_OBJECT; public: + // there is one Settings global object for MO when an instance is loaded, but + // other Settings objects are required in several places, such as in the + // Instance class, the instance dialogs, etc. + // + // any Settings object created with globalInstance==false won't set the + // singleton + // + // only WidgetSettings need to know whether it created from a globalInstance, + // see its constructor + // + // @param path path to an ini file + // @param globalInsance whether this is the global instance; creates the + // singleton and asserts if it already exists + // Settings(const QString& path, bool globalInstance=false); ~Settings(); -- cgit v1.3.1 From a8187e7dc47cd344fd309a2be3675e4687f95aaa Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 7 Nov 2020 16:31:35 -0500 Subject: moved dlls stuff to MOApplication --- src/main.cpp | 40 +--------------------------------------- src/moapplication.cpp | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 39 deletions(-) (limited to 'src') diff --git a/src/main.cpp b/src/main.cpp index 9d6ed7b0..a8ea2601 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -122,44 +122,6 @@ void setExceptionHandlers() g_prevTerminateHandler = std::set_terminate(onTerminate); } -// This adds the `dlls` directory to the path so the dlls can be found. How -// MO is able to find dlls in there is a bit convoluted: -// -// Dependencies on DLLs can be baked into an executable by passing a -// `manifestdependency` option to the linker. This can be done on the command -// line or with a pragma. Typically, the dependency will not be a hardcoded -// filename, but an assembly name, such as Microsoft.Windows.Common-Controls. -// -// When Windows loads the exe, it will look for this assembly in a variety of -// places, such as in the WinSxS folder, but also in the program's folder. It -// will look for `assemblyname.dll` or `assemblyname/assemblyname.dll` and try -// to load that. -// -// If these files don't exist, then the loader gets creative and looks for -// `assemblyname.manifest` and `assemblyname/assemblyname.manifest`. A manifest -// file is just an XML file that can contain a list of DLLs to load for this -// assembly. -// -// In MO's case, there's a `pragma` at the beginning of this file which adds -// `dlls` as an "assembly" dependency. This is a bit of a hack to just force -// the loader to eventually find `dlls/dlls.manifest`, which contains the list -// of all the DLLs MO requires to load. -// -// This file was handwritten in `modorganizer/src/dlls.manifest.qt5` and -// is copied and renamed in CMakeLists.txt into `bin/dlls/dlls.manifest`. Note -// that the useless and incorrect .qt5 extension is removed. -// -void addDllsToPath() -{ - const auto dllsPath = QDir::toNativeSeparators( - QCoreApplication::applicationDirPath() + "/dlls"); - - QCoreApplication::setLibraryPaths( - QStringList(dllsPath) + QCoreApplication::libraryPaths()); - - env::prependToPath(dllsPath); -} - std::optional handleCommandLine( const cl::CommandLine& cl, OrganizerCore& organizer) { @@ -646,8 +608,8 @@ int main(int argc, char *argv[]) SetThisThreadName("main"); initLogging(); + auto application = MOApplication::create(argc, argv); - addDllsToPath(); SingleInstance instance(cl.multiple()); if (instance.ephemeral()) { diff --git a/src/moapplication.cpp b/src/moapplication.cpp index 659b5a12..f514050f 100644 --- a/src/moapplication.cpp +++ b/src/moapplication.cpp @@ -19,6 +19,7 @@ along with Mod Organizer. If not, see . #include "moapplication.h" #include "settings.h" +#include "env.h" #include #include #include @@ -32,6 +33,12 @@ along with Mod Organizer. If not, see . #include #include +// see addDllsToPath() below +#pragma comment(linker, "/manifestDependency:\"" \ + "name='dlls' " \ + "processorArchitecture='x86' " \ + "version='1.0.0.0' " \ + "type='win32' \"") using namespace MOBase; @@ -73,6 +80,45 @@ public: }; +// This adds the `dlls` directory to the path so the dlls can be found. How +// MO is able to find dlls in there is a bit convoluted: +// +// Dependencies on DLLs can be baked into an executable by passing a +// `manifestdependency` option to the linker. This can be done on the command +// line or with a pragma. Typically, the dependency will not be a hardcoded +// filename, but an assembly name, such as Microsoft.Windows.Common-Controls. +// +// When Windows loads the exe, it will look for this assembly in a variety of +// places, such as in the WinSxS folder, but also in the program's folder. It +// will look for `assemblyname.dll` or `assemblyname/assemblyname.dll` and try +// to load that. +// +// If these files don't exist, then the loader gets creative and looks for +// `assemblyname.manifest` and `assemblyname/assemblyname.manifest`. A manifest +// file is just an XML file that can contain a list of DLLs to load for this +// assembly. +// +// In MO's case, there's a `pragma` at the beginning of this file which adds +// `dlls` as an "assembly" dependency. This is a bit of a hack to just force +// the loader to eventually find `dlls/dlls.manifest`, which contains the list +// of all the DLLs MO requires to load. +// +// This file was handwritten in `modorganizer/src/dlls.manifest.qt5` and +// is copied and renamed in CMakeLists.txt into `bin/dlls/dlls.manifest`. Note +// that the useless and incorrect .qt5 extension is removed. +// +void addDllsToPath() +{ + const auto dllsPath = QDir::toNativeSeparators( + QCoreApplication::applicationDirPath() + "/dlls"); + + QCoreApplication::setLibraryPaths( + QStringList(dllsPath) + QCoreApplication::libraryPaths()); + + env::prependToPath(dllsPath); +} + + MOApplication::MOApplication(int& argc, char** argv) : QApplication(argc, argv) { @@ -83,11 +129,13 @@ MOApplication::MOApplication(int& argc, char** argv) m_DefaultStyle = style()->objectName(); setStyle(new ProxyStyle(style())); + addDllsToPath(); } MOApplication MOApplication::create(int& argc, char** argv) { QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + return MOApplication(argc, argv); } -- cgit v1.3.1 From d288587b002b19c54dc08d08ae267d301aa2ac81 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 7 Nov 2020 16:51:55 -0500 Subject: moved instance setup/selection to instancemanager.cpp comments --- src/instancemanager.cpp | 157 +++++++++++++++++++++++++++++++++++++- src/instancemanager.h | 37 +++++++++ src/main.cpp | 181 +------------------------------------------- src/shared/error_report.cpp | 10 +++ src/shared/error_report.h | 8 ++ 5 files changed, 214 insertions(+), 179 deletions(-) (limited to 'src') diff --git a/src/instancemanager.cpp b/src/instancemanager.cpp index fbafc6e8..b68d715b 100644 --- a/src/instancemanager.cpp +++ b/src/instancemanager.cpp @@ -21,9 +21,14 @@ along with Mod Organizer. If not, see . #include "instancemanager.h" #include "selectiondialog.h" #include "settings.h" -#include "shared/appconfig.h" #include "plugincontainer.h" +#include "nexusinterface.h" +#include "createinstancedialog.h" +#include "instancemanagerdialog.h" +#include "createinstancedialogpages.h" +#include "shared/appconfig.h" #include "shared/util.h" +#include "shared/error_report.h" #include #include #include @@ -751,3 +756,153 @@ bool InstanceManager::validInstanceName(const QString& instanceName) const return (instanceName == sanitizeInstanceName(instanceName)); } + + + +std::optional selectInstance() +{ + auto& m = InstanceManager::singleton(); + + NexusInterface ni(nullptr); + PluginContainer pc(nullptr); + pc.loadPlugins(); + + if (!m.hasAnyInstances()) { + // no instances configured + CreateInstanceDialog dlg(pc, nullptr); + if (dlg.exec() != QDialog::Accepted) { + return {}; + } + + return m.currentInstance(); + } + + + InstanceManagerDialog dlg(pc); + dlg.setRestartOnSelect(false); + + dlg.show(); + dlg.activateWindow(); + dlg.raise(); + + if (dlg.exec() != QDialog::Accepted) { + return {}; + } + + return m.currentInstance(); +} + +SetupInstanceResults setupInstance(Instance& instance, PluginContainer& pc) +{ + const auto setupResult = instance.setup(pc); + + switch (setupResult) + { + case Instance::SetupResults::Ok: + { + return SetupInstanceResults::Ok; + } + + case Instance::SetupResults::BadIni: + { + MOShared::criticalOnTop( + QObject::tr("Cannot open instance '%1', failed to read INI file %2.") + .arg(instance.name()).arg(instance.iniPath())); + + return SetupInstanceResults::SelectAnother; + } + + case Instance::SetupResults::IniMissingGame: + { + MOShared::criticalOnTop( + QObject::tr( + "Cannot open instance '%1', the managed game was not found in the INI " + "file %2. Select the game managed by this instance.") + .arg(instance.name()).arg(instance.iniPath())); + + CreateInstanceDialog dlg(pc, nullptr); + dlg.setSinglePage(instance.name()); + + dlg.show(); + dlg.activateWindow(); + dlg.raise(); + + if (dlg.exec() != QDialog::Accepted) { + return SetupInstanceResults::Exit; + } + + instance.setGame( + dlg.creationInfo().game->gameName(), + dlg.creationInfo().gameLocation); + + return SetupInstanceResults::TryAgain; + } + + case Instance::SetupResults::PluginGone: + { + MOShared::criticalOnTop( + QObject::tr( + "Cannot open instance '%1', the game plugin '%2' doesn't exist. It " + "may have been deleted by an antivirus. Select another instance.") + .arg(instance.name()).arg(instance.gameName())); + + return SetupInstanceResults::SelectAnother; + } + + case Instance::SetupResults::GameGone: + { + MOShared::criticalOnTop( + QObject::tr( + "Cannot open instance '%1', the game directory '%2' doesn't exist or " + "the game plugin '%3' doesn't recognize it. Select the game managed " + "by this instance.") + .arg(instance.name()) + .arg(instance.gameDirectory()) + .arg(instance.gameName())); + + CreateInstanceDialog dlg(pc, nullptr); + dlg.setSinglePage(instance.name()); + + dlg.show(); + dlg.activateWindow(); + dlg.raise(); + + if (dlg.exec() != QDialog::Accepted) { + return SetupInstanceResults::Exit; + } + + instance.setGame( + dlg.creationInfo().game->gameName(), + dlg.creationInfo().gameLocation); + + return SetupInstanceResults::TryAgain; + } + + case Instance::SetupResults::MissingVariant: + { + CreateInstanceDialog dlg(pc, nullptr); + + dlg.getPage()->select( + instance.gamePlugin(), instance.gameDirectory()); + + dlg.setSinglePage(instance.name()); + + dlg.show(); + dlg.activateWindow(); + dlg.raise(); + + if (dlg.exec() != QDialog::Accepted) { + return SetupInstanceResults::Exit; + } + + instance.setVariant(dlg.creationInfo().gameVariant); + + return SetupInstanceResults::TryAgain; + } + + default: + { + return SetupInstanceResults::Exit; + } + } +} diff --git a/src/instancemanager.h b/src/instancemanager.h index 161df739..d75a46f4 100644 --- a/src/instancemanager.h +++ b/src/instancemanager.h @@ -332,4 +332,41 @@ private: std::optional m_overrideProfileName; }; + +// see setupInstance() +// +enum class SetupInstanceResults +{ + Ok, + TryAgain, + SelectAnother, + Exit +}; + +// if there are no instances configured, global or portable, shows the +// create instance dialog and returns the new instance or empty if the user +// cancelled +// +// if there is at least one instance available, unconditionally show the +// instance manager dialog and returns the selected instnace or empty if the +// user cancelled +// +std::optional selectInstance(); + +// calls instance.setup() tries to handle problems by itself: +// +// - if the ini is missing some information, will show dialogs and ask the user +// to fill in what's required (such as the game directory, variant, etc.); +// if successful, returns TryAgain and setupInstance() can be called again +// with the same instance +// +// - if the instance cannot be used (no game plugin found for it, ini can't +// be read, etc.), returns SelectAnother +// +// - if the user cancels at any point, returns Exit +// +// - if the instance has been set up correctly, returns Ok +// +SetupInstanceResults setupInstance(Instance& instance, PluginContainer& pc); + #endif // MODORGANIZER_INSTANCEMANAGER_INCLUDED diff --git a/src/main.cpp b/src/main.cpp index a8ea2601..fdd09872 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -34,20 +34,13 @@ along with Mod Organizer. If not, see . #include "commandline.h" #include "shared/util.h" #include "shared/appconfig.h" - +#include "shared/error_report.h" #include #include #include #include #include -// see addDllsToPath() below -#pragma comment(linker, "/manifestDependency:\"" \ - "name='dlls' " \ - "processorArchitecture='x86' " \ - "version='1.0.0.0' " \ - "type='win32' \"") - using namespace MOBase; using namespace MOShared; @@ -171,174 +164,6 @@ std::optional handleCommandLine( return {}; } -std::optional selectInstance() -{ - auto& m = InstanceManager::singleton(); - - NexusInterface ni(nullptr); - PluginContainer pc(nullptr); - pc.loadPlugins(); - - if (!m.hasAnyInstances()) { - // no instances configured - CreateInstanceDialog dlg(pc, nullptr); - if (dlg.exec() != QDialog::Accepted) { - return {}; - } - - return m.currentInstance(); - } - - - InstanceManagerDialog dlg(pc); - dlg.setRestartOnSelect(false); - - dlg.show(); - dlg.activateWindow(); - dlg.raise(); - - if (dlg.exec() != QDialog::Accepted) { - return {}; - } - - return m.currentInstance(); -} - -enum class SetupInstanceResults -{ - Ok, - TryAgain, - SelectAnother, - Exit -}; - - -void criticalOnTop(const QString& message) -{ - QMessageBox mb(QMessageBox::Critical, QObject::tr("Mod Organizer"), message); - - mb.show(); - mb.activateWindow(); - mb.raise(); - mb.exec(); -} - - -SetupInstanceResults setupInstance(Instance& instance, PluginContainer& pc) -{ - const auto setupResult = instance.setup(pc); - - switch (setupResult) - { - case Instance::SetupResults::Ok: - { - return SetupInstanceResults::Ok; - } - - case Instance::SetupResults::BadIni: - { - criticalOnTop( - QObject::tr("Cannot open instance '%1', failed to read INI file %2.") - .arg(instance.name()).arg(instance.iniPath())); - - return SetupInstanceResults::SelectAnother; - } - - case Instance::SetupResults::IniMissingGame: - { - criticalOnTop( - QObject::tr( - "Cannot open instance '%1', the managed game was not found in the INI " - "file %2. Select the game managed by this instance.") - .arg(instance.name()).arg(instance.iniPath())); - - CreateInstanceDialog dlg(pc, nullptr); - dlg.setSinglePage(instance.name()); - - dlg.show(); - dlg.activateWindow(); - dlg.raise(); - - if (dlg.exec() != QDialog::Accepted) { - return SetupInstanceResults::Exit; - } - - instance.setGame( - dlg.creationInfo().game->gameName(), - dlg.creationInfo().gameLocation); - - return SetupInstanceResults::TryAgain; - } - - case Instance::SetupResults::PluginGone: - { - criticalOnTop( - QObject::tr( - "Cannot open instance '%1', the game plugin '%2' doesn't exist. It " - "may have been deleted by an antivirus. Select another instance.") - .arg(instance.name()).arg(instance.gameName())); - - return SetupInstanceResults::SelectAnother; - } - - case Instance::SetupResults::GameGone: - { - criticalOnTop( - QObject::tr( - "Cannot open instance '%1', the game directory '%2' doesn't exist or " - "the game plugin '%3' doesn't recognize it. Select the game managed " - "by this instance.") - .arg(instance.name()) - .arg(instance.gameDirectory()) - .arg(instance.gameName())); - - CreateInstanceDialog dlg(pc, nullptr); - dlg.setSinglePage(instance.name()); - - dlg.show(); - dlg.activateWindow(); - dlg.raise(); - - if (dlg.exec() != QDialog::Accepted) { - return SetupInstanceResults::Exit; - } - - instance.setGame( - dlg.creationInfo().game->gameName(), - dlg.creationInfo().gameLocation); - - return SetupInstanceResults::TryAgain; - } - - case Instance::SetupResults::MissingVariant: - { - CreateInstanceDialog dlg(pc, nullptr); - - dlg.getPage()->select( - instance.gamePlugin(), instance.gameDirectory()); - - dlg.setSinglePage(instance.name()); - - dlg.show(); - dlg.activateWindow(); - dlg.raise(); - - if (dlg.exec() != QDialog::Accepted) { - return SetupInstanceResults::Exit; - } - - instance.setVariant(dlg.creationInfo().gameVariant); - - return SetupInstanceResults::TryAgain; - } - - default: - { - return SetupInstanceResults::Exit; - } - } -} - int runApplication( MOApplication &application, const cl::CommandLine& cl, SingleInstance &instance, const QString &dataPath, @@ -561,11 +386,11 @@ int doOneRun( // the previously used instance doesn't exist anymore if (m.hasAnyInstances()) { - criticalOnTop(QObject::tr( + MOShared::criticalOnTop(QObject::tr( "Instance at '%1' not found. Select another instance.") .arg(currentInstance->directory())); } else { - criticalOnTop(QObject::tr( + MOShared::criticalOnTop(QObject::tr( "Instance at '%1' not found. You must create a new instance") .arg(currentInstance->directory())); } diff --git a/src/shared/error_report.cpp b/src/shared/error_report.cpp index 4185b544..09fdcb49 100644 --- a/src/shared/error_report.cpp +++ b/src/shared/error_report.cpp @@ -51,4 +51,14 @@ void reportError(LPCWSTR format, ...) MessageBoxW(nullptr, buffer, L"Error", MB_OK | MB_ICONERROR); } +void criticalOnTop(const QString& message) +{ + QMessageBox mb(QMessageBox::Critical, QObject::tr("Mod Organizer"), message); + + mb.show(); + mb.activateWindow(); + mb.raise(); + mb.exec(); +} + } // namespace MOShared diff --git a/src/shared/error_report.h b/src/shared/error_report.h index da07c728..9343d3da 100644 --- a/src/shared/error_report.h +++ b/src/shared/error_report.h @@ -31,6 +31,14 @@ namespace MOShared void reportError(LPCSTR format, ...); void reportError(LPCWSTR format, ...); +// shows a critical message box that's raised to the top of the zorder, useful +// for messages without a main window, which sometimes makes them pop up behind +// all other windows +// +// the dialog is not topmost, it's just raised once when shown +// +void criticalOnTop(const QString& message); + } // namespace MOShared #endif // MODORGANIZER_SHARED_ERROR_REPORT_INCLUDED -- cgit v1.3.1 From 1bf24c7daad1eb954dc160784802de58f9809fa1 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 7 Nov 2020 17:05:29 -0500 Subject: refactored setupInstance(), comments ok -> okay --- src/instancemanager.cpp | 173 ++++++++++++++++++++++++++++++------------------ src/instancemanager.h | 8 +-- src/main.cpp | 2 +- 3 files changed, 114 insertions(+), 69 deletions(-) (limited to 'src') diff --git a/src/instancemanager.cpp b/src/instancemanager.cpp index b68d715b..4191f850 100644 --- a/src/instancemanager.cpp +++ b/src/instancemanager.cpp @@ -158,7 +158,7 @@ Instance::SetupResults Instance::setup(PluginContainer& plugins) // getting game plugin const auto r = getGamePlugin(plugins); - if (r != SetupResults::Ok) { + if (r != SetupResults::Okay) { return r; } @@ -177,7 +177,7 @@ Instance::SetupResults Instance::setup(PluginContainer& plugins) // installed m_plugin->setGamePath(m_gameDir); - return SetupResults::Ok; + return SetupResults::Okay; } void Instance::updateIni() @@ -248,7 +248,7 @@ Instance::SetupResults Instance::getGamePlugin(PluginContainer& plugins) } m_plugin = game; - return SetupResults::Ok; + return SetupResults::Okay; } } @@ -272,7 +272,7 @@ Instance::SetupResults Instance::getGamePlugin(PluginContainer& plugins) m_plugin = game; m_gameName = game->gameName(); - return SetupResults::Ok; + return SetupResults::Okay; } } @@ -301,7 +301,7 @@ Instance::SetupResults Instance::getGamePlugin(PluginContainer& plugins) m_plugin = game; m_gameDir = game->gameDirectory().absolutePath(); - return SetupResults::Ok; + return SetupResults::Okay; } else { log::warn( "found plugin {} that matches name in ini {}, but no game install " @@ -763,48 +763,126 @@ std::optional selectInstance() { auto& m = InstanceManager::singleton(); + // since there is no instance currently active, load plugins with a null + // OrganizerCore; see PluginContainer::initPlugin() NexusInterface ni(nullptr); PluginContainer pc(nullptr); pc.loadPlugins(); - if (!m.hasAnyInstances()) { - // no instances configured + if (m.hasAnyInstances()) { + // there is at least one instance available, show the instance manager + // dialog + InstanceManagerDialog dlg(pc); + + // the dialog normally restarts MO when an instance is selected, but this + // is not necessary here since MO hasn't really started yet + dlg.setRestartOnSelect(false); + + dlg.show(); + dlg.activateWindow(); + dlg.raise(); + + if (dlg.exec() != QDialog::Accepted) { + return {}; + } + + } else { + // no instances configured, ask the user to create one CreateInstanceDialog dlg(pc, nullptr); + if (dlg.exec() != QDialog::Accepted) { return {}; } + } - return m.currentInstance(); + // return the new instance or the selection + return m.currentInstance(); +} + +// shows the game selection page of the create instance dialog so the user can +// pick which game is managed by this instance +// +// this is used below in setupInstance() when the game directory is gone or +// no plugins can recognize it +// +SetupInstanceResults selectGame(Instance& instance, PluginContainer& pc) +{ + CreateInstanceDialog dlg(pc, nullptr); + + // only show the game page + dlg.setSinglePage(instance.name()); + + dlg.show(); + dlg.activateWindow(); + dlg.raise(); + + if (dlg.exec() != QDialog::Accepted) { + // cancelled + return SetupInstanceResults::Exit; } + // this info will be used instead of the ini, which should fix this + // particular problem + instance.setGame( + dlg.creationInfo().game->gameName(), + dlg.creationInfo().gameLocation); + + return SetupInstanceResults::TryAgain; +} + +// shows the game variant page of the create instance dialog so the user can +// pick which game variant is installed +// +// this is used below in setupInstance() when there is no variant in the ini +// but the game plugin requires one; this can happen when the ini is broken +// or when a new variant has become supported by the plugin for a game the +// user already has an instance for +// +SetupInstanceResults selectVariant(Instance& instance, PluginContainer& pc) +{ + CreateInstanceDialog dlg(pc, nullptr); + + // the variant page uses the game page to know which game was selected, so + // set it manually + dlg.getPage()->select( + instance.gamePlugin(), instance.gameDirectory()); - InstanceManagerDialog dlg(pc); - dlg.setRestartOnSelect(false); + // only show the variant page + dlg.setSinglePage(instance.name()); dlg.show(); dlg.activateWindow(); dlg.raise(); if (dlg.exec() != QDialog::Accepted) { - return {}; + return SetupInstanceResults::Exit; } - return m.currentInstance(); + // this info will be used instead of the ini, which should fix this + // particular problem + instance.setVariant(dlg.creationInfo().gameVariant); + + return SetupInstanceResults::TryAgain; } SetupInstanceResults setupInstance(Instance& instance, PluginContainer& pc) { + // set up the instance const auto setupResult = instance.setup(pc); switch (setupResult) { - case Instance::SetupResults::Ok: + case Instance::SetupResults::Okay: { - return SetupInstanceResults::Ok; + // all good + return SetupInstanceResults::Okay; } case Instance::SetupResults::BadIni: { + // unreadable ini, there's not much that can be done, select another + // instance + MOShared::criticalOnTop( QObject::tr("Cannot open instance '%1', failed to read INI file %2.") .arg(instance.name()).arg(instance.iniPath())); @@ -814,32 +892,26 @@ SetupInstanceResults setupInstance(Instance& instance, PluginContainer& pc) case Instance::SetupResults::IniMissingGame: { + // both the game name and directory are missing from the ini; although + // this shouldn't happen, setup() is able to handle when either is + // missing, but not both + // + // ask the user for the game managed by this instance + MOShared::criticalOnTop( QObject::tr( "Cannot open instance '%1', the managed game was not found in the INI " "file %2. Select the game managed by this instance.") .arg(instance.name()).arg(instance.iniPath())); - CreateInstanceDialog dlg(pc, nullptr); - dlg.setSinglePage(instance.name()); - - dlg.show(); - dlg.activateWindow(); - dlg.raise(); - - if (dlg.exec() != QDialog::Accepted) { - return SetupInstanceResults::Exit; - } - - instance.setGame( - dlg.creationInfo().game->gameName(), - dlg.creationInfo().gameLocation); - - return SetupInstanceResults::TryAgain; + return selectGame(instance, pc); } case Instance::SetupResults::PluginGone: { + // there is no plugin that can handle the game name/directory from the + // ini, so this instance is unusable + MOShared::criticalOnTop( QObject::tr( "Cannot open instance '%1', the game plugin '%2' doesn't exist. It " @@ -851,6 +923,9 @@ SetupInstanceResults setupInstance(Instance& instance, PluginContainer& pc) case Instance::SetupResults::GameGone: { + // the game directory doesn't exist or the plugin doesn't recognize it; + // ask the user for the game managed by this instance + MOShared::criticalOnTop( QObject::tr( "Cannot open instance '%1', the game directory '%2' doesn't exist or " @@ -860,48 +935,18 @@ SetupInstanceResults setupInstance(Instance& instance, PluginContainer& pc) .arg(instance.gameDirectory()) .arg(instance.gameName())); - CreateInstanceDialog dlg(pc, nullptr); - dlg.setSinglePage(instance.name()); - - dlg.show(); - dlg.activateWindow(); - dlg.raise(); - - if (dlg.exec() != QDialog::Accepted) { - return SetupInstanceResults::Exit; - } - - instance.setGame( - dlg.creationInfo().game->gameName(), - dlg.creationInfo().gameLocation); - - return SetupInstanceResults::TryAgain; + return selectGame(instance, pc); } case Instance::SetupResults::MissingVariant: { - CreateInstanceDialog dlg(pc, nullptr); - - dlg.getPage()->select( - instance.gamePlugin(), instance.gameDirectory()); - - dlg.setSinglePage(instance.name()); - - dlg.show(); - dlg.activateWindow(); - dlg.raise(); - - if (dlg.exec() != QDialog::Accepted) { - return SetupInstanceResults::Exit; - } - - instance.setVariant(dlg.creationInfo().gameVariant); - - return SetupInstanceResults::TryAgain; + // the game variant is missing from the ini, ask the user for it + return selectVariant(instance, pc); } default: { + // shouldn't happen return SetupInstanceResults::Exit; } } diff --git a/src/instancemanager.h b/src/instancemanager.h index d75a46f4..4f8b01c7 100644 --- a/src/instancemanager.h +++ b/src/instancemanager.h @@ -32,7 +32,7 @@ public: enum class SetupResults { // instance is ready to be used - Ok, + Okay, // error while reading the INI BadIni, @@ -337,7 +337,7 @@ private: // enum class SetupInstanceResults { - Ok, + Okay, TryAgain, SelectAnother, Exit @@ -348,7 +348,7 @@ enum class SetupInstanceResults // cancelled // // if there is at least one instance available, unconditionally show the -// instance manager dialog and returns the selected instnace or empty if the +// instance manager dialog and returns the selected instance or empty if the // user cancelled // std::optional selectInstance(); @@ -365,7 +365,7 @@ std::optional selectInstance(); // // - if the user cancels at any point, returns Exit // -// - if the instance has been set up correctly, returns Ok +// - if the instance has been set up correctly, returns Okay // SetupInstanceResults setupInstance(Instance& instance, PluginContainer& pc); diff --git a/src/main.cpp b/src/main.cpp index fdd09872..e0326b27 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -237,7 +237,7 @@ int runApplication( { const auto setupResult = setupInstance(currentInstance, *pluginContainer); - if (setupResult == SetupInstanceResults::Ok) { + if (setupResult == SetupInstanceResults::Okay) { break; } else if (setupResult == SetupInstanceResults::TryAgain) { continue; -- cgit v1.3.1 From ba7d24faef49858b9721c2ade0f3ea4a7ba4d96d Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 7 Nov 2020 17:21:13 -0500 Subject: moved handleCommandLine() to CommandLine::setupCore() --- src/commandline.cpp | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/commandline.h | 22 ++++++++++++++++++++++ src/main.cpp | 51 +-------------------------------------------------- 3 files changed, 76 insertions(+), 50 deletions(-) (limited to 'src') diff --git a/src/commandline.cpp b/src/commandline.cpp index dc8cf53f..3d0e901d 100644 --- a/src/commandline.cpp +++ b/src/commandline.cpp @@ -1,10 +1,15 @@ #include "commandline.h" #include "env.h" +#include "organizercore.h" #include "shared/util.h" +#include +#include namespace cl { +using namespace MOBase; + std::string pad_right(std::string s, std::size_t n, char c=' ') { if (s.size() < n) @@ -195,6 +200,54 @@ std::optional CommandLine::run(const std::wstring& line) } } +std::optional CommandLine::setupCore(OrganizerCore& organizer) const +{ + // if we have a command line parameter, it is either a nxm link or + // a binary to start + if (m_shortcut.isValid()) { + if (m_shortcut.hasExecutable()) { + try { + organizer.processRunner() + .setFromShortcut(m_shortcut) + .setWaitForCompletion() + .run(); + + return 0; + } + catch (const std::exception &e) { + reportError( + QObject::tr("failed to start shortcut: %1").arg(e.what())); + return 1; + } + } + } else if (m_nxmLink) { + log::debug("starting download from command line: {}", *m_nxmLink); + organizer.externalMessage(*m_nxmLink); + } else if (m_executable) { + const QString exeName = *m_executable; + log::debug("starting {} from command line", exeName); + + try + { + // pass the remaining parameters to the binary + organizer.processRunner() + .setFromFileOrExecutable(exeName, m_untouched) + .setWaitForCompletion() + .run(); + + return 0; + } + catch (const std::exception &e) + { + reportError( + QObject::tr("failed to start application: %1").arg(e.what())); + return 1; + } + } + + return {}; +} + void CommandLine::clear() { m_vm.clear(); diff --git a/src/commandline.h b/src/commandline.h index 478ee8ea..baa0bfb9 100644 --- a/src/commandline.h +++ b/src/commandline.h @@ -4,6 +4,8 @@ #include #include +class OrganizerCore; + namespace cl { @@ -193,6 +195,18 @@ protected: // if moshortcut:// is detected and has an instance, it will override -i if both // are given // +// +// the command is used in two phases: +// +// 1) run() is called in main() shortly after startup; it parses the command +// line and runs the given command, if any +// +// 2) if the command did not request to exit, the instance is loaded +// in main() et al. and setupCore() is called; it handles moshortcut, +// nxm links and starting executables/binaries +// +// either can return an exit code, which will make MO exit immediately +// class CommandLine { public: @@ -206,6 +220,14 @@ public: // std::optional run(const std::wstring& line); + // handles moshortcut, nxm links and starting processes + // + // returns an empty optional if execution should continue, or a return code + // if MO must quit + // + std::optional setupCore(OrganizerCore& organizer) const; + + // clears parsed options, used when MO is "restarted" so the options aren't // processed again // diff --git a/src/main.cpp b/src/main.cpp index e0326b27..55fedc7a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -115,55 +115,6 @@ void setExceptionHandlers() g_prevTerminateHandler = std::set_terminate(onTerminate); } -std::optional handleCommandLine( - const cl::CommandLine& cl, OrganizerCore& organizer) -{ - // if we have a command line parameter, it is either a nxm link or - // a binary to start - if (cl.shortcut().isValid()) { - if (cl.shortcut().hasExecutable()) { - try { - organizer.processRunner() - .setFromShortcut(cl.shortcut()) - .setWaitForCompletion() - .run(); - - return 0; - } - catch (const std::exception &e) { - reportError( - QObject::tr("failed to start shortcut: %1").arg(e.what())); - return 1; - } - } - } else if (cl.nxmLink()) { - log::debug("starting download from command line: {}", *cl.nxmLink()); - organizer.externalMessage(*cl.nxmLink()); - } else if (cl.executable()) { - const QString exeName = *cl.executable(); - log::debug("starting {} from command line", exeName); - - try - { - // pass the remaining parameters to the binary - organizer.processRunner() - .setFromFileOrExecutable(exeName, cl.untouched()) - .setWaitForCompletion() - .run(); - - return 0; - } - catch (const std::exception &e) - { - reportError( - QObject::tr("failed to start application: %1").arg(e.what())); - return 1; - } - } - - return {}; -} - int runApplication( MOApplication &application, const cl::CommandLine& cl, SingleInstance &instance, const QString &dataPath, @@ -274,7 +225,7 @@ int runApplication( organizer.setCurrentProfile(currentInstance.profileName()); - if (auto r=handleCommandLine(cl, organizer)) { + if (auto r=cl.setupCore(organizer)) { return *r; } -- cgit v1.3.1 From 079fd33cf18cc901d1a6a4463d8a8ccd1d730786 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 7 Nov 2020 17:32:30 -0500 Subject: added sanitychecks.h, moved to namespace sanity added a set SetThisThreadName() after creating the main window, Qt resets it --- src/main.cpp | 22 +++++++++++----------- src/moapplication.cpp | 1 + src/sanitychecks.cpp | 10 ++++++++-- src/sanitychecks.h | 27 +++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 13 deletions(-) create mode 100644 src/sanitychecks.h (limited to 'src') diff --git a/src/main.cpp b/src/main.cpp index 55fedc7a..40512006 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -32,6 +32,7 @@ along with Mod Organizer. If not, see . #include "env.h" #include "envmodule.h" #include "commandline.h" +#include "sanitychecks.h" #include "shared/util.h" #include "shared/appconfig.h" #include "shared/error_report.h" @@ -41,14 +42,9 @@ along with Mod Organizer. If not, see . #include #include - using namespace MOBase; using namespace MOShared; -void sanityChecks(const env::Environment& env); -int checkIncompatibleModule(const env::Module& m); -int checkPathsForSanity(MOBase::IPluginGame& game, const Settings& s); - void purgeOldFiles() { // remove the temporary backup directory in case we're restarting after an @@ -159,11 +155,11 @@ int runApplication( env::Environment env; env.dump(settings); settings.dump(); - sanityChecks(env); + sanity::checkEnvironment(env); const auto moduleNotification = env.onModuleLoaded(qApp, [](auto&& m) { log::debug("loaded module {}", m.toString()); - checkIncompatibleModule(m); + sanity::checkIncompatibleModule(m); }); // this must outlive `organizer` @@ -204,7 +200,7 @@ int runApplication( log::debug("this is a portable instance"); } - checkPathsForSanity(*currentInstance.gamePlugin(), settings); + sanity::checkPaths(*currentInstance.gamePlugin(), settings); organizer.setManagedGame(currentInstance.gamePlugin()); organizer.createDefaultProfile(); @@ -249,10 +245,14 @@ int runApplication( int res = 1; - { // scope to control lifetime of mainwindow + { + // scope to control lifetime of mainwindow // set up main window and its data structures MainWindow mainWindow(settings, organizer, *pluginContainer); + // qt resets the thread name somewhere when creating the main window + MOShared::SetThisThreadName("main"); + ni.getAccessManager()->setTopLevelWidget(&mainWindow); QObject::connect(&mainWindow, SIGNAL(styleChanged(QString)), &application, @@ -373,6 +373,8 @@ int doOneRun( int main(int argc, char *argv[]) { + MOShared::SetThisThreadName("main"); + cl::CommandLine cl; if (auto r=cl.run(GetCommandLineW())) { @@ -381,8 +383,6 @@ int main(int argc, char *argv[]) TimeThis tt("main() to doOneRun()"); - SetThisThreadName("main"); - initLogging(); auto application = MOApplication::create(argc, argv); diff --git a/src/moapplication.cpp b/src/moapplication.cpp index f514050f..43d787f5 100644 --- a/src/moapplication.cpp +++ b/src/moapplication.cpp @@ -20,6 +20,7 @@ along with Mod Organizer. If not, see . #include "moapplication.h" #include "settings.h" #include "env.h" +#include "shared/util.h" #include #include #include diff --git a/src/sanitychecks.cpp b/src/sanitychecks.cpp index 7afce7bd..613e4b26 100644 --- a/src/sanitychecks.cpp +++ b/src/sanitychecks.cpp @@ -1,3 +1,4 @@ +#include "sanitychecks.h" #include "env.h" #include "envmodule.h" #include "settings.h" @@ -5,6 +6,9 @@ #include #include +namespace sanity +{ + using namespace MOBase; enum class SecurityZone @@ -368,7 +372,7 @@ int checkProtected(const QDir& d, const QString& what) return 0; } -int checkPathsForSanity(IPluginGame& game, const Settings& s) +int checkPaths(IPluginGame& game, const Settings& s) { log::debug("checking paths"); @@ -390,7 +394,7 @@ int checkPathsForSanity(IPluginGame& game, const Settings& s) return n; } -void sanityChecks(const env::Environment& e) +void checkEnvironment(const env::Environment& e) { log::debug("running sanity checks..."); @@ -404,3 +408,5 @@ void sanityChecks(const env::Environment& e) "sanity checks done, {}", (n > 0 ? "problems were found" : "everything looks okay")); } + +} // namespace diff --git a/src/sanitychecks.h b/src/sanitychecks.h new file mode 100644 index 00000000..8786ee78 --- /dev/null +++ b/src/sanitychecks.h @@ -0,0 +1,27 @@ +#ifndef MODORGANIZER_SANITYCHECKS_INCLUDED +#define MODORGANIZER_SANITYCHECKS_INCLUDED + +namespace env +{ + class Environment; + class Module; +} + +namespace MOBase +{ + class IPluginGame; +} + +class Settings; + + +namespace sanity +{ + +void checkEnvironment(const env::Environment& env); +int checkIncompatibleModule(const env::Module& m); +int checkPaths(MOBase::IPluginGame& game, const Settings& s); + +} // namespace + +#endif // MODORGANIZER_SANITYCHECKS_INCLUDED -- cgit v1.3.1 From 1b7384f7cb3dc32063e588c174265b8c46cfa629 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 7 Nov 2020 17:55:25 -0500 Subject: moved most of the stuff from main.cpp into MOApplication --- src/main.cpp | 288 +---------------------------------------------- src/moapplication.cpp | 303 ++++++++++++++++++++++++++++++++++++++++++++++++-- src/moapplication.h | 20 +++- 3 files changed, 315 insertions(+), 296 deletions(-) (limited to 'src') diff --git a/src/main.cpp b/src/main.cpp index 40512006..72d598a8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -45,21 +45,6 @@ along with Mod Organizer. If not, see . using namespace MOBase; using namespace MOShared; -void purgeOldFiles() -{ - // remove the temporary backup directory in case we're restarting after an - // update - QString backupDirectory = qApp->applicationDirPath() + "/update_backup"; - if (QDir(backupDirectory).exists()) { - shellDelete(QStringList(backupDirectory)); - } - - // cycle log file - removeOldFiles( - qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath()), - "usvfs*.log", 5, QDir::Name); -} - thread_local LPTOP_LEVEL_EXCEPTION_FILTER g_prevExceptionFilter = nullptr; thread_local std::terminate_handler g_prevTerminateHandler = nullptr; @@ -111,180 +96,6 @@ void setExceptionHandlers() g_prevTerminateHandler = std::set_terminate(onTerminate); } -int runApplication( - MOApplication &application, const cl::CommandLine& cl, - SingleInstance &instance, const QString &dataPath, - Instance& currentInstance) -{ - TimeThis tt("runApplication() to exec()"); - - log::info( - "starting Mod Organizer version {} revision {} in {}, usvfs: {}", - createVersionInfo().displayString(3), GITID, - QCoreApplication::applicationDirPath(), MOShared::getUsvfsVersionString()); - - log::info("data path: {}", dataPath); - - log::info("working directory: {}", QDir::currentPath()); - - if (!instance.secondary()) { - purgeOldFiles(); - } - - QWindowsWindowFunctions::setWindowActivationBehavior( - QWindowsWindowFunctions::AlwaysActivateWindow); - - try - { - Settings settings( - dataPath + "/" + QString::fromStdWString(AppConfig::iniFileName()), - true); - - log::getDefault().setLevel(settings.diagnostics().logLevel()); - - log::debug("using ini at '{}'", settings.filename()); - - if (instance.secondary()) { - log::debug("another instance of MO is running but --multiple was given"); - } - - // global crashDumpType sits in OrganizerCore to make a bit less ugly to - // update it when the settings are changed during runtime - OrganizerCore::setGlobalCrashDumpsType(settings.diagnostics().crashDumpsType()); - - env::Environment env; - env.dump(settings); - settings.dump(); - sanity::checkEnvironment(env); - - const auto moduleNotification = env.onModuleLoaded(qApp, [](auto&& m) { - log::debug("loaded module {}", m.toString()); - sanity::checkIncompatibleModule(m); - }); - - // this must outlive `organizer` - std::unique_ptr pluginContainer; - - log::debug("initializing nexus interface"); - NexusInterface ni(&settings); - - log::debug("initializing core"); - OrganizerCore organizer(settings); - if (!organizer.bootstrap()) { - reportError("failed to set up data paths"); - InstanceManager::singleton().clearCurrentInstance(); - return 1; - } - - log::debug("initializing plugins"); - pluginContainer = std::make_unique(&organizer); - pluginContainer->loadPlugins(); - - for (;;) - { - const auto setupResult = setupInstance(currentInstance, *pluginContainer); - - if (setupResult == SetupInstanceResults::Okay) { - break; - } else if (setupResult == SetupInstanceResults::TryAgain) { - continue; - } else if (setupResult == SetupInstanceResults::SelectAnother) { - InstanceManager::singleton().clearCurrentInstance(); - return RestartExitCode; - } else { - return 1; - } - } - - if (currentInstance.isPortable()) { - log::debug("this is a portable instance"); - } - - sanity::checkPaths(*currentInstance.gamePlugin(), settings); - - organizer.setManagedGame(currentInstance.gamePlugin()); - organizer.createDefaultProfile(); - - log::info( - "using game plugin '{}' ('{}', variant {}, steam id '{}') at {}", - currentInstance.gamePlugin()->gameName(), - currentInstance.gamePlugin()->gameShortName(), - (settings.game().edition().value_or("").isEmpty() ? - "(none)" : *settings.game().edition()), - currentInstance.gamePlugin()->steamAPPId(), - currentInstance.gamePlugin()->gameDirectory().absolutePath()); - - - CategoryFactory::instance().loadCategories(); - organizer.updateExecutablesList(); - organizer.updateModInfoFromDisc(); - - organizer.setCurrentProfile(currentInstance.profileName()); - - if (auto r=cl.setupCore(organizer)) { - return *r; - } - - MOSplash splash(settings, dataPath, currentInstance.gamePlugin()); - - QString apiKey; - if (GlobalSettings::nexusApiKey(apiKey)) { - ni.getAccessManager()->apiCheck(apiKey); - } - - log::debug("initializing tutorials"); - TutorialManager::init( - qApp->applicationDirPath() + "/" - + QString::fromStdWString(AppConfig::tutorialsPath()) + "/", - &organizer); - - if (!application.setStyleFile(settings.interface().styleName().value_or(""))) { - // disable invalid stylesheet - settings.interface().setStyleName(""); - } - - int res = 1; - - { - // scope to control lifetime of mainwindow - // set up main window and its data structures - MainWindow mainWindow(settings, organizer, *pluginContainer); - - // qt resets the thread name somewhere when creating the main window - MOShared::SetThisThreadName("main"); - - ni.getAccessManager()->setTopLevelWidget(&mainWindow); - - QObject::connect(&mainWindow, SIGNAL(styleChanged(QString)), &application, - SLOT(setStyleFile(QString))); - QObject::connect(&instance, SIGNAL(messageSent(QString)), &organizer, - SLOT(externalMessage(QString))); - - log::debug("displaying main window"); - mainWindow.show(); - mainWindow.activateWindow(); - - splash.close(); - - tt.stop(); - - res = application.exec(); - mainWindow.close(); - - ni.getAccessManager()->setTopLevelWidget(nullptr); - } - - settings.geometry().resetIfNeeded(); - return res; - } - catch (const std::exception &e) - { - reportError(e.what()); - } - - return 1; -} - int forwardToPrimary(SingleInstance& instance, const cl::CommandLine& cl) { if (cl.shortcut().isValid()) { @@ -300,118 +111,25 @@ int forwardToPrimary(SingleInstance& instance, const cl::CommandLine& cl) return 0; } -void resetForRestart(cl::CommandLine& cl) -{ - LogModel::instance().clear(); - ResetExitFlag(); - - // make sure the log file isn't locked in case MO was restarted and - // the previous instance gets deleted - log::getDefault().setFile({}); - - // don't reprocess command line - cl.clear(); -} - -int doOneRun( - cl::CommandLine& cl, MOApplication& application, SingleInstance& instance) -{ - TimeThis tt("doOneRun() to runApplication()"); - - // resets things when MO is "restarted" - resetForRestart(cl); - - auto& m = InstanceManager::singleton(); - auto currentInstance = m.currentInstance(); - - if (!currentInstance) - { - currentInstance = selectInstance(); - if (!currentInstance) { - return 1; - } - } - else - { - if (!QDir(currentInstance->directory()).exists()) { - // the previously used instance doesn't exist anymore - - if (m.hasAnyInstances()) { - MOShared::criticalOnTop(QObject::tr( - "Instance at '%1' not found. Select another instance.") - .arg(currentInstance->directory())); - } else { - MOShared::criticalOnTop(QObject::tr( - "Instance at '%1' not found. You must create a new instance") - .arg(currentInstance->directory())); - } - - currentInstance = selectInstance(); - if (!currentInstance) { - return 1; - } - } - } - - const QString dataPath = currentInstance->directory(); - application.setProperty("dataPath", dataPath); - - setExceptionHandlers(); - - if (!setLogDirectory(dataPath)) { - reportError("Failed to create log folder"); - InstanceManager::singleton().clearCurrentInstance(); - return 1; - } - - log::debug("command line: '{}'", QString::fromWCharArray(GetCommandLineW())); - - tt.stop(); - - return runApplication(application, cl, instance, dataPath, *currentInstance); -} int main(int argc, char *argv[]) { MOShared::SetThisThreadName("main"); cl::CommandLine cl; - if (auto r=cl.run(GetCommandLineW())) { return *r; } - TimeThis tt("main() to doOneRun()"); - initLogging(); - auto application = MOApplication::create(argc, argv); + QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + MOApplication app(cl, argc, argv); SingleInstance instance(cl.multiple()); if (instance.ephemeral()) { return forwardToPrimary(instance, cl); } - tt.stop(); - - if (cl.instance()) - InstanceManager::singleton().overrideInstance(*cl.instance()); - - if (cl.profile()) { - InstanceManager::singleton().overrideProfile(*cl.profile()); - } - - // makes plugin data path available to plugins, see - // IOrganizer::getPluginDataPath() - MOBase::details::setPluginDataPath(OrganizerCore::pluginDataPath()); - - for (;;) - { - const auto r = doOneRun(cl, application, instance); - if (r == RestartExitCode) { - continue; - } - - return r; - } + return app.run(instance); } diff --git a/src/moapplication.cpp b/src/moapplication.cpp index 43d787f5..b4ba7da6 100644 --- a/src/moapplication.cpp +++ b/src/moapplication.cpp @@ -20,6 +20,18 @@ along with Mod Organizer. If not, see . #include "moapplication.h" #include "settings.h" #include "env.h" +#include "commandline.h" +#include "instancemanager.h" +#include "organizercore.h" +#include "thread_utils.h" +#include "loglist.h" +#include "singleinstance.h" +#include "nexusinterface.h" +#include "nxmaccessmanager.h" +#include "tutorialmanager.h" +#include "sanitychecks.h" +#include "mainwindow.h" +#include "shared/error_report.h" #include "shared/util.h" #include #include @@ -42,7 +54,7 @@ along with Mod Organizer. If not, see . "type='win32' \"") using namespace MOBase; - +using namespace MOShared; class ProxyStyle : public QProxyStyle { public: @@ -120,8 +132,8 @@ void addDllsToPath() } -MOApplication::MOApplication(int& argc, char** argv) - : QApplication(argc, argv) +MOApplication::MOApplication(cl::CommandLine& cl, int& argc, char** argv) + : QApplication(argc, argv), m_cl(cl) { connect(&m_StyleWatcher, &QFileSystemWatcher::fileChanged, [&](auto&& file){ log::debug("style file '{}' changed, reloading", file); @@ -133,11 +145,288 @@ MOApplication::MOApplication(int& argc, char** argv) addDllsToPath(); } -MOApplication MOApplication::create(int& argc, char** argv) +int MOApplication::run(SingleInstance& singleInstance) +{ + auto& m = InstanceManager::singleton(); + + if (m_cl.instance()) + m.overrideInstance(*m_cl.instance()); + + if (m_cl.profile()) { + m.overrideProfile(*m_cl.profile()); + } + + // makes plugin data path available to plugins, see + // IOrganizer::getPluginDataPath() + MOBase::details::setPluginDataPath(OrganizerCore::pluginDataPath()); + + for (;;) + { + const auto r = doOneRun(singleInstance); + if (r == RestartExitCode) { + continue; + } + + return r; + } +} + +int MOApplication::doOneRun(SingleInstance& singleInstance) { - QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + TimeThis tt("doOneRun() to runApplication()"); + + // resets things when MO is "restarted" + resetForRestart(); + + auto& m = InstanceManager::singleton(); + auto currentInstance = m.currentInstance(); + + if (!currentInstance) + { + currentInstance = selectInstance(); + if (!currentInstance) { + return 1; + } + } + else + { + if (!QDir(currentInstance->directory()).exists()) { + // the previously used instance doesn't exist anymore + + if (m.hasAnyInstances()) { + MOShared::criticalOnTop(QObject::tr( + "Instance at '%1' not found. Select another instance.") + .arg(currentInstance->directory())); + } else { + MOShared::criticalOnTop(QObject::tr( + "Instance at '%1' not found. You must create a new instance") + .arg(currentInstance->directory())); + } + + currentInstance = selectInstance(); + if (!currentInstance) { + return 1; + } + } + } + + const QString dataPath = currentInstance->directory(); + setProperty("dataPath", dataPath); + + setExceptionHandlers(); + + if (!setLogDirectory(dataPath)) { + reportError("Failed to create log folder"); + InstanceManager::singleton().clearCurrentInstance(); + return 1; + } + + log::debug("command line: '{}'", QString::fromWCharArray(GetCommandLineW())); + + tt.stop(); - return MOApplication(argc, argv); + return runApplication(singleInstance, dataPath, *currentInstance); +} + +int MOApplication::runApplication( + SingleInstance& singleInstance, + const QString &dataPath, Instance& currentInstance) +{ + TimeThis tt("runApplication() to exec()"); + + log::info( + "starting Mod Organizer version {} revision {} in {}, usvfs: {}", + createVersionInfo().displayString(3), GITID, + QCoreApplication::applicationDirPath(), MOShared::getUsvfsVersionString()); + + log::info("data path: {}", dataPath); + + log::info("working directory: {}", QDir::currentPath()); + + if (!singleInstance.secondary()) { + purgeOldFiles(); + } + + QWindowsWindowFunctions::setWindowActivationBehavior( + QWindowsWindowFunctions::AlwaysActivateWindow); + + try + { + Settings settings( + dataPath + "/" + QString::fromStdWString(AppConfig::iniFileName()), + true); + + log::getDefault().setLevel(settings.diagnostics().logLevel()); + + log::debug("using ini at '{}'", settings.filename()); + + if (singleInstance.secondary()) { + log::debug("another instance of MO is running but --multiple was given"); + } + + // global crashDumpType sits in OrganizerCore to make a bit less ugly to + // update it when the settings are changed during runtime + OrganizerCore::setGlobalCrashDumpsType(settings.diagnostics().crashDumpsType()); + + env::Environment env; + env.dump(settings); + settings.dump(); + sanity::checkEnvironment(env); + + const auto moduleNotification = env.onModuleLoaded(qApp, [](auto&& m) { + log::debug("loaded module {}", m.toString()); + sanity::checkIncompatibleModule(m); + }); + + // this must outlive `organizer` + std::unique_ptr pluginContainer; + + log::debug("initializing nexus interface"); + NexusInterface ni(&settings); + + log::debug("initializing core"); + OrganizerCore organizer(settings); + if (!organizer.bootstrap()) { + reportError("failed to set up data paths"); + InstanceManager::singleton().clearCurrentInstance(); + return 1; + } + + log::debug("initializing plugins"); + pluginContainer = std::make_unique(&organizer); + pluginContainer->loadPlugins(); + + for (;;) + { + const auto setupResult = setupInstance(currentInstance, *pluginContainer); + + if (setupResult == SetupInstanceResults::Okay) { + break; + } else if (setupResult == SetupInstanceResults::TryAgain) { + continue; + } else if (setupResult == SetupInstanceResults::SelectAnother) { + InstanceManager::singleton().clearCurrentInstance(); + return RestartExitCode; + } else { + return 1; + } + } + + if (currentInstance.isPortable()) { + log::debug("this is a portable instance"); + } + + sanity::checkPaths(*currentInstance.gamePlugin(), settings); + + organizer.setManagedGame(currentInstance.gamePlugin()); + organizer.createDefaultProfile(); + + log::info( + "using game plugin '{}' ('{}', variant {}, steam id '{}') at {}", + currentInstance.gamePlugin()->gameName(), + currentInstance.gamePlugin()->gameShortName(), + (settings.game().edition().value_or("").isEmpty() ? + "(none)" : *settings.game().edition()), + currentInstance.gamePlugin()->steamAPPId(), + currentInstance.gamePlugin()->gameDirectory().absolutePath()); + + + CategoryFactory::instance().loadCategories(); + organizer.updateExecutablesList(); + organizer.updateModInfoFromDisc(); + + organizer.setCurrentProfile(currentInstance.profileName()); + + if (auto r=m_cl.setupCore(organizer)) { + return *r; + } + + MOSplash splash(settings, dataPath, currentInstance.gamePlugin()); + + QString apiKey; + if (GlobalSettings::nexusApiKey(apiKey)) { + ni.getAccessManager()->apiCheck(apiKey); + } + + log::debug("initializing tutorials"); + TutorialManager::init( + qApp->applicationDirPath() + "/" + + QString::fromStdWString(AppConfig::tutorialsPath()) + "/", + &organizer); + + if (!setStyleFile(settings.interface().styleName().value_or(""))) { + // disable invalid stylesheet + settings.interface().setStyleName(""); + } + + int res = 1; + + { + // scope to control lifetime of mainwindow + // set up main window and its data structures + MainWindow mainWindow(settings, organizer, *pluginContainer); + + // qt resets the thread name somewhere when creating the main window + MOShared::SetThisThreadName("main"); + + ni.getAccessManager()->setTopLevelWidget(&mainWindow); + + QObject::connect(&mainWindow, SIGNAL(styleChanged(QString)), this, + SLOT(setStyleFile(QString))); + QObject::connect(&singleInstance, SIGNAL(messageSent(QString)), &organizer, + SLOT(externalMessage(QString))); + + log::debug("displaying main window"); + mainWindow.show(); + mainWindow.activateWindow(); + + splash.close(); + + tt.stop(); + + res = exec(); + mainWindow.close(); + + ni.getAccessManager()->setTopLevelWidget(nullptr); + } + + settings.geometry().resetIfNeeded(); + return res; + } + catch (const std::exception &e) + { + reportError(e.what()); + } + + return 1; +} + +void MOApplication::purgeOldFiles() +{ + // remove the temporary backup directory in case we're restarting after an + // update + QString backupDirectory = qApp->applicationDirPath() + "/update_backup"; + if (QDir(backupDirectory).exists()) { + shellDelete(QStringList(backupDirectory)); + } + + // cycle log file + removeOldFiles( + qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath()), + "usvfs*.log", 5, QDir::Name); +} + +void MOApplication::resetForRestart() +{ + LogModel::instance().clear(); + ResetExitFlag(); + + // make sure the log file isn't locked in case MO was restarted and + // the previous instance gets deleted + log::getDefault().setFile({}); + + // don't reprocess command line + m_cl.clear(); } bool MOApplication::setStyleFile(const QString& styleName) @@ -163,7 +452,6 @@ bool MOApplication::setStyleFile(const QString& styleName) return true; } - bool MOApplication::notify(QObject* receiver, QEvent* event) { try { @@ -183,7 +471,6 @@ bool MOApplication::notify(QObject* receiver, QEvent* event) } } - void MOApplication::updateStyle(const QString& fileName) { if (QStyleFactory::keys().contains(fileName)) { diff --git a/src/moapplication.h b/src/moapplication.h index 0ee04492..c1512d9b 100644 --- a/src/moapplication.h +++ b/src/moapplication.h @@ -24,15 +24,21 @@ along with Mod Organizer. If not, see . #include class Settings; +class SingleInstance; +class Instance; + namespace MOBase { class IPluginGame; } +namespace cl { class CommandLine; } class MOApplication : public QApplication { Q_OBJECT public: - static MOApplication create(int& argc, char** argv); - virtual bool notify (QObject* receiver, QEvent* event); + MOApplication(cl::CommandLine& cl, int& argc, char** argv); + + int run(SingleInstance& si); + virtual bool notify(QObject* receiver, QEvent* event); public slots: bool setStyleFile(const QString& style); @@ -43,8 +49,16 @@ private slots: private: QFileSystemWatcher m_StyleWatcher; QString m_DefaultStyle; + cl::CommandLine& m_cl; + + int doOneRun(SingleInstance& singleInstance); + + int runApplication( + SingleInstance& singleInstance, + const QString &dataPath, Instance& currentInstance); - MOApplication(int& argc, char** argv); + void purgeOldFiles(); + void resetForRestart(); }; -- cgit v1.3.1 From 98b782caff4d1b5c316f5773d7a07494912bb2d8 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 7 Nov 2020 17:58:07 -0500 Subject: reordered functions, removed unused headers --- src/main.cpp | 113 +++++++++++++++++++++-------------------------------------- 1 file changed, 39 insertions(+), 74 deletions(-) (limited to 'src') diff --git a/src/main.cpp b/src/main.cpp index 72d598a8..a2e16cc0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,53 +1,56 @@ -/* -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 "mainwindow.h" #include "singleinstance.h" #include "loglist.h" -#include "selectiondialog.h" #include "moapplication.h" -#include "tutorialmanager.h" -#include "nxmaccessmanager.h" -#include "instancemanager.h" -#include "instancemanagerdialog.h" -#include "createinstancedialog.h" -#include "createinstancedialogpages.h" #include "organizercore.h" -#include "env.h" -#include "envmodule.h" #include "commandline.h" -#include "sanitychecks.h" #include "shared/util.h" -#include "shared/appconfig.h" -#include "shared/error_report.h" -#include -#include #include #include -#include using namespace MOBase; -using namespace MOShared; thread_local LPTOP_LEVEL_EXCEPTION_FILTER g_prevExceptionFilter = nullptr; thread_local std::terminate_handler g_prevTerminateHandler = nullptr; +int forwardToPrimary(SingleInstance& instance, const cl::CommandLine& cl); + +int main(int argc, char *argv[]) +{ + MOShared::SetThisThreadName("main"); + + cl::CommandLine cl; + if (auto r=cl.run(GetCommandLineW())) { + return *r; + } + + initLogging(); + + QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + MOApplication app(cl, argc, argv); + + SingleInstance instance(cl.multiple()); + if (instance.ephemeral()) { + return forwardToPrimary(instance, cl); + } + + return app.run(instance); +} + +int forwardToPrimary(SingleInstance& instance, const cl::CommandLine& cl) +{ + if (cl.shortcut().isValid()) { + instance.sendMessage(cl.shortcut().toString()); + } else if (cl.nxmLink()) { + instance.sendMessage(*cl.nxmLink()); + } else { + QMessageBox::information( + nullptr, QObject::tr("Mod Organizer"), + QObject::tr("An instance of Mod Organizer is already running")); + } + + return 0; +} + LONG WINAPI onUnhandledException(_EXCEPTION_POINTERS* ptrs) { const std::wstring& dumpPath = OrganizerCore::crashDumpsPath(); @@ -95,41 +98,3 @@ void setExceptionHandlers() g_prevExceptionFilter = SetUnhandledExceptionFilter(onUnhandledException); g_prevTerminateHandler = std::set_terminate(onTerminate); } - -int forwardToPrimary(SingleInstance& instance, const cl::CommandLine& cl) -{ - if (cl.shortcut().isValid()) { - instance.sendMessage(cl.shortcut().toString()); - } else if (cl.nxmLink()) { - instance.sendMessage(*cl.nxmLink()); - } else { - QMessageBox::information( - nullptr, QObject::tr("Mod Organizer"), - QObject::tr("An instance of Mod Organizer is already running")); - } - - return 0; -} - - -int main(int argc, char *argv[]) -{ - MOShared::SetThisThreadName("main"); - - cl::CommandLine cl; - if (auto r=cl.run(GetCommandLineW())) { - return *r; - } - - initLogging(); - - QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); - MOApplication app(cl, argc, argv); - - SingleInstance instance(cl.multiple()); - if (instance.ephemeral()) { - return forwardToPrimary(instance, cl); - } - - return app.run(instance); -} -- cgit v1.3.1 From b4f6275c3ef888551e14e5d64739e1c32978b8e8 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 7 Nov 2020 18:14:24 -0500 Subject: split getCurrentInstance() --- src/moapplication.cpp | 54 ++++++++++++++++++++++++++++----------------------- src/moapplication.h | 5 +++++ 2 files changed, 35 insertions(+), 24 deletions(-) (limited to 'src') diff --git a/src/moapplication.cpp b/src/moapplication.cpp index b4ba7da6..24cafd05 100644 --- a/src/moapplication.cpp +++ b/src/moapplication.cpp @@ -160,8 +160,13 @@ int MOApplication::run(SingleInstance& singleInstance) // IOrganizer::getPluginDataPath() MOBase::details::setPluginDataPath(OrganizerCore::pluginDataPath()); + // MO runs in a loop because it can be restarted in several ways, such as + // when switching instances or changing some settings for (;;) { + // resets things when MO is "restarted" + resetForRestart(); + const auto r = doOneRun(singleInstance); if (r == RestartExitCode) { continue; @@ -175,18 +180,37 @@ int MOApplication::doOneRun(SingleInstance& singleInstance) { TimeThis tt("doOneRun() to runApplication()"); - // resets things when MO is "restarted" - resetForRestart(); + auto currentInstance = getCurrentInstance(); + if (!currentInstance) { + return 1; + } + + const QString dataPath = currentInstance->directory(); + setProperty("dataPath", dataPath); + + setExceptionHandlers(); + + if (!setLogDirectory(dataPath)) { + reportError("Failed to create log folder"); + InstanceManager::singleton().clearCurrentInstance(); + return 1; + } + + log::debug("command line: '{}'", QString::fromWCharArray(GetCommandLineW())); + + tt.stop(); + + return runApplication(singleInstance, dataPath, *currentInstance); +} +std::optional MOApplication::getCurrentInstance() +{ auto& m = InstanceManager::singleton(); auto currentInstance = m.currentInstance(); if (!currentInstance) { currentInstance = selectInstance(); - if (!currentInstance) { - return 1; - } } else { @@ -204,28 +228,10 @@ int MOApplication::doOneRun(SingleInstance& singleInstance) } currentInstance = selectInstance(); - if (!currentInstance) { - return 1; - } } } - const QString dataPath = currentInstance->directory(); - setProperty("dataPath", dataPath); - - setExceptionHandlers(); - - if (!setLogDirectory(dataPath)) { - reportError("Failed to create log folder"); - InstanceManager::singleton().clearCurrentInstance(); - return 1; - } - - log::debug("command line: '{}'", QString::fromWCharArray(GetCommandLineW())); - - tt.stop(); - - return runApplication(singleInstance, dataPath, *currentInstance); + return currentInstance; } int MOApplication::runApplication( diff --git a/src/moapplication.h b/src/moapplication.h index c1512d9b..8cbb5f28 100644 --- a/src/moapplication.h +++ b/src/moapplication.h @@ -37,7 +37,10 @@ class MOApplication : public QApplication public: MOApplication(cl::CommandLine& cl, int& argc, char** argv); + // sets up everything, creates the main window and runs it + // int run(SingleInstance& si); + virtual bool notify(QObject* receiver, QEvent* event); public slots: @@ -53,6 +56,8 @@ private: int doOneRun(SingleInstance& singleInstance); + std::optional getCurrentInstance(); + int runApplication( SingleInstance& singleInstance, const QString &dataPath, Instance& currentInstance); -- cgit v1.3.1 From 08c952e53a4efcd5b50c0ec947bf216101c027ef Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 7 Nov 2020 19:05:15 -0500 Subject: stopped using core dump stuff from usvfs, mo has its own set exception handler at the start, it can handle not having qt or data paths hopefully fixed infinite crash dumps --- src/CMakeLists.txt | 1 + src/env.cpp | 27 ++++++++++++++-------- src/env.h | 21 ----------------- src/envdump.h | 30 ++++++++++++++++++++++++ src/main.cpp | 27 ++++++++++++++-------- src/mainwindow.cpp | 4 ++-- src/moapplication.cpp | 4 +--- src/organizercore.cpp | 48 +++++++++++++++++++++++---------------- src/organizercore.h | 13 +++++------ src/settings.cpp | 12 +++++----- src/settings.h | 9 ++++---- src/settingsdialogdiagnostics.cpp | 20 ++++++++-------- src/usvfsconnector.cpp | 33 +++++++++++++++------------ src/usvfsconnector.h | 3 ++- 14 files changed, 146 insertions(+), 106 deletions(-) create mode 100644 src/envdump.h (limited to 'src') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index eec33460..30614e59 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -70,6 +70,7 @@ add_filter(NAME src/downloads GROUPS add_filter(NAME src/env GROUPS env + envdump envfs envmetrics envmodule diff --git a/src/env.cpp b/src/env.cpp index 5bed84c1..98d2e6ea 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -4,6 +4,7 @@ #include "envsecurity.h" #include "envshortcut.h" #include "envwindows.h" +#include "envdump.h" #include "settings.h" #include #include @@ -1182,8 +1183,16 @@ HandlePtr tempFile(const std::wstring dir) return {}; } -HandlePtr dumpFile() +HandlePtr dumpFile(const wchar_t* dir) { + // try the given directory, if any + if (dir) { + HandlePtr h = tempFile(dir); + if (h.get() != INVALID_HANDLE_VALUE) { + return h; + } + } + // try the current directory HandlePtr h = tempFile(L"."); if (h.get() != INVALID_HANDLE_VALUE) { @@ -1193,10 +1202,10 @@ HandlePtr dumpFile() std::wclog << L"cannot write dump file in current directory\n"; // try the temp directory - const auto dir = tempDir(); + const auto temp = tempDir(); - if (!dir.empty()) { - h = tempFile(dir.c_str()); + if (!temp.empty()) { + h = tempFile(temp.c_str()); if (h.get() != INVALID_HANDLE_VALUE) { return h; } @@ -1235,11 +1244,11 @@ std::string toString(CoreDumpTypes type) } -bool createMiniDump(HANDLE process, CoreDumpTypes type) +bool createMiniDump(const wchar_t* dir, HANDLE process, CoreDumpTypes type) { const DWORD pid = GetProcessId(process); - const HandlePtr file = dumpFile(); + const HandlePtr file = dumpFile(dir); if (!file) { std::wcerr << L"nowhere to write the dump file\n"; return false; @@ -1278,10 +1287,10 @@ bool createMiniDump(HANDLE process, CoreDumpTypes type) } -bool coredump(CoreDumpTypes type) +bool coredump(const wchar_t* dir, CoreDumpTypes type) { std::wclog << L"creating minidump for the current process\n"; - return createMiniDump(GetCurrentProcess(), type); + return createMiniDump(dir, GetCurrentProcess(), type); } bool coredumpOther(CoreDumpTypes type) @@ -1309,7 +1318,7 @@ bool coredumpOther(CoreDumpTypes type) return false; } - return createMiniDump(handle.get(), type); + return createMiniDump(nullptr, handle.get(), type); } } // namespace diff --git a/src/env.h b/src/env.h index dbbd5cf4..e9cb7a36 100644 --- a/src/env.h +++ b/src/env.h @@ -309,27 +309,6 @@ bool registryValueExists(const QString& key, const QString& value); // void deleteRegistryKeyIfEmpty(const QString& name); - -enum class CoreDumpTypes -{ - Mini = 1, - Data, - Full -}; - - -CoreDumpTypes coreDumpTypeFromString(const std::string& s); -std::string toString(CoreDumpTypes type); - -// creates a minidump file for this process -// -bool coredump(CoreDumpTypes type); - -// finds another process with the same name as this one and creates a minidump -// file for it -// -bool coredumpOther(CoreDumpTypes type); - } // namespace env #endif // ENV_ENV_H diff --git a/src/envdump.h b/src/envdump.h new file mode 100644 index 00000000..355df783 --- /dev/null +++ b/src/envdump.h @@ -0,0 +1,30 @@ +#ifndef MODORGANIZER_ENVDUMP_INCLUDED +#define MODORGANIZER_ENVDUMP_INCLUDED + +namespace env +{ + +enum class CoreDumpTypes +{ + None, + Mini, + Data, + Full +}; + + +CoreDumpTypes coreDumpTypeFromString(const std::string& s); +std::string toString(CoreDumpTypes type); + +// creates a minidump file for this process +// +bool coredump(const wchar_t* dir, CoreDumpTypes type); + +// finds another process with the same name as this one and creates a minidump +// file for it +// +bool coredumpOther(CoreDumpTypes type); + +} // namespace + +#endif // MODORGANIZER_ENVDUMP_INCLUDED diff --git a/src/main.cpp b/src/main.cpp index a2e16cc0..ad074501 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,8 +3,9 @@ #include "moapplication.h" #include "organizercore.h" #include "commandline.h" +#include "env.h" +#include "thread_utils.h" #include "shared/util.h" -#include #include using namespace MOBase; @@ -17,6 +18,7 @@ int forwardToPrimary(SingleInstance& instance, const cl::CommandLine& cl); int main(int argc, char *argv[]) { MOShared::SetThisThreadName("main"); + setExceptionHandlers(); cl::CommandLine cl; if (auto r=cl.run(GetCommandLineW())) { @@ -53,20 +55,20 @@ int forwardToPrimary(SingleInstance& instance, const cl::CommandLine& cl) LONG WINAPI onUnhandledException(_EXCEPTION_POINTERS* ptrs) { - const std::wstring& dumpPath = OrganizerCore::crashDumpsPath(); + const auto path = OrganizerCore::getGlobalCoreDumpPath(); + const auto type = OrganizerCore::getGlobalCoreDumpType(); - const int r = CreateMiniDump( - ptrs, OrganizerCore::getGlobalCrashDumpsType(), dumpPath.c_str()); + const auto r = env::coredump(path.empty() ? nullptr : path.c_str(), type); - if (r == 0) { - log::error("ModOrganizer has crashed, crash dump created."); + if (r) { + log::error("ModOrganizer has crashed, core dump created."); } else { - log::error( - "ModOrganizer has crashed, CreateMiniDump failed ({}, error {}).", - r, GetLastError()); + log::error("ModOrganizer has crashed, core dump failed"); } - if (g_prevExceptionFilter && ptrs) + // g_prevExceptionFilter somehow sometimes point to this function, making this + // recurse and create hundreds of core dump, not sure why + if (g_prevExceptionFilter && ptrs && g_prevExceptionFilter != onUnhandledException) return g_prevExceptionFilter(ptrs); else return EXCEPTION_CONTINUE_SEARCH; @@ -95,6 +97,11 @@ void onTerminate() noexcept void setExceptionHandlers() { + if (g_prevExceptionFilter) { + // already called + return; + } + g_prevExceptionFilter = SetUnhandledExceptionFilter(onUnhandledException); g_prevTerminateHandler = std::set_terminate(onTerminate); } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index ea8a0efe..41958b80 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -5084,7 +5084,7 @@ void MainWindow::on_actionSettings_triggered() bool proxy = settings.network().useProxy(); DownloadManager *dlManager = m_OrganizerCore.downloadManager(); const bool oldCheckForUpdates = settings.checkForUpdates(); - const int oldMaxDumps = settings.diagnostics().crashDumpsMax(); + const int oldMaxDumps = settings.diagnostics().maxCoreDumps(); SettingsDialog dialog(&m_PluginContainer, settings, this); @@ -5171,7 +5171,7 @@ void MainWindow::on_actionSettings_triggered() m_OrganizerCore.setLogLevel(settings.diagnostics().logLevel()); - if (settings.diagnostics().crashDumpsMax() != oldMaxDumps) { + if (settings.diagnostics().maxCoreDumps() != oldMaxDumps) { m_OrganizerCore.cycleDiagnostics(); } diff --git a/src/moapplication.cpp b/src/moapplication.cpp index 24cafd05..bb7b4922 100644 --- a/src/moapplication.cpp +++ b/src/moapplication.cpp @@ -188,8 +188,6 @@ int MOApplication::doOneRun(SingleInstance& singleInstance) const QString dataPath = currentInstance->directory(); setProperty("dataPath", dataPath); - setExceptionHandlers(); - if (!setLogDirectory(dataPath)) { reportError("Failed to create log folder"); InstanceManager::singleton().clearCurrentInstance(); @@ -272,7 +270,7 @@ int MOApplication::runApplication( // global crashDumpType sits in OrganizerCore to make a bit less ugly to // update it when the settings are changed during runtime - OrganizerCore::setGlobalCrashDumpsType(settings.diagnostics().crashDumpsType()); + OrganizerCore::setGlobalCoreDumpType(settings.diagnostics().coreDumpType()); env::Environment env; env.dump(settings); diff --git a/src/organizercore.cpp b/src/organizercore.cpp index d01aab96..f57903e2 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -76,8 +76,7 @@ using namespace MOShared; using namespace MOBase; -//static -CrashDumpsType OrganizerCore::m_globalCrashDumpsType = CrashDumpsType::None; +static env::CoreDumpTypes g_coreDumpType = env::CoreDumpTypes::Mini; template QStringList toStringList(InputIterator current, InputIterator end) @@ -478,7 +477,7 @@ bool OrganizerCore::bootstrap() m_Settings.paths().mods(), m_Settings.paths().downloads(), m_Settings.paths().overwrite(), - QString::fromStdWString(crashDumpsPath()) + QString::fromStdWString(getGlobalCoreDumpPath()) }; for (auto&& dir : dirs) { @@ -497,14 +496,14 @@ bool OrganizerCore::bootstrap() // log if there are any dmp files const auto hasCrashDumps = - !QDir(QString::fromStdWString(crashDumpsPath())) + !QDir(QString::fromStdWString(getGlobalCoreDumpPath())) .entryList({"*.dmp"}, QDir::Files) .empty(); if (hasCrashDumps) { log::debug( "there are crash dumps in '{}'", - QString::fromStdWString(crashDumpsPath())); + QString::fromStdWString(getGlobalCoreDumpPath())); } return true; @@ -528,13 +527,14 @@ void OrganizerCore::prepareVFS() } void OrganizerCore::updateVFSParams( - log::Levels logLevel, CrashDumpsType crashDumpsType, + log::Levels logLevel, env::CoreDumpTypes coreDumpType, const QString& crashDumpsPath, std::chrono::seconds spawnDelay, QString executableBlacklist) { - setGlobalCrashDumpsType(crashDumpsType); + setGlobalCoreDumpType(coreDumpType); + m_USVFS.updateParams( - logLevel, crashDumpsType, crashDumpsPath, spawnDelay, executableBlacklist); + logLevel, coreDumpType, crashDumpsPath, spawnDelay, executableBlacklist); } void OrganizerCore::setLogLevel(log::Levels level) @@ -543,8 +543,8 @@ void OrganizerCore::setLogLevel(log::Levels level) updateVFSParams( m_Settings.diagnostics().logLevel(), - m_Settings.diagnostics().crashDumpsType(), - QString::fromStdWString(crashDumpsPath()), + m_Settings.diagnostics().coreDumpType(), + QString::fromStdWString(getGlobalCoreDumpPath()), m_Settings.diagnostics().spawnDelay(), m_Settings.executablesBlacklist()); @@ -553,8 +553,8 @@ void OrganizerCore::setLogLevel(log::Levels level) bool OrganizerCore::cycleDiagnostics() { - const auto maxDumps = settings().diagnostics().crashDumpsMax(); - const auto path = QString::fromStdWString(crashDumpsPath()); + const auto maxDumps = settings().diagnostics().maxCoreDumps(); + const auto path = QString::fromStdWString(getGlobalCoreDumpPath()); if (maxDumps > 0) { removeOldFiles(path, "*.dmp", maxDumps, QDir::Time|QDir::Reversed); @@ -563,16 +563,26 @@ bool OrganizerCore::cycleDiagnostics() return true; } -void OrganizerCore::setGlobalCrashDumpsType(CrashDumpsType type) +env::CoreDumpTypes OrganizerCore::getGlobalCoreDumpType() +{ + return g_coreDumpType; +} + +void OrganizerCore::setGlobalCoreDumpType(env::CoreDumpTypes type) { - m_globalCrashDumpsType = type; + g_coreDumpType = type; } -std::wstring OrganizerCore::crashDumpsPath() { - return ( - qApp->property("dataPath").toString() + "/" - + QString::fromStdWString(AppConfig::dumpsDir()) - ).toStdWString(); +std::wstring OrganizerCore::getGlobalCoreDumpPath() +{ + if (qApp) { + const auto dp = qApp->property("dataPath"); + if (!dp.isNull()) { + return dp.toString().toStdWString() + L"/" + AppConfig::dumpsDir(); + } + } + + return {}; } void OrganizerCore::setCurrentProfile(const QString &profileName) diff --git a/src/organizercore.h b/src/organizercore.h index 80b89a43..b566e626 100644 --- a/src/organizercore.h +++ b/src/organizercore.h @@ -13,6 +13,7 @@ #include "moshortcut.h" #include "processrunner.h" #include "uilocker.h" +#include "envdump.h" #include #include #include @@ -277,17 +278,17 @@ public: void prepareVFS(); void updateVFSParams( - MOBase::log::Levels logLevel, CrashDumpsType crashDumpsType, - const QString& crashDumpsPath, std::chrono::seconds spawnDelay, + MOBase::log::Levels logLevel, env::CoreDumpTypes coreDumpType, + const QString& coreDumpsPath, std::chrono::seconds spawnDelay, QString executableBlacklist); void setLogLevel(MOBase::log::Levels level); bool cycleDiagnostics(); - static CrashDumpsType getGlobalCrashDumpsType() { return m_globalCrashDumpsType; } - static void setGlobalCrashDumpsType(CrashDumpsType crashDumpsType); - static std::wstring crashDumpsPath(); + static env::CoreDumpTypes getGlobalCoreDumpType(); + static void setGlobalCoreDumpType(env::CoreDumpTypes type); + static std::wstring getGlobalCoreDumpPath(); /** * @brief Returns the name of all the mods in the priority order of the given profile. @@ -487,8 +488,6 @@ private: UsvfsConnector m_USVFS; UILocker m_UILocker; - - static CrashDumpsType m_globalCrashDumpsType; }; #endif // ORGANIZERCORE_H diff --git a/src/settings.cpp b/src/settings.cpp index 99b41e1f..a8ada6ba 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -2110,23 +2110,23 @@ void DiagnosticsSettings::setLootLogLevel(lootcli::LogLevels level) set(m_Settings, "Settings", "loot_log_level", level); } -CrashDumpsType DiagnosticsSettings::crashDumpsType() const +env::CoreDumpTypes DiagnosticsSettings::coreDumpType() const { - return get(m_Settings, - "Settings", "crash_dumps_type", CrashDumpsType::Mini); + return get(m_Settings, + "Settings", "crash_dumps_type", env::CoreDumpTypes::Mini); } -void DiagnosticsSettings::setCrashDumpsType(CrashDumpsType type) +void DiagnosticsSettings::setCoreDumpType(env::CoreDumpTypes type) { set(m_Settings, "Settings", "crash_dumps_type", type); } -int DiagnosticsSettings::crashDumpsMax() const +int DiagnosticsSettings::maxCoreDumps() const { return get(m_Settings, "Settings", "crash_dumps_max", 5); } -void DiagnosticsSettings::setCrashDumpsMax(int n) +void DiagnosticsSettings::setMaxCoreDumps(int n) { set(m_Settings, "Settings", "crash_dumps_max", n); } diff --git a/src/settings.h b/src/settings.h index fc7789f0..df081c5c 100644 --- a/src/settings.h +++ b/src/settings.h @@ -21,6 +21,7 @@ along with Mod Organizer. If not, see . #define SETTINGS_H #include "loadmechanism.h" +#include "envdump.h" #include #include #include @@ -651,13 +652,13 @@ public: // crash dump type for both MO and usvfs // - CrashDumpsType crashDumpsType() const; - void setCrashDumpsType(CrashDumpsType type); + env::CoreDumpTypes coreDumpType() const; + void setCoreDumpType(env::CoreDumpTypes type); // maximum number of dump files keps, for both MO and usvfs // - int crashDumpsMax() const; - void setCrashDumpsMax(int n); + int maxCoreDumps() const; + void setMaxCoreDumps(int n); std::chrono::seconds spawnDelay() const; void setSpawnDelay(std::chrono::seconds t); diff --git a/src/settingsdialogdiagnostics.cpp b/src/settingsdialogdiagnostics.cpp index 4ade6900..33175a66 100644 --- a/src/settingsdialogdiagnostics.cpp +++ b/src/settingsdialogdiagnostics.cpp @@ -13,7 +13,7 @@ DiagnosticsSettingsTab::DiagnosticsSettingsTab(Settings& s, SettingsDialog& d) setLootLogLevel(); setCrashDumpTypesBox(); - ui->dumpsMaxEdit->setValue(settings().diagnostics().crashDumpsMax()); + ui->dumpsMaxEdit->setValue(settings().diagnostics().maxCoreDumps()); QString logsPath = qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath()); @@ -22,7 +22,7 @@ DiagnosticsSettingsTab::DiagnosticsSettingsTab(Settings& s, SettingsDialog& d) ui->diagnosticsExplainedLabel->text() .replace("LOGS_FULL_PATH", logsPath) .replace("LOGS_DIR", QString::fromStdWString(AppConfig::logPath())) - .replace("DUMPS_FULL_PATH", QString::fromStdWString(OrganizerCore::crashDumpsPath())) + .replace("DUMPS_FULL_PATH", QString::fromStdWString(OrganizerCore::getGlobalCoreDumpPath())) .replace("DUMPS_DIR", QString::fromStdWString(AppConfig::dumpsDir())) ); } @@ -78,13 +78,13 @@ void DiagnosticsSettingsTab::setCrashDumpTypesBox() ui->dumpsTypeBox->addItem(text, static_cast(type)); }; - add(QObject::tr("None"), CrashDumpsType::None); - add(QObject::tr("Mini (recommended)"), CrashDumpsType::Mini); - add(QObject::tr("Data"), CrashDumpsType::Data); - add(QObject::tr("Full"), CrashDumpsType::Full); + add(QObject::tr("None"), env::CoreDumpTypes::None); + add(QObject::tr("Mini (recommended)"), env::CoreDumpTypes::Mini); + add(QObject::tr("Data"), env::CoreDumpTypes::Data); + add(QObject::tr("Full"), env::CoreDumpTypes::Full); const auto current = static_cast( - settings().diagnostics().crashDumpsType()); + settings().diagnostics().coreDumpType()); for (int i=0; idumpsTypeBox->count(); ++i) { if (ui->dumpsTypeBox->itemData(i) == current) { @@ -99,10 +99,10 @@ void DiagnosticsSettingsTab::update() settings().diagnostics().setLogLevel( static_cast(ui->logLevelBox->currentData().toInt())); - settings().diagnostics().setCrashDumpsType( - static_cast(ui->dumpsTypeBox->currentData().toInt())); + settings().diagnostics().setCoreDumpType( + static_cast(ui->dumpsTypeBox->currentData().toInt())); - settings().diagnostics().setCrashDumpsMax(ui->dumpsMaxEdit->value()); + settings().diagnostics().setMaxCoreDumps(ui->dumpsMaxEdit->value()); settings().diagnostics().setLootLogLevel( static_cast(ui->lootLogLevel->currentData().toInt())); diff --git a/src/usvfsconnector.cpp b/src/usvfsconnector.cpp index ed6e31ce..c8f2f1c4 100644 --- a/src/usvfsconnector.cpp +++ b/src/usvfsconnector.cpp @@ -107,17 +107,22 @@ LogLevel toUsvfsLogLevel(log::Levels level) } } -CrashDumpsType crashDumpsType(int type) +CrashDumpsType toUsvfsCrashDumpsType(env::CoreDumpTypes type) { - switch (static_cast(type)) { - case CrashDumpsType::Mini: - return CrashDumpsType::Mini; - case CrashDumpsType::Data: - return CrashDumpsType::Data; - case CrashDumpsType::Full: - return CrashDumpsType::Full; - default: - return CrashDumpsType::None; + switch (type) + { + case env::CoreDumpTypes::None: + return CrashDumpsType::None; + + case env::CoreDumpTypes::Data: + return CrashDumpsType::Data; + + case env::CoreDumpTypes::Full: + return CrashDumpsType::Full; + + case env::CoreDumpTypes::Mini: + default: + return CrashDumpsType::Mini; } } @@ -128,9 +133,9 @@ UsvfsConnector::UsvfsConnector() const auto& s = Settings::instance(); const LogLevel logLevel = toUsvfsLogLevel(s.diagnostics().logLevel()); - const CrashDumpsType dumpType = s.diagnostics().crashDumpsType(); + const auto dumpType = toUsvfsCrashDumpsType(s.diagnostics().coreDumpType()); const auto delay = duration_cast(s.diagnostics().spawnDelay()); - std::string dumpPath = MOShared::ToString(OrganizerCore::crashDumpsPath(), true); + std::string dumpPath = MOShared::ToString(OrganizerCore::getGlobalCoreDumpPath(), true); usvfsParameters* params = usvfsCreateParameters(); @@ -224,7 +229,7 @@ void UsvfsConnector::updateMapping(const MappingType &mapping) } void UsvfsConnector::updateParams( - MOBase::log::Levels logLevel, CrashDumpsType crashDumpsType, + MOBase::log::Levels logLevel, env::CoreDumpTypes coreDumpType, const QString& crashDumpsPath, std::chrono::seconds spawnDelay, QString executableBlacklist) { @@ -234,7 +239,7 @@ void UsvfsConnector::updateParams( usvfsSetDebugMode(p, FALSE); usvfsSetLogLevel(p, toUsvfsLogLevel(logLevel)); - usvfsSetCrashDumpType(p, crashDumpsType); + usvfsSetCrashDumpType(p, toUsvfsCrashDumpsType(coreDumpType)); usvfsSetCrashDumpPath(p, crashDumpsPath.toStdString().c_str()); usvfsSetProcessDelay(p, duration_cast(spawnDelay).count()); diff --git a/src/usvfsconnector.h b/src/usvfsconnector.h index 5982778b..bf5d49ce 100644 --- a/src/usvfsconnector.h +++ b/src/usvfsconnector.h @@ -31,6 +31,7 @@ along with Mod Organizer. If not, see . #include #include #include "executableinfo.h" +#include "envdump.h" class LogWorker : public QThread { @@ -87,7 +88,7 @@ public: void updateMapping(const MappingType &mapping); void updateParams( - MOBase::log::Levels logLevel, CrashDumpsType crashDumpsType, + MOBase::log::Levels logLevel, env::CoreDumpTypes coreDumpType, const QString& crashDumpsPath, std::chrono::seconds spawnDelay, QString executableBlacklist); -- cgit v1.3.1 From dd5367f488de7cd312e53705ffc970a723951ca2 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 7 Nov 2020 19:30:54 -0500 Subject: fixed AppConfig::logFileName so it can be used refactored MOApplication so everything is in doOneRun() --- src/loglist.cpp | 5 +- src/moapplication.cpp | 335 ++++++++++++++++++++++++----------------------- src/moapplication.h | 7 +- src/shared/appconfig.inc | 2 +- 4 files changed, 179 insertions(+), 170 deletions(-) (limited to 'src') diff --git a/src/loglist.cpp b/src/loglist.cpp index fad4678c..8df21111 100644 --- a/src/loglist.cpp +++ b/src/loglist.cpp @@ -345,7 +345,10 @@ bool createAndMakeWritable(const std::wstring &subPath) { bool setLogDirectory(const QString& dir) { - const auto logFile = dir + "/logs/mo_interface.log"; + const auto logFile = + dir + "/" + + QString::fromStdWString(AppConfig::logPath()) + "/" + + QString::fromStdWString(AppConfig::logFileName()); if (!createAndMakeWritable(AppConfig::logPath())) { return false; diff --git a/src/moapplication.cpp b/src/moapplication.cpp index bb7b4922..885abc37 100644 --- a/src/moapplication.cpp +++ b/src/moapplication.cpp @@ -164,27 +164,36 @@ int MOApplication::run(SingleInstance& singleInstance) // when switching instances or changing some settings for (;;) { - // resets things when MO is "restarted" - resetForRestart(); + try + { + // resets things when MO is "restarted" + resetForRestart(); - const auto r = doOneRun(singleInstance); - if (r == RestartExitCode) { - continue; - } + const auto r = doOneRun(singleInstance); + if (r == RestartExitCode) { + continue; + } - return r; + return r; + } + catch (const std::exception &e) + { + reportError(e.what()); + return 1; + } } } int MOApplication::doOneRun(SingleInstance& singleInstance) { - TimeThis tt("doOneRun() to runApplication()"); - + // figuring out the current instance auto currentInstance = getCurrentInstance(); if (!currentInstance) { return 1; } + // first time the data path is available, set the global property and log + // directory, then log a bunch of debug stuff const QString dataPath = currentInstance->directory(); setProperty("dataPath", dataPath); @@ -196,57 +205,20 @@ int MOApplication::doOneRun(SingleInstance& singleInstance) log::debug("command line: '{}'", QString::fromWCharArray(GetCommandLineW())); - tt.stop(); - - return runApplication(singleInstance, dataPath, *currentInstance); -} - -std::optional MOApplication::getCurrentInstance() -{ - auto& m = InstanceManager::singleton(); - auto currentInstance = m.currentInstance(); - - if (!currentInstance) - { - currentInstance = selectInstance(); - } - else - { - if (!QDir(currentInstance->directory()).exists()) { - // the previously used instance doesn't exist anymore - - if (m.hasAnyInstances()) { - MOShared::criticalOnTop(QObject::tr( - "Instance at '%1' not found. Select another instance.") - .arg(currentInstance->directory())); - } else { - MOShared::criticalOnTop(QObject::tr( - "Instance at '%1' not found. You must create a new instance") - .arg(currentInstance->directory())); - } - - currentInstance = selectInstance(); - } - } - - return currentInstance; -} - -int MOApplication::runApplication( - SingleInstance& singleInstance, - const QString &dataPath, Instance& currentInstance) -{ - TimeThis tt("runApplication() to exec()"); - log::info( "starting Mod Organizer version {} revision {} in {}, usvfs: {}", createVersionInfo().displayString(3), GITID, QCoreApplication::applicationDirPath(), MOShared::getUsvfsVersionString()); - log::info("data path: {}", dataPath); + if (singleInstance.secondary()) { + log::debug("another instance of MO is running but --multiple was given"); + } + log::info("data path: {}", currentInstance->directory()); log::info("working directory: {}", QDir::currentPath()); + + // deleting old files, only for the main instance if (!singleInstance.secondary()) { purgeOldFiles(); } @@ -254,155 +226,192 @@ int MOApplication::runApplication( QWindowsWindowFunctions::setWindowActivationBehavior( QWindowsWindowFunctions::AlwaysActivateWindow); - try - { - Settings settings( - dataPath + "/" + QString::fromStdWString(AppConfig::iniFileName()), - true); - log::getDefault().setLevel(settings.diagnostics().logLevel()); + // loading settings + Settings settings(currentInstance->iniPath(), true); + log::getDefault().setLevel(settings.diagnostics().logLevel()); + log::debug("using ini at '{}'", settings.filename()); - log::debug("using ini at '{}'", settings.filename()); + OrganizerCore::setGlobalCoreDumpType(settings.diagnostics().coreDumpType()); - if (singleInstance.secondary()) { - log::debug("another instance of MO is running but --multiple was given"); - } - // global crashDumpType sits in OrganizerCore to make a bit less ugly to - // update it when the settings are changed during runtime - OrganizerCore::setGlobalCoreDumpType(settings.diagnostics().coreDumpType()); + // logging and checking + env::Environment env; + env.dump(settings); + settings.dump(); + sanity::checkEnvironment(env); - env::Environment env; - env.dump(settings); - settings.dump(); - sanity::checkEnvironment(env); + const auto moduleNotification = env.onModuleLoaded(qApp, [](auto&& m) { + log::debug("loaded module {}", m.toString()); + sanity::checkIncompatibleModule(m); + }); - const auto moduleNotification = env.onModuleLoaded(qApp, [](auto&& m) { - log::debug("loaded module {}", m.toString()); - sanity::checkIncompatibleModule(m); - }); - // this must outlive `organizer` - std::unique_ptr pluginContainer; + // this must outlive `organizer` + std::unique_ptr pluginContainer; - log::debug("initializing nexus interface"); - NexusInterface ni(&settings); + // nexus interface + log::debug("initializing nexus interface"); + NexusInterface ni(&settings); - log::debug("initializing core"); - OrganizerCore organizer(settings); - if (!organizer.bootstrap()) { - reportError("failed to set up data paths"); - InstanceManager::singleton().clearCurrentInstance(); - return 1; - } + // organizer core + log::debug("initializing core"); + OrganizerCore organizer(settings); + if (!organizer.bootstrap()) { + reportError("failed to set up data paths"); + InstanceManager::singleton().clearCurrentInstance(); + return 1; + } - log::debug("initializing plugins"); - pluginContainer = std::make_unique(&organizer); - pluginContainer->loadPlugins(); + // plugins + log::debug("initializing plugins"); + pluginContainer = std::make_unique(&organizer); + pluginContainer->loadPlugins(); - for (;;) - { - const auto setupResult = setupInstance(currentInstance, *pluginContainer); + // instance + if (auto r=setupInstanceLoop(*currentInstance, *pluginContainer)) { + return *r; + } - if (setupResult == SetupInstanceResults::Okay) { - break; - } else if (setupResult == SetupInstanceResults::TryAgain) { - continue; - } else if (setupResult == SetupInstanceResults::SelectAnother) { - InstanceManager::singleton().clearCurrentInstance(); - return RestartExitCode; - } else { - return 1; - } - } + if (currentInstance->isPortable()) { + log::debug("this is a portable instance"); + } - if (currentInstance.isPortable()) { - log::debug("this is a portable instance"); - } + sanity::checkPaths(*currentInstance->gamePlugin(), settings); - sanity::checkPaths(*currentInstance.gamePlugin(), settings); + // setting up organizer core + organizer.setManagedGame(currentInstance->gamePlugin()); + organizer.createDefaultProfile(); - organizer.setManagedGame(currentInstance.gamePlugin()); - organizer.createDefaultProfile(); + log::info( + "using game plugin '{}' ('{}', variant {}, steam id '{}') at {}", + currentInstance->gamePlugin()->gameName(), + currentInstance->gamePlugin()->gameShortName(), + (settings.game().edition().value_or("").isEmpty() ? + "(none)" : *settings.game().edition()), + currentInstance->gamePlugin()->steamAPPId(), + currentInstance->gamePlugin()->gameDirectory().absolutePath()); + + CategoryFactory::instance().loadCategories(); + organizer.updateExecutablesList(); + organizer.updateModInfoFromDisc(); + organizer.setCurrentProfile(currentInstance->profileName()); + + // checking command line + if (auto r=m_cl.setupCore(organizer)) { + return *r; + } - log::info( - "using game plugin '{}' ('{}', variant {}, steam id '{}') at {}", - currentInstance.gamePlugin()->gameName(), - currentInstance.gamePlugin()->gameShortName(), - (settings.game().edition().value_or("").isEmpty() ? - "(none)" : *settings.game().edition()), - currentInstance.gamePlugin()->steamAPPId(), - currentInstance.gamePlugin()->gameDirectory().absolutePath()); + // show splash + MOSplash splash( + settings, currentInstance->directory(), currentInstance->gamePlugin()); + // start an api check + QString apiKey; + if (GlobalSettings::nexusApiKey(apiKey)) { + ni.getAccessManager()->apiCheck(apiKey); + } - CategoryFactory::instance().loadCategories(); - organizer.updateExecutablesList(); - organizer.updateModInfoFromDisc(); + // tutorials + log::debug("initializing tutorials"); + TutorialManager::init( + qApp->applicationDirPath() + "/" + + QString::fromStdWString(AppConfig::tutorialsPath()) + "/", + &organizer); + + // styling + if (!setStyleFile(settings.interface().styleName().value_or(""))) { + // disable invalid stylesheet + settings.interface().setStyleName(""); + } - organizer.setCurrentProfile(currentInstance.profileName()); - if (auto r=m_cl.setupCore(organizer)) { - return *r; - } + int res = 1; + + { + MainWindow mainWindow(settings, organizer, *pluginContainer); - MOSplash splash(settings, dataPath, currentInstance.gamePlugin()); + // qt resets the thread name somewhere when creating the main window + MOShared::SetThisThreadName("main"); - QString apiKey; - if (GlobalSettings::nexusApiKey(apiKey)) { - ni.getAccessManager()->apiCheck(apiKey); - } + // the nexus interface can show dialogs, make sure they're parented to the + // main window + ni.getAccessManager()->setTopLevelWidget(&mainWindow); - log::debug("initializing tutorials"); - TutorialManager::init( - qApp->applicationDirPath() + "/" - + QString::fromStdWString(AppConfig::tutorialsPath()) + "/", - &organizer); + QObject::connect(&mainWindow, SIGNAL(styleChanged(QString)), this, + SLOT(setStyleFile(QString))); - if (!setStyleFile(settings.interface().styleName().value_or(""))) { - // disable invalid stylesheet - settings.interface().setStyleName(""); - } + QObject::connect(&singleInstance, SIGNAL(messageSent(QString)), &organizer, + SLOT(externalMessage(QString))); - int res = 1; - { - // scope to control lifetime of mainwindow - // set up main window and its data structures - MainWindow mainWindow(settings, organizer, *pluginContainer); + log::debug("displaying main window"); + mainWindow.show(); + mainWindow.activateWindow(); + splash.close(); - // qt resets the thread name somewhere when creating the main window - MOShared::SetThisThreadName("main"); + res = exec(); + mainWindow.close(); - ni.getAccessManager()->setTopLevelWidget(&mainWindow); + // main window is about to be destroyed + ni.getAccessManager()->setTopLevelWidget(nullptr); + } - QObject::connect(&mainWindow, SIGNAL(styleChanged(QString)), this, - SLOT(setStyleFile(QString))); - QObject::connect(&singleInstance, SIGNAL(messageSent(QString)), &organizer, - SLOT(externalMessage(QString))); + // reset geometry if the flag was set from the settings dialog + settings.geometry().resetIfNeeded(); - log::debug("displaying main window"); - mainWindow.show(); - mainWindow.activateWindow(); + return res; +} - splash.close(); +std::optional MOApplication::getCurrentInstance() +{ + auto& m = InstanceManager::singleton(); + auto currentInstance = m.currentInstance(); - tt.stop(); + if (!currentInstance) + { + currentInstance = selectInstance(); + } + else + { + if (!QDir(currentInstance->directory()).exists()) { + // the previously used instance doesn't exist anymore - res = exec(); - mainWindow.close(); + if (m.hasAnyInstances()) { + MOShared::criticalOnTop(QObject::tr( + "Instance at '%1' not found. Select another instance.") + .arg(currentInstance->directory())); + } else { + MOShared::criticalOnTop(QObject::tr( + "Instance at '%1' not found. You must create a new instance") + .arg(currentInstance->directory())); + } - ni.getAccessManager()->setTopLevelWidget(nullptr); + currentInstance = selectInstance(); } - - settings.geometry().resetIfNeeded(); - return res; } - catch (const std::exception &e) + + return currentInstance; +} + +std::optional MOApplication::setupInstanceLoop( + Instance& currentInstance, PluginContainer& pc) +{ + for (;;) { - reportError(e.what()); - } + const auto setupResult = setupInstance(currentInstance, pc); - return 1; + if (setupResult == SetupInstanceResults::Okay) { + return {}; + } else if (setupResult == SetupInstanceResults::TryAgain) { + continue; + } else if (setupResult == SetupInstanceResults::SelectAnother) { + InstanceManager::singleton().clearCurrentInstance(); + return RestartExitCode; + } else { + return 1; + } + } } void MOApplication::purgeOldFiles() diff --git a/src/moapplication.h b/src/moapplication.h index 8cbb5f28..7423c897 100644 --- a/src/moapplication.h +++ b/src/moapplication.h @@ -26,6 +26,7 @@ along with Mod Organizer. If not, see . class Settings; class SingleInstance; class Instance; +class PluginContainer; namespace MOBase { class IPluginGame; } namespace cl { class CommandLine; } @@ -57,11 +58,7 @@ private: int doOneRun(SingleInstance& singleInstance); std::optional getCurrentInstance(); - - int runApplication( - SingleInstance& singleInstance, - const QString &dataPath, Instance& currentInstance); - + std::optional setupInstanceLoop(Instance& currentInstance, PluginContainer& pc); void purgeOldFiles(); void resetForRestart(); }; diff --git a/src/shared/appconfig.inc b/src/shared/appconfig.inc index 807f1d69..5839c2f4 100644 --- a/src/shared/appconfig.inc +++ b/src/shared/appconfig.inc @@ -11,7 +11,7 @@ APPPARAM(std::wstring, logPath, L"logs") APPPARAM(std::wstring, dumpsDir, L"crashDumps") APPPARAM(std::wstring, defaultProfileName, L"Default") APPPARAM(std::wstring, profileTweakIni, L"profile_tweaks.ini") -APPPARAM(std::wstring, logFileName, L"ModOrganizer.log") +APPPARAM(std::wstring, logFileName, L"mo_interface.log") APPPARAM(std::wstring, iniFileName, L"ModOrganizer.ini") APPPARAM(std::wstring, proxyDLLTarget, L"steam_api.dll") APPPARAM(std::wstring, proxyDLLOrig, L"steam_api_orig.dll") // needs to be identical to the value used in proxydll-project -- cgit v1.3.1 From 0b8fbfdc57ad22c996fa88f6d974269afec004fd Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 7 Nov 2020 19:43:11 -0500 Subject: larger initial size for the instance dialog save geometry if settings are available --- src/instancemanagerdialog.cpp | 28 +++++++++++++++++++++++++++- src/instancemanagerdialog.h | 10 ++++++++++ src/instancemanagerdialog.ui | 4 ++-- src/settings.cpp | 8 +++++++- src/settings.h | 10 +++++++++- 5 files changed, 55 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/instancemanagerdialog.cpp b/src/instancemanagerdialog.cpp index 71fed78d..d579ea9c 100644 --- a/src/instancemanagerdialog.cpp +++ b/src/instancemanagerdialog.cpp @@ -111,7 +111,7 @@ InstanceManagerDialog::InstanceManagerDialog( { ui->setupUi(this); - ui->splitter->setSizes({200, 1}); + ui->splitter->setSizes({250, 1}); ui->splitter->setStretchFactor(0, 0); ui->splitter->setStretchFactor(1, 1); @@ -146,6 +146,32 @@ InstanceManagerDialog::InstanceManagerDialog( connect(ui->close, &QPushButton::clicked, [&]{ close(); }); } +void InstanceManagerDialog::showEvent(QShowEvent* e) +{ + // there might not be a global Settings object if this is called on startup + // when there's no current instance + const auto* s = Settings::maybeInstance(); + + if (s) { + s->geometry().restoreGeometry(this); + } + + QDialog::showEvent(e); +} + +void InstanceManagerDialog::done(int r) +{ + // there might not be a global Settings object if this is called on startup + // when there's no current instance + auto* s = Settings::maybeInstance(); + + if (s) { + s->geometry().saveGeometry(this); + } + + QDialog::done(r); +} + void InstanceManagerDialog::updateInstances() { auto& m = InstanceManager::singleton(); diff --git a/src/instancemanagerdialog.h b/src/instancemanagerdialog.h index 94f379ca..2641f716 100644 --- a/src/instancemanagerdialog.h +++ b/src/instancemanagerdialog.h @@ -78,6 +78,16 @@ public: // void setRestartOnSelect(bool b); + + // saves geometry + // + void done(int r) override; + +protected: + // restores geometry + // + void showEvent(QShowEvent* e) override; + private: static const std::size_t NoSelection = -1; diff --git a/src/instancemanagerdialog.ui b/src/instancemanagerdialog.ui index 45b99e21..4137d1b6 100644 --- a/src/instancemanagerdialog.ui +++ b/src/instancemanagerdialog.ui @@ -6,8 +6,8 @@ 0 0 - 689 - 413 + 884 + 539 diff --git a/src/settings.cpp b/src/settings.cpp index a8ada6ba..54d32786 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -86,14 +86,20 @@ Settings::~Settings() } } -Settings &Settings::instance() +Settings& Settings::instance() { if (s_Instance == nullptr) { throw std::runtime_error("no instance of \"Settings\""); } + return *s_Instance; } +Settings* Settings::maybeInstance() +{ + return s_Instance; +} + void Settings::processUpdates( const QVersionNumber& currentVersion, const QVersionNumber& lastVersion) { diff --git a/src/settings.h b/src/settings.h index df081c5c..689067d7 100644 --- a/src/settings.h +++ b/src/settings.h @@ -693,7 +693,15 @@ public: Settings(const QString& path, bool globalInstance=false); ~Settings(); - static Settings &instance(); + + // throws if there is no global Settings instance + // + static Settings& instance(); + + // returns null if there is no global Settings instance + // + static Settings* maybeInstance(); + // name of the ini file // -- cgit v1.3.1 From a28aeb2b1871b5626dedfaedafcdb3102eaeea5d Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 7 Nov 2020 19:53:13 -0500 Subject: implemented browse buttons --- src/createinstancedialogpages.cpp | 34 +++++++++++++++++++++++++++------- src/createinstancedialogpages.h | 4 ++++ 2 files changed, 31 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/createinstancedialogpages.cpp b/src/createinstancedialogpages.cpp index 0f39f6c1..49b118f7 100644 --- a/src/createinstancedialogpages.cpp +++ b/src/createinstancedialogpages.cpp @@ -863,12 +863,23 @@ PathsPage::PathsPage(CreateInstanceDialog& dlg) : m_advancedInvalid(ui->advancedDirInvalid), m_okay(false) { - QObject::connect(ui->location, &QLineEdit::textEdited, [&]{ onChanged(); }); - QObject::connect(ui->base, &QLineEdit::textEdited, [&]{ onChanged(); }); - QObject::connect(ui->downloads, &QLineEdit::textEdited, [&]{ onChanged(); }); - QObject::connect(ui->mods, &QLineEdit::textEdited, [&]{ onChanged(); }); - QObject::connect(ui->profiles, &QLineEdit::textEdited, [&]{ onChanged(); }); - QObject::connect(ui->overwrite, &QLineEdit::textEdited, [&]{ onChanged(); }); + using O = QObject; + using E = QLineEdit; + using B = QAbstractButton; + + O::connect(ui->location, &E::textEdited, [&]{ onChanged(); }); + O::connect(ui->base, &E::textEdited, [&]{ onChanged(); }); + O::connect(ui->downloads, &E::textEdited, [&]{ onChanged(); }); + O::connect(ui->mods, &E::textEdited, [&]{ onChanged(); }); + O::connect(ui->profiles, &E::textEdited, [&]{ onChanged(); }); + O::connect(ui->overwrite, &E::textEdited, [&]{ onChanged(); }); + + O::connect(ui->browseLocation, &B::clicked, [&]{ browse(ui->location); }); + O::connect(ui->browseBase, &B::clicked, [&]{ browse(ui->base); }); + O::connect(ui->browseDownloads, &B::clicked, [&]{ browse(ui->downloads); }); + O::connect(ui->browseMods, &B::clicked, [&]{ browse(ui->mods); }); + O::connect(ui->browseProfiles, &B::clicked, [&]{ browse(ui->profiles); }); + O::connect(ui->browseOverwrite, &B::clicked, [&]{ browse(ui->overwrite); }); QObject::connect( ui->advancedPathOptions, &QCheckBox::clicked, [&]{ onAdvanced(); }); @@ -876,7 +887,6 @@ PathsPage::PathsPage(CreateInstanceDialog& dlg) : ui->pathPages->setCurrentIndex(0); } - bool PathsPage::ready() const { // set when the page is activated, textboxes are changed or the advanced @@ -928,6 +938,16 @@ void PathsPage::onChanged() updateNavigation(); } +void PathsPage::browse(QLineEdit* e) +{ + const auto s = QFileDialog::getExistingDirectory(&m_dlg, {}, e->text()); + if (s.isNull() || s.isEmpty()) { + return; + } + + e->setText(QDir::toNativeSeparators(s)); +} + void PathsPage::checkPaths() { if (ui->advancedPathOptions->isChecked()) { diff --git a/src/createinstancedialogpages.h b/src/createinstancedialogpages.h index e2eaf0fb..4fde089b 100644 --- a/src/createinstancedialogpages.h +++ b/src/createinstancedialogpages.h @@ -487,6 +487,10 @@ private: // void onChanged(); + // opens a browse directory dialog and sets the given textbox + // + void browse(QLineEdit* e); + // checks the simple or advanced paths, sets m_okay // void checkPaths(); -- cgit v1.3.1 From f9feb5fc5b4dc2834827862131035c2c8894f879 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 7 Nov 2020 20:09:16 -0500 Subject: show a confirmation before switching instances --- src/instancemanagerdialog.cpp | 42 ++++++++++++++++++++++++++++++++++++++++-- src/instancemanagerdialog.h | 5 ++++- 2 files changed, 44 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/instancemanagerdialog.cpp b/src/instancemanagerdialog.cpp index d579ea9c..b461d39d 100644 --- a/src/instancemanagerdialog.cpp +++ b/src/instancemanagerdialog.cpp @@ -294,10 +294,16 @@ void InstanceManagerDialog::openSelectedInstance() return; } - if (m_instances[i]->isPortable()) { + const auto& to = *m_instances[i]; + + if (!confirmSwitch(to)) { + return; + } + + if (to.isPortable()) { InstanceManager::singleton().setCurrentInstance(""); } else { - InstanceManager::singleton().setCurrentInstance(m_instances[i]->name()); + InstanceManager::singleton().setCurrentInstance(to.name()); } if (m_restartOnSelect) { @@ -307,6 +313,38 @@ void InstanceManagerDialog::openSelectedInstance() accept(); } +bool InstanceManagerDialog::confirmSwitch(const Instance& to) +{ + // there might not be a global Settings object if this is called on startup + // when there's no current instance + const auto* s = Settings::maybeInstance(); + + // if there is are no settings, no instances are loaded and the confirmation + // wouldn't make sense + if (!s) { + return true; + } + + if (!s->interface().showChangeGameConfirmation()) { + // user disabled confirmation + return true; + } + + MOBase::TaskDialog dlg(this); + + const auto r = dlg + .title(tr("Switching instances")) + .main(tr("Mod Organizer must restart to manage the instance '%1'.") + .arg(to.name())) + .content(tr("This confirmation can be disabled in the settings.")) + .icon(QMessageBox::Question) + .button({tr("Restart Mod Organizer"), QMessageBox::Ok}) + .button({tr("Cancel"), QMessageBox::Cancel}) + .exec(); + + return (r == QMessageBox::Ok); +} + void InstanceManagerDialog::rename() { auto* i = singleSelection(); diff --git a/src/instancemanagerdialog.h b/src/instancemanagerdialog.h index 2641f716..86fe0dac 100644 --- a/src/instancemanagerdialog.h +++ b/src/instancemanagerdialog.h @@ -38,7 +38,6 @@ public: // void openSelectedInstance(); - // renames the currently selected instance // void rename(); @@ -110,6 +109,10 @@ private: // void createNew(); + // shows a confirmation to the user before switching + // + bool confirmSwitch(const Instance& to); + // returns the index of selected instance, NoSelection if none // -- cgit v1.3.1 From 2f817f36ef00971a03ea3e699c1455daf660cf5d Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 7 Nov 2020 20:55:01 -0500 Subject: task dialog for plugin loadcheck added option to skip plugin for this session only --- src/plugincontainer.cpp | 73 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 64 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp index 908677c2..dd4ccbb1 100644 --- a/src/plugincontainer.cpp +++ b/src/plugincontainer.cpp @@ -405,22 +405,58 @@ void PluginContainer::loadPlugins() } QFile loadCheck; + QString skipPlugin; if (m_Organizer) { loadCheck.setFileName(qApp->property("dataPath").toString() + "/plugin_loadcheck.tmp"); + if (loadCheck.exists() && loadCheck.open(QIODevice::ReadOnly)) { // oh, there was a failed plugin load last time. Find out which plugin was loaded last QString fileName; while (!loadCheck.atEnd()) { fileName = QString::fromUtf8(loadCheck.readLine().constData()).trimmed(); } - if (QMessageBox::question(nullptr, QObject::tr("Plugin error"), - QObject::tr("It appears the plugin \"%1\" failed to load last startup and caused MO to crash. Do you want to disable it?\n" - "(Please note: If this is the first time you see this message for this plugin you may want to give it another try. " - "The plugin may be able to recover from the problem)").arg(fileName), - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) == QMessageBox::Yes) { - m_Organizer->settings().plugins().addBlacklist(fileName); + + log::warn("loadcheck file found for plugin '{}'", fileName); + + MOBase::TaskDialog dlg; + + const auto Skip = QMessageBox::Ignore; + const auto Blacklist = QMessageBox::Cancel; + const auto Load = QMessageBox::Ok; + + const auto r = dlg + .title(tr("Plugin error")) + .main(tr( + "Mod Organizer failed to load the plugin '%1' last time it was started.") + .arg(fileName)) + .content(tr( + "The plugin can be skipped for this session, blacklisted, " + "or loaded normally, in which case it might fail again. Blacklisted " + "plugins can be re-enabled later in the settings.")) + .icon(QMessageBox::Warning) + .button({tr("Skip this plugin"), Skip}) + .button({tr("Blacklist this plugin"), Blacklist}) + .button({tr("Load this plugin"), Load}) + .exec(); + + switch (r) + { + case Skip: + log::warn("user wants to skip plugin '{}'", fileName); + skipPlugin = fileName; + break; + + case Blacklist: + log::warn("user wants to blacklist plugin '{}'", fileName); + m_Organizer->settings().plugins().addBlacklist(fileName); + break; + + case Load: + log::warn("user wants to load plugin '{}' anyway", fileName); + break; } + loadCheck.close(); } @@ -434,6 +470,11 @@ void PluginContainer::loadPlugins() while (iter.hasNext()) { iter.next(); + if (skipPlugin == iter.fileName()) { + log::debug("plugin \"{}\" skipped for this session", iter.fileName()); + continue; + } + if (m_Organizer) { if (m_Organizer->settings().plugins().blacklisted(iter.fileName())) { log::debug("plugin \"{}\" blacklisted", iter.fileName()); @@ -467,11 +508,25 @@ void PluginContainer::loadPlugins() } } - // remove the load check file on success - if (loadCheck.isOpen()) { - loadCheck.remove(); + if (skipPlugin.isEmpty()) { + // remove the load check file on success + if (loadCheck.isOpen()) { + loadCheck.remove(); + } + } else { + // remember the plugin for next time + if (loadCheck.isOpen()) { + loadCheck.close(); + } + + log::warn("user skipped plugin '{}', remembering in loadcheck", skipPlugin); + loadCheck.open(QIODevice::WriteOnly); + loadCheck.write(skipPlugin.toUtf8()); + loadCheck.write("\n"); + loadCheck.flush(); } + bf::at_key(m_Plugins).push_back(this); if (m_Organizer) { -- cgit v1.3.1 From a104f5751d3c39a06571faebec5401ebe25dc2a1 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 7 Nov 2020 21:48:04 -0500 Subject: changed wiki link to Instances --- src/createinstancedialog.ui | 2 +- src/instancemanagerdialog.ui | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/createinstancedialog.ui b/src/createinstancedialog.ui index b10d74a7..4d1eda1f 100644 --- a/src/createinstancedialog.ui +++ b/src/createinstancedialog.ui @@ -86,7 +86,7 @@ - <html><head/><body><p><a href="https://github.com/ModOrganizer2/modorganizer/wiki/Instance">More information</a></p></body></html> + <html><head/><body><p><a href="https://github.com/ModOrganizer2/modorganizer/wiki/Instances">More information</a></p></body></html> true diff --git a/src/instancemanagerdialog.ui b/src/instancemanagerdialog.ui index 4137d1b6..44f4ff1f 100644 --- a/src/instancemanagerdialog.ui +++ b/src/instancemanagerdialog.ui @@ -56,7 +56,7 @@ - <html><head/><body><p><a href="https://github.com/ModOrganizer2/modorganizer/wiki/Instance">What is an instance?</a></p></body></html> + <html><head/><body><p><a href="https://github.com/ModOrganizer2/modorganizer/wiki/Instances">What is an instance?</a></p></body></html> true -- cgit v1.3.1 From 346a6babdf6263568b7fe20b9019d3f4ca0d68f4 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 7 Nov 2020 21:51:47 -0500 Subject: renamed "change game" button to "manage instances" --- src/mainwindow.ui | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/mainwindow.ui b/src/mainwindow.ui index 466bd7bd..ace0dfeb 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -1768,16 +1768,16 @@ p, li { white-space: pre-wrap; } :/MO/gui/instance_switch:/MO/gui/instance_switch - &Change Game... + Manage Instan&ces... - &Change Game + Manage Instan&ces... - Open the Instance selection dialog to manage a different Game + Open the instance manager window to manage a different instance - Open the Instance selection dialog to manage a different Game + Open the instance manager window to manage a different instance @@ -1877,6 +1877,11 @@ p, li { white-space: pre-wrap; } + + LinkLabel + QLabel +
linklabel.h
+
MOBase::LineEditClear QLineEdit @@ -1917,11 +1922,6 @@ p, li { white-space: pre-wrap; } QStatusBar
statusbar.h
- - LinkLabel - QLabel -
linklabel.h
-
-- cgit v1.3.1 From 54bf7e1fa230c78e98f429559b51a6be42f29e4a Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sun, 8 Nov 2020 21:58:02 -0500 Subject: timings --- src/main.cpp | 4 ++++ src/moapplication.cpp | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) (limited to 'src') diff --git a/src/main.cpp b/src/main.cpp index ad074501..8f99cf1d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,6 +17,8 @@ int forwardToPrimary(SingleInstance& instance, const cl::CommandLine& cl); int main(int argc, char *argv[]) { + TimeThis tt("main()"); + MOShared::SetThisThreadName("main"); setExceptionHandlers(); @@ -35,6 +37,8 @@ int main(int argc, char *argv[]) return forwardToPrimary(instance, cl); } + tt.stop(); + return app.run(instance); } diff --git a/src/moapplication.cpp b/src/moapplication.cpp index 885abc37..3f17e436 100644 --- a/src/moapplication.cpp +++ b/src/moapplication.cpp @@ -135,6 +135,8 @@ void addDllsToPath() MOApplication::MOApplication(cl::CommandLine& cl, int& argc, char** argv) : QApplication(argc, argv), m_cl(cl) { + TimeThis tt("MOApplication()"); + connect(&m_StyleWatcher, &QFileSystemWatcher::fileChanged, [&](auto&& file){ log::debug("style file '{}' changed, reloading", file); updateStyle(file); @@ -147,6 +149,8 @@ MOApplication::MOApplication(cl::CommandLine& cl, int& argc, char** argv) int MOApplication::run(SingleInstance& singleInstance) { + TimeThis tt("MOApplication run() to doOneRun()"); + auto& m = InstanceManager::singleton(); if (m_cl.instance()) @@ -169,6 +173,7 @@ int MOApplication::run(SingleInstance& singleInstance) // resets things when MO is "restarted" resetForRestart(); + tt.stop(); const auto r = doOneRun(singleInstance); if (r == RestartExitCode) { continue; @@ -186,6 +191,8 @@ int MOApplication::run(SingleInstance& singleInstance) int MOApplication::doOneRun(SingleInstance& singleInstance) { + TimeThis tt("MOApplication::doOneRun() instances"); + // figuring out the current instance auto currentInstance = getCurrentInstance(); if (!currentInstance) { @@ -218,6 +225,8 @@ int MOApplication::doOneRun(SingleInstance& singleInstance) log::info("working directory: {}", QDir::currentPath()); + tt.start("MOApplication::doOneRun() settings"); + // deleting old files, only for the main instance if (!singleInstance.secondary()) { purgeOldFiles(); @@ -235,6 +244,8 @@ int MOApplication::doOneRun(SingleInstance& singleInstance) OrganizerCore::setGlobalCoreDumpType(settings.diagnostics().coreDumpType()); + tt.start("MOApplication::doOneRun() log and checks"); + // logging and checking env::Environment env; env.dump(settings); @@ -251,11 +262,14 @@ int MOApplication::doOneRun(SingleInstance& singleInstance) std::unique_ptr pluginContainer; // nexus interface + tt.start("MOApplication::doOneRun() NexusInterface"); log::debug("initializing nexus interface"); NexusInterface ni(&settings); // organizer core + tt.start("MOApplication::doOneRun() OrganizerCore"); log::debug("initializing core"); + OrganizerCore organizer(settings); if (!organizer.bootstrap()) { reportError("failed to set up data paths"); @@ -264,7 +278,9 @@ int MOApplication::doOneRun(SingleInstance& singleInstance) } // plugins + tt.start("MOApplication::doOneRun() plugins"); log::debug("initializing plugins"); + pluginContainer = std::make_unique(&organizer); pluginContainer->loadPlugins(); @@ -277,6 +293,8 @@ int MOApplication::doOneRun(SingleInstance& singleInstance) log::debug("this is a portable instance"); } + tt.start("MOApplication::doOneRun() OrganizerCore setup"); + sanity::checkPaths(*currentInstance->gamePlugin(), settings); // setting up organizer core @@ -298,14 +316,19 @@ int MOApplication::doOneRun(SingleInstance& singleInstance) organizer.setCurrentProfile(currentInstance->profileName()); // checking command line + tt.start("MOApplication::doOneRun() command line"); if (auto r=m_cl.setupCore(organizer)) { return *r; } // show splash + tt.start("MOApplication::doOneRun() splash"); + MOSplash splash( settings, currentInstance->directory(), currentInstance->gamePlugin()); + tt.start("MOApplication::doOneRun() finishing"); + // start an api check QString apiKey; if (GlobalSettings::nexusApiKey(apiKey)) { @@ -329,6 +352,7 @@ int MOApplication::doOneRun(SingleInstance& singleInstance) int res = 1; { + tt.start("MOApplication::doOneRun() MainWindow setup"); MainWindow mainWindow(settings, organizer, *pluginContainer); // qt resets the thread name somewhere when creating the main window @@ -350,6 +374,8 @@ int MOApplication::doOneRun(SingleInstance& singleInstance) mainWindow.activateWindow(); splash.close(); + tt.stop(); + res = exec(); mainWindow.close(); -- cgit v1.3.1 From a5469ae4e0668c36e4bf36bf7395fa25073b0bc6 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sun, 8 Nov 2020 22:07:15 -0500 Subject: renamed SingleInstance to MOMultiProcess rewrote some comments to avoid using "instance" --- src/commandline.cpp | 5 ++--- src/main.cpp | 16 ++++++++-------- src/moapplication.cpp | 12 ++++++------ src/moapplication.h | 6 +++--- src/singleinstance.cpp | 36 ++++++++---------------------------- src/singleinstance.h | 48 ++++++++++++++---------------------------------- 6 files changed, 41 insertions(+), 82 deletions(-) (limited to 'src') diff --git a/src/commandline.cpp b/src/commandline.cpp index 3d0e901d..fd2fcb51 100644 --- a/src/commandline.cpp +++ b/src/commandline.cpp @@ -144,7 +144,8 @@ std::optional CommandLine::run(const std::wstring& line) // the first word on the command line is not a valid command, try the other - // stuff; this is handled in main.cpp + // stuff; this is used in setupCore() below when called from + // MOApplication::doOneRun() // look for help if (m_vm.count("help")) { @@ -202,8 +203,6 @@ std::optional CommandLine::run(const std::wstring& line) std::optional CommandLine::setupCore(OrganizerCore& organizer) const { - // if we have a command line parameter, it is either a nxm link or - // a binary to start if (m_shortcut.isValid()) { if (m_shortcut.hasExecutable()) { try { diff --git a/src/main.cpp b/src/main.cpp index 8f99cf1d..440489d3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,7 +13,7 @@ using namespace MOBase; thread_local LPTOP_LEVEL_EXCEPTION_FILTER g_prevExceptionFilter = nullptr; thread_local std::terminate_handler g_prevTerminateHandler = nullptr; -int forwardToPrimary(SingleInstance& instance, const cl::CommandLine& cl); +int forwardToPrimary(MOMultiProcess& multiProcess, const cl::CommandLine& cl); int main(int argc, char *argv[]) { @@ -32,22 +32,22 @@ int main(int argc, char *argv[]) QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); MOApplication app(cl, argc, argv); - SingleInstance instance(cl.multiple()); - if (instance.ephemeral()) { - return forwardToPrimary(instance, cl); + MOMultiProcess multiProcess(cl.multiple()); + if (multiProcess.ephemeral()) { + return forwardToPrimary(multiProcess, cl); } tt.stop(); - return app.run(instance); + return app.run(multiProcess); } -int forwardToPrimary(SingleInstance& instance, const cl::CommandLine& cl) +int forwardToPrimary(MOMultiProcess& multiProcess, const cl::CommandLine& cl) { if (cl.shortcut().isValid()) { - instance.sendMessage(cl.shortcut().toString()); + multiProcess.sendMessage(cl.shortcut().toString()); } else if (cl.nxmLink()) { - instance.sendMessage(*cl.nxmLink()); + multiProcess.sendMessage(*cl.nxmLink()); } else { QMessageBox::information( nullptr, QObject::tr("Mod Organizer"), diff --git a/src/moapplication.cpp b/src/moapplication.cpp index 3f17e436..db559421 100644 --- a/src/moapplication.cpp +++ b/src/moapplication.cpp @@ -147,7 +147,7 @@ MOApplication::MOApplication(cl::CommandLine& cl, int& argc, char** argv) addDllsToPath(); } -int MOApplication::run(SingleInstance& singleInstance) +int MOApplication::run(MOMultiProcess& multiProcess) { TimeThis tt("MOApplication run() to doOneRun()"); @@ -174,7 +174,7 @@ int MOApplication::run(SingleInstance& singleInstance) resetForRestart(); tt.stop(); - const auto r = doOneRun(singleInstance); + const auto r = doOneRun(multiProcess); if (r == RestartExitCode) { continue; } @@ -189,7 +189,7 @@ int MOApplication::run(SingleInstance& singleInstance) } } -int MOApplication::doOneRun(SingleInstance& singleInstance) +int MOApplication::doOneRun(MOMultiProcess& multiProcess) { TimeThis tt("MOApplication::doOneRun() instances"); @@ -217,7 +217,7 @@ int MOApplication::doOneRun(SingleInstance& singleInstance) createVersionInfo().displayString(3), GITID, QCoreApplication::applicationDirPath(), MOShared::getUsvfsVersionString()); - if (singleInstance.secondary()) { + if (multiProcess.secondary()) { log::debug("another instance of MO is running but --multiple was given"); } @@ -228,7 +228,7 @@ int MOApplication::doOneRun(SingleInstance& singleInstance) tt.start("MOApplication::doOneRun() settings"); // deleting old files, only for the main instance - if (!singleInstance.secondary()) { + if (!multiProcess.secondary()) { purgeOldFiles(); } @@ -365,7 +365,7 @@ int MOApplication::doOneRun(SingleInstance& singleInstance) QObject::connect(&mainWindow, SIGNAL(styleChanged(QString)), this, SLOT(setStyleFile(QString))); - QObject::connect(&singleInstance, SIGNAL(messageSent(QString)), &organizer, + QObject::connect(&multiProcess, SIGNAL(messageSent(QString)), &organizer, SLOT(externalMessage(QString))); diff --git a/src/moapplication.h b/src/moapplication.h index 7423c897..91b8023a 100644 --- a/src/moapplication.h +++ b/src/moapplication.h @@ -24,7 +24,7 @@ along with Mod Organizer. If not, see . #include class Settings; -class SingleInstance; +class MOMultiProcess; class Instance; class PluginContainer; @@ -40,7 +40,7 @@ public: // sets up everything, creates the main window and runs it // - int run(SingleInstance& si); + int run(MOMultiProcess& multiProcess); virtual bool notify(QObject* receiver, QEvent* event); @@ -55,7 +55,7 @@ private: QString m_DefaultStyle; cl::CommandLine& m_cl; - int doOneRun(SingleInstance& singleInstance); + int doOneRun(MOMultiProcess& multiProcess); std::optional getCurrentInstance(); std::optional setupInstanceLoop(Instance& currentInstance, PluginContainer& pc); diff --git a/src/singleinstance.cpp b/src/singleinstance.cpp index bd7ccc43..401381fd 100644 --- a/src/singleinstance.cpp +++ b/src/singleinstance.cpp @@ -1,22 +1,3 @@ -/* -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 "singleinstance.h" #include "utility.h" #include @@ -28,7 +9,7 @@ static const int s_Timeout = 5000; using MOBase::reportError; -SingleInstance::SingleInstance(bool allowMultiple, QObject *parent) : +MOMultiProcess::MOMultiProcess(bool allowMultiple, QObject *parent) : QObject(parent), m_Ephemeral(false), m_OwnsSM(false) { m_SharedMem.setKey(s_Key); @@ -58,7 +39,7 @@ SingleInstance::SingleInstance(bool allowMultiple, QObject *parent) : } -void SingleInstance::sendMessage(const QString &message) +void MOMultiProcess::sendMessage(const QString &message) { if (m_OwnsSM) { // nobody there to receive the message @@ -72,19 +53,19 @@ void SingleInstance::sendMessage(const QString &message) Sleep(250); } - // other instance may be just starting up + // other process may be just starting up socket.connectToServer(s_Key, QIODevice::WriteOnly); connected = socket.waitForConnected(s_Timeout); } if (!connected) { - reportError(tr("failed to connect to running instance: %1").arg(socket.errorString())); + reportError(tr("failed to connect to running process: %1").arg(socket.errorString())); return; } socket.write(message.toUtf8()); if (!socket.waitForBytesWritten(s_Timeout)) { - reportError(tr("failed to communicate with running instance: %1").arg(socket.errorString())); + reportError(tr("failed to communicate with running process: %1").arg(socket.errorString())); return; } @@ -92,8 +73,7 @@ void SingleInstance::sendMessage(const QString &message) socket.waitForDisconnected(); } - -void SingleInstance::receiveMessage() +void MOMultiProcess::receiveMessage() { QLocalSocket *socket = m_Server.nextPendingConnection(); if (!socket) { @@ -108,10 +88,10 @@ void SingleInstance::receiveMessage() if (av <= 0) { MOBase::log::error( - "failed to receive data from secondary instance: {}", + "failed to receive data from secondary process: {}", socket->errorString()); - reportError(tr("failed to receive data from secondary instance: %1").arg(socket->errorString())); + reportError(tr("failed to receive data from secondary process: %1").arg(socket->errorString())); return; } } diff --git a/src/singleinstance.h b/src/singleinstance.h index 5f6c3633..810e63d2 100644 --- a/src/singleinstance.h +++ b/src/singleinstance.h @@ -1,24 +1,5 @@ -/* -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 . -*/ - -#ifndef SINGLEINSTANCE_H -#define SINGLEINSTANCE_H +#ifndef MODORGANIZER_MOMULTIPROCESS_INCLUDED +#define MODORGANIZER_MOMULTIPROCESS_INCLUDED #include #include @@ -26,30 +7,29 @@ along with Mod Organizer. If not, see . /** - * used to ensure only a single instance of Mod Organizer is started and to - * allow ephemeral instances to send messages to the primary (visible) one. - * This way, other instances can start a download in the primary one + * used to ensure only a single process of Mod Organizer is started and to + * allow ephemeral processes to send messages to the primary (visible) one. + * This way, other processes can start a download in the primary one **/ -class SingleInstance : public QObject +class MOMultiProcess : public QObject { - Q_OBJECT public: - // `allowMultiple`: if another instance is running, run this one + // `allowMultiple`: if another process is running, run this one // disconnected from the shared memory - explicit SingleInstance(bool allowMultiple, QObject *parent = 0); + explicit MOMultiProcess(bool allowMultiple, QObject *parent = 0); /** - * @return true if this instance's job is to forward data to the primary - * instance through shared memory + * @return true if this process's job is to forward data to the primary + * process through shared memory **/ bool ephemeral() const { return m_Ephemeral; } - // returns true if this is not the primary instance, but was allowed because + // returns true if this is not the primary process, but was allowed because // of the AllowMultiple flag // bool secondary() const @@ -58,7 +38,7 @@ public: } /** - * send a message to the primary instance. This can be used to transmit download urls + * send a message to the primary process. This can be used to transmit download urls * * @param message message to send **/ @@ -67,7 +47,7 @@ public: signals: /** - * @brief emitted when an ephemeral instance has sent a message (to us) + * @brief emitted when an ephemeral process has sent a message (to us) * * @param message the message we received **/ @@ -87,4 +67,4 @@ private: }; -#endif // SINGLEINSTANCE_H +#endif // MODORGANIZER_MOMULTIPROCESS_INCLUDED -- cgit v1.3.1 From d7ae42d3775f88d5cd63fa2f8860bd31e18f981e Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sun, 8 Nov 2020 22:09:42 -0500 Subject: renamed singleinstance.h/cpp to multiprocess.h/cpp --- src/CMakeLists.txt | 2 +- src/main.cpp | 2 +- src/moapplication.cpp | 2 +- src/multiprocess.cpp | 102 +++++++++++++++++++++++++++++++++++++++++++++++++ src/multiprocess.h | 70 +++++++++++++++++++++++++++++++++ src/singleinstance.cpp | 102 ------------------------------------------------- src/singleinstance.h | 70 --------------------------------- 7 files changed, 175 insertions(+), 175 deletions(-) create mode 100644 src/multiprocess.cpp create mode 100644 src/multiprocess.h delete mode 100644 src/singleinstance.cpp delete mode 100644 src/singleinstance.h (limited to 'src') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 30614e59..bb4b151f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -15,9 +15,9 @@ add_filter(NAME src/application GROUPS main moapplication moshortcut + multiprocess sanitychecks selfupdater - singleinstance ) add_filter(NAME src/browser GROUPS diff --git a/src/main.cpp b/src/main.cpp index 440489d3..554ed006 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,4 +1,4 @@ -#include "singleinstance.h" +#include "multiprocess.h" #include "loglist.h" #include "moapplication.h" #include "organizercore.h" diff --git a/src/moapplication.cpp b/src/moapplication.cpp index db559421..9139acbb 100644 --- a/src/moapplication.cpp +++ b/src/moapplication.cpp @@ -25,7 +25,7 @@ along with Mod Organizer. If not, see . #include "organizercore.h" #include "thread_utils.h" #include "loglist.h" -#include "singleinstance.h" +#include "multiprocess.h" #include "nexusinterface.h" #include "nxmaccessmanager.h" #include "tutorialmanager.h" diff --git a/src/multiprocess.cpp b/src/multiprocess.cpp new file mode 100644 index 00000000..4ce58718 --- /dev/null +++ b/src/multiprocess.cpp @@ -0,0 +1,102 @@ +#include "multiprocess.h" +#include "utility.h" +#include +#include +#include + +static const char s_Key[] = "mo-43d1a3ad-eeb0-4818-97c9-eda5216c29b5"; +static const int s_Timeout = 5000; + +using MOBase::reportError; + +MOMultiProcess::MOMultiProcess(bool allowMultiple, QObject *parent) : + QObject(parent), m_Ephemeral(false), m_OwnsSM(false) +{ + m_SharedMem.setKey(s_Key); + + if (!m_SharedMem.create(1)) { + if (m_SharedMem.error() == QSharedMemory::AlreadyExists) { + if (!allowMultiple) { + m_SharedMem.attach(); + m_Ephemeral = true; + } + } + + if ((m_SharedMem.error() != QSharedMemory::NoError) && + (m_SharedMem.error() != QSharedMemory::AlreadyExists)) { + throw MOBase::MyException(tr("SHM error: %1").arg(m_SharedMem.errorString())); + } + } else { + m_OwnsSM = true; + } + + if (m_OwnsSM) { + connect(&m_Server, SIGNAL(newConnection()), this, SLOT(receiveMessage()), Qt::QueuedConnection); + // has to be called before listen + m_Server.setSocketOptions(QLocalServer::WorldAccessOption); + m_Server.listen(s_Key); + } +} + + +void MOMultiProcess::sendMessage(const QString &message) +{ + if (m_OwnsSM) { + // nobody there to receive the message + return; + } + QLocalSocket socket(this); + + bool connected = false; + for(int i = 0; i < 2 && !connected; ++i) { + if (i > 0) { + Sleep(250); + } + + // other process may be just starting up + socket.connectToServer(s_Key, QIODevice::WriteOnly); + connected = socket.waitForConnected(s_Timeout); + } + + if (!connected) { + reportError(tr("failed to connect to running process: %1").arg(socket.errorString())); + return; + } + + socket.write(message.toUtf8()); + if (!socket.waitForBytesWritten(s_Timeout)) { + reportError(tr("failed to communicate with running process: %1").arg(socket.errorString())); + return; + } + + socket.disconnectFromServer(); + socket.waitForDisconnected(); +} + +void MOMultiProcess::receiveMessage() +{ + QLocalSocket *socket = m_Server.nextPendingConnection(); + if (!socket) { + return; + } + + if (!socket->waitForReadyRead(s_Timeout)) { + // check if there are bytes available; if so, it probably means the data was + // already received by the time waitForReadyRead() was called and the + // connection has been closed + const auto av = socket->bytesAvailable(); + + if (av <= 0) { + MOBase::log::error( + "failed to receive data from secondary process: {}", + socket->errorString()); + + reportError(tr("failed to receive data from secondary process: %1").arg(socket->errorString())); + return; + } + } + + QString message = QString::fromUtf8(socket->readAll().constData()); + emit messageSent(message); + socket->disconnectFromServer(); +} diff --git a/src/multiprocess.h b/src/multiprocess.h new file mode 100644 index 00000000..810e63d2 --- /dev/null +++ b/src/multiprocess.h @@ -0,0 +1,70 @@ +#ifndef MODORGANIZER_MOMULTIPROCESS_INCLUDED +#define MODORGANIZER_MOMULTIPROCESS_INCLUDED + +#include +#include +#include + + +/** + * used to ensure only a single process of Mod Organizer is started and to + * allow ephemeral processes to send messages to the primary (visible) one. + * This way, other processes can start a download in the primary one + **/ +class MOMultiProcess : public QObject +{ + Q_OBJECT + +public: + // `allowMultiple`: if another process is running, run this one + // disconnected from the shared memory + explicit MOMultiProcess(bool allowMultiple, QObject *parent = 0); + + /** + * @return true if this process's job is to forward data to the primary + * process through shared memory + **/ + bool ephemeral() const + { + return m_Ephemeral; + } + + // returns true if this is not the primary process, but was allowed because + // of the AllowMultiple flag + // + bool secondary() const + { + return !m_Ephemeral && !m_OwnsSM; + } + + /** + * send a message to the primary process. This can be used to transmit download urls + * + * @param message message to send + **/ + void sendMessage(const QString &message); + +signals: + + /** + * @brief emitted when an ephemeral process has sent a message (to us) + * + * @param message the message we received + **/ + void messageSent(const QString &message); + +public slots: + +private slots: + + void receiveMessage(); + +private: + bool m_Ephemeral; + bool m_OwnsSM; + QSharedMemory m_SharedMem; + QLocalServer m_Server; + +}; + +#endif // MODORGANIZER_MOMULTIPROCESS_INCLUDED diff --git a/src/singleinstance.cpp b/src/singleinstance.cpp deleted file mode 100644 index 401381fd..00000000 --- a/src/singleinstance.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "singleinstance.h" -#include "utility.h" -#include -#include -#include - -static const char s_Key[] = "mo-43d1a3ad-eeb0-4818-97c9-eda5216c29b5"; -static const int s_Timeout = 5000; - -using MOBase::reportError; - -MOMultiProcess::MOMultiProcess(bool allowMultiple, QObject *parent) : - QObject(parent), m_Ephemeral(false), m_OwnsSM(false) -{ - m_SharedMem.setKey(s_Key); - - if (!m_SharedMem.create(1)) { - if (m_SharedMem.error() == QSharedMemory::AlreadyExists) { - if (!allowMultiple) { - m_SharedMem.attach(); - m_Ephemeral = true; - } - } - - if ((m_SharedMem.error() != QSharedMemory::NoError) && - (m_SharedMem.error() != QSharedMemory::AlreadyExists)) { - throw MOBase::MyException(tr("SHM error: %1").arg(m_SharedMem.errorString())); - } - } else { - m_OwnsSM = true; - } - - if (m_OwnsSM) { - connect(&m_Server, SIGNAL(newConnection()), this, SLOT(receiveMessage()), Qt::QueuedConnection); - // has to be called before listen - m_Server.setSocketOptions(QLocalServer::WorldAccessOption); - m_Server.listen(s_Key); - } -} - - -void MOMultiProcess::sendMessage(const QString &message) -{ - if (m_OwnsSM) { - // nobody there to receive the message - return; - } - QLocalSocket socket(this); - - bool connected = false; - for(int i = 0; i < 2 && !connected; ++i) { - if (i > 0) { - Sleep(250); - } - - // other process may be just starting up - socket.connectToServer(s_Key, QIODevice::WriteOnly); - connected = socket.waitForConnected(s_Timeout); - } - - if (!connected) { - reportError(tr("failed to connect to running process: %1").arg(socket.errorString())); - return; - } - - socket.write(message.toUtf8()); - if (!socket.waitForBytesWritten(s_Timeout)) { - reportError(tr("failed to communicate with running process: %1").arg(socket.errorString())); - return; - } - - socket.disconnectFromServer(); - socket.waitForDisconnected(); -} - -void MOMultiProcess::receiveMessage() -{ - QLocalSocket *socket = m_Server.nextPendingConnection(); - if (!socket) { - return; - } - - if (!socket->waitForReadyRead(s_Timeout)) { - // check if there are bytes available; if so, it probably means the data was - // already received by the time waitForReadyRead() was called and the - // connection has been closed - const auto av = socket->bytesAvailable(); - - if (av <= 0) { - MOBase::log::error( - "failed to receive data from secondary process: {}", - socket->errorString()); - - reportError(tr("failed to receive data from secondary process: %1").arg(socket->errorString())); - return; - } - } - - QString message = QString::fromUtf8(socket->readAll().constData()); - emit messageSent(message); - socket->disconnectFromServer(); -} diff --git a/src/singleinstance.h b/src/singleinstance.h deleted file mode 100644 index 810e63d2..00000000 --- a/src/singleinstance.h +++ /dev/null @@ -1,70 +0,0 @@ -#ifndef MODORGANIZER_MOMULTIPROCESS_INCLUDED -#define MODORGANIZER_MOMULTIPROCESS_INCLUDED - -#include -#include -#include - - -/** - * used to ensure only a single process of Mod Organizer is started and to - * allow ephemeral processes to send messages to the primary (visible) one. - * This way, other processes can start a download in the primary one - **/ -class MOMultiProcess : public QObject -{ - Q_OBJECT - -public: - // `allowMultiple`: if another process is running, run this one - // disconnected from the shared memory - explicit MOMultiProcess(bool allowMultiple, QObject *parent = 0); - - /** - * @return true if this process's job is to forward data to the primary - * process through shared memory - **/ - bool ephemeral() const - { - return m_Ephemeral; - } - - // returns true if this is not the primary process, but was allowed because - // of the AllowMultiple flag - // - bool secondary() const - { - return !m_Ephemeral && !m_OwnsSM; - } - - /** - * send a message to the primary process. This can be used to transmit download urls - * - * @param message message to send - **/ - void sendMessage(const QString &message); - -signals: - - /** - * @brief emitted when an ephemeral process has sent a message (to us) - * - * @param message the message we received - **/ - void messageSent(const QString &message); - -public slots: - -private slots: - - void receiveMessage(); - -private: - bool m_Ephemeral; - bool m_OwnsSM; - QSharedMemory m_SharedMem; - QLocalServer m_Server; - -}; - -#endif // MODORGANIZER_MOMULTIPROCESS_INCLUDED -- cgit v1.3.1 From 3cc568a852e9b64bb6e0f34f74869965d04f6a0c Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sun, 8 Nov 2020 23:55:17 -0500 Subject: keyboard nav for create instance dialog change the skip intro setting on click instead of on completion ctrl+f in game page --- src/createinstancedialog.cpp | 62 +++++++++++++++---- src/createinstancedialog.h | 23 +++++++- src/createinstancedialog.ui | 6 ++ src/createinstancedialogpages.cpp | 121 +++++++++++++++++++++++++++++--------- src/createinstancedialogpages.h | 66 +++++++++++++++------ 5 files changed, 220 insertions(+), 58 deletions(-) (limited to 'src') diff --git a/src/createinstancedialog.cpp b/src/createinstancedialog.cpp index 1c1b62d0..47cb9d5c 100644 --- a/src/createinstancedialog.cpp +++ b/src/createinstancedialog.cpp @@ -141,8 +141,16 @@ CreateInstanceDialog::CreateInstanceDialog( next(); } + ui->next->setFocus(); + updateNavigation(); + addShortcutAction(QKeySequence::Find, Actions::Find); + + addShortcut(Qt::ALT+Qt::Key_Left, [&]{ back(); }); + addShortcut(Qt::ALT+Qt::Key_Right, [&]{ next(false); }); + addShortcut(Qt::CTRL+Qt::Key_Return, [&]{ next(); }); + connect(ui->next, &QPushButton::clicked, [&]{ next(); }); connect(ui->back, &QPushButton::clicked, [&]{ back(); }); connect(ui->cancel, &QPushButton::clicked, [&]{ reject(); }); @@ -176,17 +184,23 @@ bool CreateInstanceDialog::isOnLastPage() const return true; } -void CreateInstanceDialog::next() +void CreateInstanceDialog::next(bool allowFinish) { + if (!canNext()) { + return; + } + const auto i = ui->pages->currentIndex(); const auto last = isOnLastPage(); if (last) { - if (m_singlePage) { - // just close the dialog - accept(); - } else { - finish(); + if (allowFinish) { + if (m_singlePage) { + // just close the dialog + accept(); + } else { + finish(); + } } } else { changePage(+1); @@ -195,9 +209,40 @@ void CreateInstanceDialog::next() void CreateInstanceDialog::back() { + if (!canBack()) { + return; + } + changePage(-1); } +void CreateInstanceDialog::addShortcut( + QKeySequence seq, std::function f) +{ + auto* sc = new QShortcut(seq, this); + + sc->setAutoRepeat(false); + sc->setContext(Qt::WidgetWithChildrenShortcut); + + QObject::connect(sc, &QShortcut::activated, f); +} + +void CreateInstanceDialog::addShortcutAction(QKeySequence seq, Actions a) +{ + addShortcut(seq, [this, a]{ doAction(a); }); +} + +void CreateInstanceDialog::doAction(Actions a) +{ + std::size_t i = static_cast(ui->pages->currentIndex()); + + if (i >= m_pages.size()) { + return; + } + + m_pages[i]->action(a); +} + void CreateInstanceDialog::setSinglePageImpl(const QString& instanceName) { m_singlePage = true; @@ -347,11 +392,6 @@ void CreateInstanceDialog::finish() logCreation(tr("Done.")); - // remember settings - if (ui->hideIntro->isChecked()) { - GlobalSettings::setHideCreateInstanceIntro(true); - } - // launch the new instance if (ui->launch->isChecked()) { InstanceManager::singleton().setCurrentInstance(ci.instanceName); diff --git a/src/createinstancedialog.h b/src/createinstancedialog.h index d541f45e..8e8fe517 100644 --- a/src/createinstancedialog.h +++ b/src/createinstancedialog.h @@ -27,6 +27,11 @@ class CreateInstanceDialog : public QDialog Q_OBJECT public: + enum class Actions + { + Find = 1 + }; + // instance type // enum Types @@ -107,9 +112,10 @@ public: } - // moves to the next page calls finish() if on the last one + // moves to the next page; if `allowFinish` is true, calls finish() if + // currently on the last page // - void next(); + void next(bool allowFinish=true); // moves to the previous page, if any // @@ -171,6 +177,19 @@ private: bool m_singlePage; + // creates a shortcut for the given sequence + // + void addShortcut(QKeySequence seq, std::function f); + + // creates a shortcut for the given sequence and executes the action when + // activated + // + void addShortcutAction(QKeySequence seq, Actions a); + + // calls action() with the given action on the selected page, if any + // + void doAction(Actions a); + // called from setSinglePage(), does whatever doesn't need the T // void setSinglePageImpl(const QString& instanceName); diff --git a/src/createinstancedialog.ui b/src/createinstancedialog.ui index 4d1eda1f..86fa900b 100644 --- a/src/createinstancedialog.ui +++ b/src/createinstancedialog.ui @@ -1102,6 +1102,9 @@ QTextEdit::NoWrap + + true +
@@ -1112,6 +1115,9 @@ Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + Instance creation log +
diff --git a/src/createinstancedialogpages.cpp b/src/createinstancedialogpages.cpp index 49b118f7..882776df 100644 --- a/src/createinstancedialogpages.cpp +++ b/src/createinstancedialogpages.cpp @@ -59,8 +59,9 @@ void PlaceholderLabel::setVisible(bool b) -Page::Page(CreateInstanceDialog& dlg) - : ui(dlg.getUI()), m_dlg(dlg), m_pc(dlg.pluginContainer()), m_skip(false) +Page::Page(CreateInstanceDialog& dlg) : + ui(dlg.getUI()), m_dlg(dlg), m_pc(dlg.pluginContainer()), + m_skip(false), m_firstActivation(true) { } @@ -80,11 +81,17 @@ bool Page::doSkip() const return false; } -void Page::activated() +void Page::doActivated(bool) { // no-op } +void Page::activated() +{ + doActivated(m_firstActivation); + m_firstActivation = false; +} + void Page::setSkip(bool b) { m_skip = b; @@ -100,6 +107,12 @@ void Page::next() m_dlg.next(); } +bool Page::action(CreateInstanceDialog::Actions a) +{ + // no-op + return false; +} + CreateInstanceDialog::Types Page::selectedInstanceType() const { @@ -139,13 +152,16 @@ CreateInstanceDialog::Paths Page::selectedPaths() const IntroPage::IntroPage(CreateInstanceDialog& dlg) - : Page(dlg) + : Page(dlg), m_skip(GlobalSettings::hideCreateInstanceIntro()) { + QObject::connect(ui->hideIntro, &QCheckBox::toggled, [&] { + GlobalSettings::setHideCreateInstanceIntro(ui->hideIntro->isChecked()); + }); } bool IntroPage::doSkip() const { - return GlobalSettings::hideCreateInstanceIntro(); + return m_skip; } @@ -207,6 +223,13 @@ void TypePage::portable() next(); } +void TypePage::doActivated(bool firstTime) +{ + if (firstTime) { + ui->createGlobal->setFocus(); + } +} + GamePage::Game::Game(IPluginGame* g) : game(g), installed(g->isInstalled()) @@ -224,6 +247,7 @@ GamePage::GamePage(CreateInstanceDialog& dlg) fillList(); m_filter.setEdit(ui->gamesFilter); + m_filter.setUpdateDelay(0); QObject::connect(&m_filter, &FilterWidget::changed, [&]{ fillList(); }); QObject::connect(ui->showAllGames, &QCheckBox::clicked, [&]{ fillList(); }); @@ -234,6 +258,18 @@ bool GamePage::ready() const return (m_selection != nullptr); } +bool GamePage::action(CreateInstanceDialog::Actions a) +{ + using Actions = CreateInstanceDialog::Actions; + + if (a == Actions::Find) { + ui->gamesFilter->setFocus(); + return true; + } + + return false; +} + IPluginGame* GamePage::selectedGame() const { if (!m_selection) { @@ -263,7 +299,8 @@ void GamePage::select(IPluginGame* game, const QString& dir) // ask the user const auto path = QFileDialog::getExistingDirectory( - &m_dlg, QObject::tr("Find game installation")); + &m_dlg, QObject::tr("Find game installation for %1") + .arg(game->gameName())); if (path.isEmpty()) { // cancelled @@ -291,11 +328,13 @@ void GamePage::select(IPluginGame* game, const QString& dir) // select this plugin, if any m_selection = checked; - selectButton(checked); // update the button associated with it in case the paths have changed updateButton(checked); + // toggle it on + selectButton(checked); + updateNavigation(); if (checked) { @@ -519,6 +558,8 @@ void GamePage::fillList() clearButtons(); + Game* firstButton = nullptr; + for (auto& g : m_games) { if (!showAll && !g->installed) { // not installed @@ -532,10 +573,18 @@ void GamePage::fillList() createGameButton(g.get()); addButton(g->button); + + if (!firstButton) { + firstButton = g.get(); + } } // browse button addButton(createCustomButton()); + + if (firstButton) { + firstButton->button->setDefault(true); + } } GamePage::Game* GamePage::checkInstallation(const QString& path, Game* g) @@ -680,7 +729,7 @@ bool VariantsPage::doSkip() const return (g->gameVariants().size() < 2); } -void VariantsPage::activated() +void VariantsPage::doActivated(bool) { auto* g = m_dlg.rawCreationInfo().game; @@ -749,6 +798,10 @@ void VariantsPage::fillList() ui->editions->addButton(b, QDialogButtonBox::AcceptRole); m_buttons.push_back(b); } + + if (!m_buttons.empty()) { + m_buttons[0]->setDefault(true); + } } @@ -759,6 +812,9 @@ NamePage::NamePage(CreateInstanceDialog& dlg) : { QObject::connect( ui->instanceName, &QLineEdit::textEdited, [&]{ onChanged(); }); + + QObject::connect( + ui->instanceName, &QLineEdit::returnPressed, [&]{ next(); }); } bool NamePage::ready() const @@ -773,7 +829,7 @@ bool NamePage::doSkip() const return (m_dlg.rawCreationInfo().type == CreateInstanceDialog::Portable); } -void NamePage::activated() +void NamePage::doActivated(bool) { auto* g = m_dlg.rawCreationInfo().game; if (!g) { @@ -863,23 +919,28 @@ PathsPage::PathsPage(CreateInstanceDialog& dlg) : m_advancedInvalid(ui->advancedDirInvalid), m_okay(false) { - using O = QObject; - using E = QLineEdit; - using B = QAbstractButton; - - O::connect(ui->location, &E::textEdited, [&]{ onChanged(); }); - O::connect(ui->base, &E::textEdited, [&]{ onChanged(); }); - O::connect(ui->downloads, &E::textEdited, [&]{ onChanged(); }); - O::connect(ui->mods, &E::textEdited, [&]{ onChanged(); }); - O::connect(ui->profiles, &E::textEdited, [&]{ onChanged(); }); - O::connect(ui->overwrite, &E::textEdited, [&]{ onChanged(); }); - - O::connect(ui->browseLocation, &B::clicked, [&]{ browse(ui->location); }); - O::connect(ui->browseBase, &B::clicked, [&]{ browse(ui->base); }); - O::connect(ui->browseDownloads, &B::clicked, [&]{ browse(ui->downloads); }); - O::connect(ui->browseMods, &B::clicked, [&]{ browse(ui->mods); }); - O::connect(ui->browseProfiles, &B::clicked, [&]{ browse(ui->profiles); }); - O::connect(ui->browseOverwrite, &B::clicked, [&]{ browse(ui->overwrite); }); + auto setEdit = [&](QLineEdit* e) { + QObject::connect(e, &QLineEdit::textEdited, [&]{ onChanged(); }); + QObject::connect(e, &QLineEdit::returnPressed, [&]{ next(); }); + }; + + auto setBrowse = [&](QAbstractButton* b, QLineEdit* e) { + QObject::connect(b, &QAbstractButton::clicked, [&]{ browse(e); }); + }; + + setEdit(ui->location); + setEdit(ui->base); + setEdit(ui->downloads); + setEdit(ui->mods); + setEdit(ui->profiles); + setEdit(ui->overwrite); + + setBrowse(ui->browseLocation, ui->location); + setBrowse(ui->browseBase, ui->base); + setBrowse(ui->browseDownloads, ui->downloads); + setBrowse(ui->browseMods, ui->mods); + setBrowse(ui->browseProfiles, ui->profiles); + setBrowse(ui->browseOverwrite, ui->overwrite); QObject::connect( ui->advancedPathOptions, &QCheckBox::clicked, [&]{ onAdvanced(); }); @@ -894,7 +955,7 @@ bool PathsPage::ready() const return m_okay; } -void PathsPage::activated() +void PathsPage::doActivated(bool firstTime) { const auto name = m_dlg.rawCreationInfo().instanceName; const auto type = m_dlg.rawCreationInfo().type; @@ -913,6 +974,10 @@ void PathsPage::activated() m_label.setText(m_dlg.rawCreationInfo().game->gameName()); m_lastInstanceName = name; m_lastType = type; + + if (firstTime) { + ui->location->setFocus(); + } } CreateInstanceDialog::Paths PathsPage::selectedPaths() const @@ -1120,7 +1185,7 @@ ConfirmationPage::ConfirmationPage(CreateInstanceDialog& dlg) { } -void ConfirmationPage::activated() +void ConfirmationPage::doActivated(bool) { ui->review->setPlainText(makeReview()); ui->creationLog->clear(); diff --git a/src/createinstancedialogpages.h b/src/createinstancedialogpages.h index 4fde089b..08953a74 100644 --- a/src/createinstancedialogpages.h +++ b/src/createinstancedialogpages.h @@ -60,7 +60,7 @@ public: // called every time a page is shown in the screen // - virtual void activated(); + void activated(); // overrides whether this page should be skipped; this is used by // CreateInstanceDialog::setSinglePage() to disable all other pages @@ -82,6 +82,12 @@ public: void next(); + // called from the dialog when an action is requested on the current page; + // returns true when handled + // + virtual bool action(CreateInstanceDialog::Actions a); + + // returns the instance type // virtual CreateInstanceDialog::Types selectedInstanceType() const; @@ -111,8 +117,14 @@ protected: CreateInstanceDialog& m_dlg; const PluginContainer& m_pc; bool m_skip; + bool m_firstActivation; + // called every time a page is shown in the screen; `firstTime` is true for + // first activation + // + virtual void doActivated(bool firstTime); + // implemented by derived classes, overridden by setSkip(true) // virtual bool doSkip() const; @@ -128,6 +140,12 @@ public: protected: bool doSkip() const override; + +private: + // the setting is only checked once when opening the dialog, or going forwards + // then back after checking the box wouldn't show the intro page any more, + // which would be unexpected + bool m_skip; }; @@ -154,6 +172,11 @@ public: // void portable(); +protected: + // focuses global instance button on first activation + // + void doActivated(bool firstTime) override; + private: CreateInstanceDialog::Types m_type; }; @@ -187,6 +210,10 @@ public: // bool ready() const override; + // handles find + // + bool action(CreateInstanceDialog::Actions a) override; + // returns the selected game // MOBase::IPluginGame* selectedGame() const override; @@ -262,7 +289,6 @@ private: // Game* findGame(MOBase::IPluginGame* game); - // creates the ui for the given game button // void createGameButton(Game* g); @@ -275,8 +301,13 @@ private: // void updateButton(Game* g); - // called when a button has been clicked; selects the game or asks the user - // for directory, depending + // game buttons are toggles, this creates the button for the given game if + // it doesn't exist and toggles it on + // + // the button might not exist if, for example: + // 1) this game is currently filtered out (not installed, doesn't match + // filter text, etc) and, + // 2) the user browses to a directory that a hidden plugin can use // void selectButton(Game* g); @@ -343,7 +374,7 @@ public: // uses the game selected in the previous page to fill the list, this must be // called every time because the user may go back in forth in the wizard // - void activated() override; + void doActivated(bool firstTime) override; // returns the selected variant, if any // @@ -388,18 +419,18 @@ public: // bool ready() const override; + // returns the instance name + // + QString selectedInstanceName() const override; + +protected: // uses the selected game to generate an instance name // // as long as the user hasn't modified the textbox, this will regenerate a new // instance name every time the selected game changes // - void activated() override; + void doActivated(bool firstTime) override; - // returns the instance name - // - QString selectedInstanceName() const override; - -protected: // returns true for portable instances // bool doSkip() const override; @@ -451,15 +482,16 @@ public: // bool ready() const override; + // returns the selected paths + // + CreateInstanceDialog::Paths selectedPaths() const override; + +protected: // resets all the paths if the instance type or instance name have changed, // the current values are kept as long as these don't change; also updates the // game name in the ui // - void activated() override; - - // returns the selected paths - // - CreateInstanceDialog::Paths selectedPaths() const override; + void doActivated(bool firstTime) override; private: // instance name the last time this page was active @@ -572,7 +604,7 @@ public: // recreates the log with the latest settings // - void activated() override; + void doActivated(bool firstTime) override; // returns the text for the log // -- cgit v1.3.1 From efdda8e7385446096a1032c2b7c2aeaf57132c78 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Mon, 9 Nov 2020 00:01:07 -0500 Subject: removed excess padding in plugins tab put blacklist into a group box --- src/settingsdialog.ui | 64 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui index 33c1c2ee..85c5a4e4 100644 --- a/src/settingsdialog.ui +++ b/src/settingsdialog.ui @@ -1044,9 +1044,6 @@ Qt::Vertical - - 2 - @@ -1055,6 +1052,18 @@ + + 0 + + + 0 + + + 0 + + + 0 + @@ -1066,11 +1075,20 @@ Qt::Horizontal - - 2 - + + 0 + + + 0 + + + 0 + + + 0 + @@ -1095,6 +1113,18 @@ + + 0 + + + 0 + + + 0 + + + 0 + @@ -1182,21 +1212,17 @@ - + 0 1 + + Blacklisted Plugins (use <del> to remove): + - - - - Blacklisted Plugins (use <del> to remove): - - - @@ -1613,16 +1639,16 @@ programs you are intentionally running. - - ColorTable - QTableWidget -
colortable.h
-
LinkLabel QLabel
linklabel.h
+ + ColorTable + QTableWidget +
colortable.h
+
tabWidget -- cgit v1.3.1