diff options
| author | Qudix <17361645+Qudix@users.noreply.github.com> | 2020-11-09 13:02:01 -0600 |
|---|---|---|
| committer | Qudix <17361645+Qudix@users.noreply.github.com> | 2020-11-09 13:02:01 -0600 |
| commit | 0a3bdad8afd18a21a3697804ae344abd09e3712e (patch) | |
| tree | 0bf234b41dff43cdfe39afea48c7c1d8bdd995c2 /src | |
| parent | b7b843d42badab83b262260035658259f24e4f9a (diff) | |
| parent | fef8543a58b4226242ce26c4c8876abaa64394cb (diff) | |
Merge branch 'master' of https://github.com/ModOrganizer2/modorganizer into master
Diffstat (limited to 'src')
40 files changed, 3273 insertions, 1790 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f2c31d37..50ce4178 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 @@ -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/commandline.cpp b/src/commandline.cpp index 3f8a6b1a..fd2fcb51 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 <log.h> +#include <report.h> namespace cl { +using namespace MOBase; + std::string pad_right(std::string s, std::size_t n, char c=' ') { if (s.size() < n) @@ -13,6 +18,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<std::pair<std::string, std::string>>& v, std::size_t indent, std::size_t spacing) @@ -60,6 +67,9 @@ std::optional<int> 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 +79,29 @@ std::optional<int> 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<std::string>(); + // 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 +125,7 @@ std::optional<int> CommandLine::run(const std::wstring& line) return 0; } + // run the command return c->run(line, m_vm, opts); } catch(po::error& e) @@ -122,6 +142,12 @@ std::optional<int> CommandLine::run(const std::wstring& line) } } + + // the first word on the command line is not a valid command, try the other + // stuff; this is used in setupCore() below when called from + // MOApplication::doOneRun() + + // look for help if (m_vm.count("help")) { env::Console console; std::cout << usage() << "\n"; @@ -130,12 +156,25 @@ std::optional<int> 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; } } @@ -162,6 +201,52 @@ std::optional<int> CommandLine::run(const std::wstring& line) } } +std::optional<int> CommandLine::setupCore(OrganizerCore& organizer) const +{ + 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(); @@ -182,6 +267,7 @@ void CommandLine::createOptions() ("command", po::value<std::string>(), "command") ("subargs", po::value<std::vector<std::string> >(), "args"); + // one command name, followed by any arguments for that command m_positional .add("command", 1) .add("subargs", -1); @@ -210,6 +296,7 @@ std::string CommandLine::usage(const Command* c) const << "\n" << "Commands:\n"; + // name and description for all commands std::vector<std::pair<std::string, std::string>> v; for (auto&& c : m_commands) { v.push_back({c->name(), c->description()}); @@ -224,6 +311,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 +335,8 @@ std::optional<QString> CommandLine::profile() const std::optional<QString> 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 +459,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<int> Command::run( const std::wstring& originalLine, po::variables_map vm, @@ -569,7 +644,7 @@ std::optional<int> ExeCommand::doRun() const auto args = vm()["arguments"].as<std::string>(); const auto cwd = vm()["cwd"].as<std::string>(); - + std::cout << "not implemented\n"; return 0; } @@ -587,6 +662,7 @@ Command::Meta RunCommand::meta() const std::optional<int> RunCommand::doRun() { + std::cout << "not implemented\n"; return {}; } diff --git a/src/commandline.h b/src/commandline.h index 72018ba3..baa0bfb9 100644 --- a/src/commandline.h +++ b/src/commandline.h @@ -4,49 +4,103 @@ #include <vector> #include <memory> +class OrganizerCore; + 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<int> run( const std::wstring& originalLine, po::variables_map vm, std::vector<std::wstring> 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<int> 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<std::wstring>& untouched() const; private: @@ -56,6 +110,8 @@ private: }; +// generates a crash dump for another MO process +// class CrashDumpCommand : public Command { protected: @@ -93,6 +149,8 @@ protected: }; +// runs a configured executable +// class ExeCommand : public Command { protected: @@ -105,6 +163,8 @@ protected: }; +// runs an arbitrary executable +// class RunCommand : public Command { protected: @@ -114,24 +174,96 @@ 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 +// +// +// 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: 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<int> 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<int> setupCore(OrganizerCore& organizer) const; + + + // 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<QString> profile() const; + + // instance override (-i) + // std::optional<QString> instance() const; + // returns the data parsed from an moshortcut:// option, if any + // const MOShortcut& shortcut() const; + + // returns the nxm:// link, if any + // std::optional<QString> nxmLink() const; + + // returns the executable/binary, if any std::optional<QString> executable() const; + // returns the list of arguments, excluding moshortcut or executable name; + // deprecated, only use with executable() + // const QStringList& untouched() const; private: diff --git a/src/createinstancedialog.cpp b/src/createinstancedialog.cpp index 2ff8adf3..47cb9d5c 100644 --- a/src/createinstancedialog.cpp +++ b/src/createinstancedialog.cpp @@ -10,6 +10,105 @@ 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: + DirectoryCreator(const DirectoryCreator&) = delete; + DirectoryCreator& operator=(const DirectoryCreator&) = delete; + + static std::unique_ptr<DirectoryCreator> create( + const QDir& target, std::function<void (QString)> log) + { + return std::unique_ptr<DirectoryCreator>(new DirectoryCreator(target, log)); + } + + ~DirectoryCreator() + { + rollback(); + } + + void commit() + { + m_created.clear(); + } + + void rollback() noexcept + { + 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) { + m_logger(r.toString()); + } + } + + m_created.clear(); + } + catch(...) + { + // eat it + } + } + +private: + std::function<void (QString)> m_logger; + + DirectoryCreator(const QDir& target, std::function<void (QString)> log) + : m_logger(log) + { + try + { + // split on separators + const QString s = QDir::toNativeSeparators(target.absolutePath()); + const QStringList cs = s.split("\\"); + + if (cs.empty()) { + return; + } + + // root directory + QDir d(cs[0]); + + // for each directory after the root + for (int i=1; i<cs.size(); ++i) { + d = d.filePath(cs[i]); + + if (!d.exists()) { + m_logger(QObject::tr("Creating %1").arg(d.path())); + const auto r = shell::CreateDirectories(d); + + if (!r) { + m_logger(r.toString()); + throw Failed(); + } + + m_created.push_back(d); + } + } + } + catch(...) + { + rollback(); + throw; + } + } + +private: + std::vector<QDir> m_created; +}; + + CreateInstanceDialog::CreateInstanceDialog( const PluginContainer& pc, Settings* s, QWidget *parent) : QDialog(parent), ui(new Ui::CreateInstanceDialog), m_pc(pc), m_settings(s), @@ -42,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(); }); @@ -77,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); @@ -96,9 +209,40 @@ void CreateInstanceDialog::next() void CreateInstanceDialog::back() { + if (!canBack()) { + return; + } + changePage(-1); } +void CreateInstanceDialog::addShortcut( + QKeySequence seq, std::function<void ()> 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<std::size_t>(ui->pages->currentIndex()); + + if (i >= m_pages.size()) { + return; + } + + m_pages[i]->action(a); +} + void CreateInstanceDialog::setSinglePageImpl(const QString& instanceName) { m_singlePage = true; @@ -121,6 +265,7 @@ void CreateInstanceDialog::changePage(int d) // first/last page if (d > 0) { + // forwards for (;;) { ++i; @@ -133,6 +278,7 @@ void CreateInstanceDialog::changePage(int d) } } } else { + // backwards for (;;) { if (i == 0) { break; @@ -151,94 +297,6 @@ void CreateInstanceDialog::changePage(int d) } } - -class Failed {}; - -class DirectoryCreator -{ -public: - DirectoryCreator(const DirectoryCreator&) = delete; - DirectoryCreator& operator=(const DirectoryCreator&) = delete; - - static std::unique_ptr<DirectoryCreator> create( - const QDir& target, std::function<void (QString)> log) - { - return std::unique_ptr<DirectoryCreator>(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<void (QString)> m_logger; - - DirectoryCreator(const QDir& target, std::function<void (QString)> 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<cs.size(); ++i) { - d = d.filePath(cs[i]); - - if (!d.exists()) { - m_logger(QObject::tr("Creating %1").arg(d.path())); - const auto r = shell::CreateDirectories(d); - - if (!r) { - m_logger(r.toString()); - throw Failed(); - } - - m_created.push_back(d); - } - } - } - catch(...) - { - rollback(); - throw; - } - } - -private: - std::vector<QDir> m_created; -}; - void CreateInstanceDialog::finish() { ui->creationLog->clear(); @@ -260,6 +318,8 @@ void CreateInstanceDialog::finish() { std::vector<std::unique_ptr<DirectoryCreator>> 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 +328,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 +362,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 +384,15 @@ void CreateInstanceDialog::finish() throw Failed(); } + + // committing all the directories so they don't get deleted for (auto& d : dirs) { d->commit(); } logCreation(tr("Done.")); - if (ui->hideIntro->isChecked()) { - GlobalSettings::setHideCreateInstanceIntro(true); - } - + // launch the new instance if (ui->launch->isChecked()) { InstanceManager::singleton().setCurrentInstance(ci.instanceName); @@ -338,15 +400,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 } } @@ -413,89 +476,67 @@ 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 +bool CreateInstanceDialog::switching() const { - return getSelected(&cid::Page::selectedGameLocation); + return m_switching; } -QString CreateInstanceDialog::gameVariant() const +CreateInstanceDialog::CreationInfo +CreateInstanceDialog::rawCreationInfo() const { - return getSelected(&cid::Page::selectedGameVariant); -} + const auto iniFilename = QString::fromStdWString(AppConfig::iniFileName()); -QString CreateInstanceDialog::instanceName() const -{ - return getSelected(&cid::Page::selectedInstanceName); -} + CreationInfo ci; -QString CreateInstanceDialog::dataPath() const -{ - QString s; + 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.game); + ci.instanceName = getSelected(&cid::Page::selectedInstanceName); + ci.paths = getSelected(&cid::Page::selectedPaths); - if (instanceType() == Portable) { - s = QDir(InstanceManager::portablePath()).absolutePath(); + if (ci.type == Portable) { + ci.dataPath = QDir(InstanceManager::singleton().portablePath()).absolutePath(); } else { - s = InstanceManager::singleton().instancePath(instanceName()); + ci.dataPath = InstanceManager::singleton().instancePath(ci.instanceName); } - return QDir::toNativeSeparators(s); -} - -CreateInstanceDialog::Paths CreateInstanceDialog::paths() const -{ - return getSelected(&cid::Page::selectedPaths); -} + ci.dataPath = QDir::toNativeSeparators(ci.dataPath); + ci.iniPath = ci.dataPath + "/" + iniFilename; -bool CreateInstanceDialog::switching() const -{ - return m_switching; + return ci; } -void fixVarDir(QString& path, const std::wstring& defaultDir) +CreateInstanceDialog::CreationInfo +CreateInstanceDialog::creationInfo() const { - if (path.isEmpty()) { - path = cid::makeDefaultPath(defaultDir); - } else if (!path.contains(PathSettings::BaseDirVariable)) { - path = QDir(path).absolutePath(); - } + 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 - path = QDir::toNativeSeparators(path); -} + if (path.isEmpty()) { + path = cid::makeDefaultPath(defaultDir); + } else if (!path.contains(PathSettings::BaseDirVariable)) { + path = QDir(path).absolutePath(); + } -void fixDirPath(QString& path) -{ - path = QDir::toNativeSeparators(QDir(path).absolutePath()); -} + path = QDir::toNativeSeparators(path); + }; -void fixFilePath(QString& path) -{ - path = QDir::toNativeSeparators(QFileInfo(path).absolutePath()); -} + auto fixDirPath = [](QString& path) + { + path = QDir::toNativeSeparators(QDir(path).absolutePath()); + }; -CreateInstanceDialog::CreationInfo CreateInstanceDialog::creationInfo() const -{ - const auto iniFilename = QString::fromStdWString(AppConfig::iniFileName()); + auto fixFilePath = [](QString& path) + { + path = QDir::toNativeSeparators(QFileInfo(path).absolutePath()); + }; - CreationInfo ci; - 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.instanceName = getSelected(&cid::Page::selectedInstanceName); - ci.paths = getSelected(&cid::Page::selectedPaths); - ci.dataPath = dataPath(); - ci.iniPath = ci.dataPath + "/" + iniFilename; + auto ci = rawCreationInfo(); fixDirPath(ci.paths.base); fixFilePath(ci.paths.ini); diff --git a/src/createinstancedialog.h b/src/createinstancedialog.h index 25e383eb..8e8fe517 100644 --- a/src/createinstancedialog.h +++ b/src/createinstancedialog.h @@ -10,11 +10,30 @@ 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: + enum class Actions + { + Find = 1 + }; + + // instance type + // enum Types { NoType = 0, @@ -22,6 +41,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 +57,8 @@ public: auto operator<=>(const Paths&) const = default; }; + // all the info filled in the various pages + // struct CreationInfo { Types type; @@ -53,10 +78,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 <class Page> void setSinglePage(const QString& instanceName) { @@ -71,6 +97,8 @@ public: setSinglePageImpl(instanceName); } + // returns the page having the give path, or null + // template <class Page> Page* getPage() { @@ -83,24 +111,60 @@ public: return nullptr; } - void next(); + + // moves to the next page; if `allowFinish` is true, calls finish() if + // currently on the last page + // + void next(bool allowFinish=true); + + // 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 +177,39 @@ private: bool m_singlePage; + // creates a shortcut for the given sequence + // + void addShortcut(QKeySequence seq, std::function<void ()> 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); - template <class T> - 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 <class MF, class... Args> + auto getSelected(MF mf, Args&&... args) const { + // return type + using T = decltype((std::declval<cid::Page>().*mf)(std::forward<Args>(args)...)); + for (auto&& p : m_pages) { - const auto t = (p.get()->*mf)(); + const auto t = (p.get()->*mf)(std::forward<Args>(args)...); if (t != T()) { return t; } @@ -127,12 +217,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/createinstancedialog.ui b/src/createinstancedialog.ui index b10d74a7..86fa900b 100644 --- a/src/createinstancedialog.ui +++ b/src/createinstancedialog.ui @@ -86,7 +86,7 @@ <item> <widget class="LinkLabel" name="label_19"> <property name="text"> - <string><html><head/><body><p><a href="https://github.com/ModOrganizer2/modorganizer/wiki/Instance">More information</a></p></body></html></string> + <string><html><head/><body><p><a href="https://github.com/ModOrganizer2/modorganizer/wiki/Instances">More information</a></p></body></html></string> </property> <property name="openExternalLinks"> <bool>true</bool> @@ -1102,6 +1102,9 @@ <property name="lineWrapMode"> <enum>QTextEdit::NoWrap</enum> </property> + <property name="readOnly"> + <bool>true</bool> + </property> </widget> </item> <item> @@ -1112,6 +1115,9 @@ <property name="textInteractionFlags"> <set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> </property> + <property name="placeholderText"> + <string>Instance creation log</string> + </property> </widget> </item> <item> diff --git a/src/createinstancedialogpages.cpp b/src/createinstancedialogpages.cpp index 26f2f61d..882776df 100644 --- a/src/createinstancedialogpages.cpp +++ b/src/createinstancedialogpages.cpp @@ -15,12 +15,29 @@ namespace cid using namespace MOBase; using MOBase::TaskDialog; +// returns %base_dir%/dir +// QString makeDefaultPath(const std::wstring& dir) { return QDir::toNativeSeparators(PathSettings::makeDefaultPath( QString::fromStdWString(dir))); } +QString toLocalizedString(CreateInstanceDialog::Types t) +{ + 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("?")); + } +} + PlaceholderLabel::PlaceholderLabel(QLabel* label) @@ -42,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) { } @@ -54,6 +72,7 @@ bool Page::ready() const bool Page::skip() const { + // setSkip() overrides this if it's true return m_skip || doSkip(); } @@ -62,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; @@ -82,6 +107,12 @@ void Page::next() m_dlg.next(); } +bool Page::action(CreateInstanceDialog::Actions a) +{ + // no-op + return false; +} + CreateInstanceDialog::Types Page::selectedInstanceType() const { @@ -101,7 +132,7 @@ QString Page::selectedGameLocation() const return {}; } -QString Page::selectedGameVariant() const +QString Page::selectedGameVariant(MOBase::IPluginGame*) const { // no-op return {}; @@ -121,27 +152,32 @@ 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; } TypePage::TypePage(CreateInstanceDialog& dlg) : Page(dlg), m_type(CreateInstanceDialog::NoType) { + // replace placeholders with actual paths 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())); + // disable portable button if it already exists if (InstanceManager::singleton().portableInstanceExists()) { ui->createPortable->setEnabled(false); ui->portableExistsLabel->setVisible(true); @@ -187,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()) @@ -204,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(); }); @@ -214,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) { @@ -239,26 +295,50 @@ 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")); + &m_dlg, QObject::tr("Find game installation for %1") + .arg(game->gameName())); 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) { + // 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; } } } + + // select this plugin, if any m_selection = checked; + + // update the button associated with it in case the paths have changed + updateButton(checked); + + // toggle it on selectButton(checked); + updateNavigation(); if (checked) { + // automatically move to the next page when a game is selected next(); } } @@ -274,22 +354,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"; @@ -314,10 +405,12 @@ std::vector<IPluginGame*> GamePage::sortedGamePlugins() const { std::vector<IPluginGame*> v; + // all game plugins for (auto* game : m_pc.plugins<IPluginGame>()) { v.push_back(game); } + // natsort std::sort(v.begin(), v.end(), [](auto* a, auto* b) { return (naturalCompare(a->gameName(), b->gameName()) < 0); }); @@ -325,6 +418,15 @@ std::vector<IPluginGame*> GamePage::sortedGamePlugins() const return v; } +void GamePage::createGames() +{ + m_games.clear(); + + for (auto* game : sortedGamePlugins()) { + m_games.push_back(std::make_unique<Game>(game)); + } +} + GamePage::Game* GamePage::findGame(IPluginGame* game) { for (auto& g : m_games) { @@ -336,13 +438,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>(game)); - } + updateButton(g); + + QObject::connect(g->button, &QAbstractButton::clicked, [g, this] { + select(g->game); + }); +} + +void GamePage::addButton(QAbstractButton* b) +{ + auto* ly = static_cast<QVBoxLayout*>(ui->games->layout()); + + // insert before the stretch + ly->insertWidget(ly->count() - 1, b); } void GamePage::updateButton(Game* g) @@ -399,6 +512,31 @@ void GamePage::selectButton(Game* g) } } +void GamePage::clearButtons() +{ + auto* ly = static_cast<QVBoxLayout*>(ui->games->layout()); + + ui->games->setUpdatesEnabled(false); + + // delete all children + qDeleteAll(ui->games->findChildren<QWidget*>("", 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); + + for (auto& g : m_games) { + // all buttons have been deleted + g->button = nullptr; + } +} + QCommandLinkButton* GamePage::createCustomButton() { auto* b = new QCommandLinkButton; @@ -414,69 +552,39 @@ 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(); clearButtons(); - for (auto& g : m_games) { - g->button = nullptr; + Game* firstButton = nullptr; + for (auto& g : m_games) { if (!showAll && !g->installed) { // not installed continue; } if (!m_filter.matches(g->game->gameName())) { + // filtered out continue; } createGameButton(g.get()); addButton(g->button); + + if (!firstButton) { + firstButton = g.get(); + } } + // browse button addButton(createCustomButton()); -} - -void GamePage::clearButtons() -{ - auto* ly = static_cast<QVBoxLayout*>(ui->games->layout()); - - ui->games->setUpdatesEnabled(false); - - // delete all children - qDeleteAll(ui->games->findChildren<QWidget*>("", 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<QVBoxLayout*>(ui->games->layout()); - - // insert before the stretch - ly->insertWidget(ly->count() - 1, b); + if (firstButton) { + firstButton->button->setDefault(true); + } } GamePage::Game* GamePage::checkInstallation(const QString& path, Game* g) @@ -487,13 +595,22 @@ 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<IPluginGame>()) { + if (gg->looksValid(path)) { + otherGame = gg; + break; + } + } + if (otherGame == g->game) { // shouldn't happen, but okay return g; } if (otherGame) { + // an alternative was found, ask the user about it auto* confirmedGame = confirmOtherGame(path, g->game, otherGame); if (!confirmedGame) { @@ -523,17 +640,6 @@ GamePage::Game* GamePage::checkInstallation(const QString& path, Game* g) return g; } -IPluginGame* GamePage::findAnotherGame(const QString& path) -{ - for (auto* otherGame : m_pc.plugins<IPluginGame>()) { - if (otherGame->looksValid(path)) { - return otherGame; - } - } - - return nullptr; -} - bool GamePage::confirmUnknown(const QString& path, IPluginGame* game) { const auto r = TaskDialog(&m_dlg) @@ -606,26 +712,29 @@ 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(); } bool VariantsPage::doSkip() const { - auto* g = m_dlg.game(); + auto* g = m_dlg.rawCreationInfo().game; if (!g) { // shouldn't happen return true; } - const auto variants = g->gameVariants(); - return (variants.size() < 2); + return (g->gameVariants().size() < 2); } -void VariantsPage::activated() +void VariantsPage::doActivated(bool) { - auto* g = m_dlg.game(); + auto* g = m_dlg.rawCreationInfo().game; if (m_previousGame != g) { + // recreate the list, the game has changed m_previousGame = g; m_selection = ""; fillList(); @@ -634,9 +743,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); @@ -646,20 +757,18 @@ void VariantsPage::select(const QString& variant) updateNavigation(); if (!m_selection.isEmpty()) { + // automatically move to the next page when a variant is selected next(); } } -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(); - if (variants.size() < 2) { + if (game->gameVariants().size() < 2) { return {}; } else { return m_selection; @@ -671,14 +780,14 @@ 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; } - 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); @@ -689,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); + } } @@ -699,21 +812,26 @@ NamePage::NamePage(CreateInstanceDialog& dlg) : { QObject::connect( ui->instanceName, &QLineEdit::textEdited, [&]{ onChanged(); }); + + QObject::connect( + ui->instanceName, &QLineEdit::returnPressed, [&]{ next(); }); } bool NamePage::ready() const { + // checked when textboxes change or when the page is activated return m_okay; } bool NamePage::doSkip() const { - return (m_dlg.instanceType() == CreateInstanceDialog::Portable); + // portable instances have no name + return (m_dlg.rawCreationInfo().type == CreateInstanceDialog::Portable); } -void NamePage::activated() +void NamePage::doActivated(bool) { - auto* g = m_dlg.game(); + auto* g = m_dlg.rawCreationInfo().game; if (!g) { // shouldn't happen, next should be disabled return; @@ -721,13 +839,15 @@ 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); m_modified = false; } - updateWarnings(); + verify(); } QString NamePage::selectedInstanceName() const @@ -743,13 +863,12 @@ QString NamePage::selectedInstanceName() const void NamePage::onChanged() { m_modified = true; - updateWarnings(); + verify(); } -void NamePage::updateWarnings() +void NamePage::verify() { - const auto root = InstanceManager::singleton().instancesPath(); - + const auto root = InstanceManager::singleton().globalInstancesRootPath(); m_okay = checkName(root, ui->instanceName->text()); updateNavigation(); } @@ -797,14 +916,31 @@ 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(); }); - 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(); }); + 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(); }); @@ -812,26 +948,36 @@ PathsPage::PathsPage(CreateInstanceDialog& dlg) : ui->pathPages->setCurrentIndex(0); } - bool PathsPage::ready() const { - return checkPaths(); + // set when the page is activated, textboxes are changed or the advanced + // checkbox is toggled + return m_okay; } -void PathsPage::activated() +void PathsPage::doActivated(bool firstTime) { - 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; + // 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.game()->gameName()); + m_label.setText(m_dlg.rawCreationInfo().game->gameName()); m_lastInstanceName = name; m_lastType = type; + + if (firstTime) { + ui->location->setFocus(); + } } CreateInstanceDialog::Paths PathsPage::selectedPaths() const @@ -857,21 +1003,39 @@ void PathsPage::onChanged() updateNavigation(); } -bool PathsPage::checkPaths() const +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()) { - return + // checking advanced paths + 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); + // checking simple path + 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); } @@ -883,6 +1047,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); @@ -896,19 +1063,21 @@ void PathsPage::onAdvanced() void PathsPage::setPaths(const QString& name, bool force) { - QString path; + QString basePath; - if (m_dlg.instanceType() == CreateInstanceDialog::Portable) { - path = InstanceManager::portablePath(); + if (m_dlg.rawCreationInfo().type == CreateInstanceDialog::Portable) { + basePath = InstanceManager::singleton().portablePath(); } else { - const auto root = InstanceManager::singleton().instancesPath(); - path = root + "/" + name; + const auto root = InstanceManager::singleton().globalInstancesRootPath(); + basePath = root + "/" + name; } - path = QDir::toNativeSeparators(QDir::cleanPath(path)); + basePath = QDir::toNativeSeparators(QDir::cleanPath(basePath)); + + // all paths are set regardless of advanced checkbox - setIfEmpty(ui->location, path, force); - setIfEmpty(ui->base, path, force); + setIfEmpty(ui->location, basePath, force); + setIfEmpty(ui->base, basePath, force); setIfEmpty(ui->downloads, makeDefaultPath(AppConfig::downloadPath()), force); setIfEmpty(ui->mods, makeDefaultPath(AppConfig::modsPath()), force); @@ -925,8 +1094,10 @@ void PathsPage::setIfEmpty(QLineEdit* e, const QString& path, bool force) bool PathsPage::checkPath( QString path, - PlaceholderLabel& existsLabel, PlaceholderLabel& invalidLabel) const + PlaceholderLabel& existsLabel, PlaceholderLabel& invalidLabel) { + auto& m = InstanceManager::singleton(); + bool exists = false; bool invalid = false; bool empty = false; @@ -938,11 +1109,11 @@ bool PathsPage::checkPath( } else { const QDir d(path); - if (InstanceManager::singleton().validInstanceName(d.dirName())) { - if (m_dlg.instanceType() == CreateInstanceDialog::Portable) { + 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 { @@ -999,6 +1170,7 @@ NexusPage::~NexusPage() = default; bool NexusPage::ready() const { + // this page is optional return true; } @@ -1007,71 +1179,56 @@ bool NexusPage::doSkip() const return m_skip; } -void NexusPage::activated() -{ -} - ConfirmationPage::ConfirmationPage(CreateInstanceDialog& dlg) : Page(dlg) { } -void ConfirmationPage::activated() +void ConfirmationPage::doActivated(bool) { ui->review->setPlainText(makeReview()); 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; - 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..08953a74 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,73 @@ 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; - virtual void activated(); + // called every time a page is shown in the screen + // + 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(); + + // 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; + + // returns the game plugin + // virtual MOBase::IPluginGame* selectedGame() const; + + // returns the game directory + // virtual QString selectedGameLocation() const; - virtual QString selectedGameVariant() 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: @@ -57,11 +117,22 @@ 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; }; +// introduction page, can be disabled by a global setting +// class IntroPage : public Page { public: @@ -69,179 +140,480 @@ 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; }; +// 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(); +protected: + // focuses global instance button on first activation + // + void doActivated(bool firstTime) override; + private: CreateInstanceDialog::Types m_type; }; +// 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; + + // handles find + // + bool action(CreateInstanceDialog::Actions a) 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<std::unique_ptr<Game>> 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<MOBase::IPluginGame*> 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); + + // 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); + + // 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; - void activated() override; - QString selectedGameVariant() 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 doActivated(bool firstTime) 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<QCommandLinkButton*> 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; - void activated() 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 doActivated(bool firstTime) override; + + // 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; - void activated() 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 doActivated(bool firstTime) 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; + + // 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(); + + // 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<NexusConnectionUI> 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); - void activated() override; + // recreates the log with the latest settings + // + void doActivated(bool firstTime) 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; }; 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 <log.h> #include <utility.h> @@ -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 @@ -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/instancemanager.cpp b/src/instancemanager.cpp index 8163149b..4191f850 100644 --- a/src/instancemanager.cpp +++ b/src/instancemanager.cpp @@ -21,9 +21,14 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #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 <report.h> #include <iplugingame.h> #include <utility.h> @@ -38,7 +43,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. 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 +54,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 +67,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; @@ -79,7 +89,7 @@ QString Instance::profileName() const QString Instance::iniPath() const { - return InstanceManager::iniPath(m_dir); + return InstanceManager::singleton().iniPath(m_dir); } bool Instance::isPortable() const @@ -87,15 +97,32 @@ 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() if (m_gameName.isEmpty()) { if (auto v=s.game().name()) m_gameName = *v; @@ -106,35 +133,71 @@ Instance::SetupResults Instance::setup(PluginContainer& plugins) m_gameDir = *v; } - const auto r = getGamePlugin(plugins); - if (r != SetupResults::Ok) { - return r; - } - 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::Okay) { + 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); } - 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::Okay; +} +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); 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); - - m_plugin->setGamePath(m_gameDir); - - return SetupResults::Ok; + } } void Instance::setGame(const QString& name, const QString& dir) @@ -185,7 +248,7 @@ Instance::SetupResults Instance::getGamePlugin(PluginContainer& plugins) } m_plugin = game; - return SetupResults::Ok; + return SetupResults::Okay; } } @@ -209,7 +272,7 @@ Instance::SetupResults Instance::getGamePlugin(PluginContainer& plugins) m_plugin = game; m_gameName = game->gameName(); - return SetupResults::Ok; + return SetupResults::Okay; } } @@ -238,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 " @@ -262,7 +325,6 @@ Instance::SetupResults Instance::getGamePlugin(PluginContainer& plugins) } } - void Instance::getProfile(const Settings& s) { if (!m_profile.isEmpty()) { @@ -284,6 +346,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::Object> 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<Object> 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<Object> 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<Object> 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<Object> 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<Object> 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<Object> 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() { @@ -299,48 +531,44 @@ 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<Instance> InstanceManager::currentInstance() const { - const QString profile = m_overrideProfile ? m_overrideProfileName : ""; + const QString profile = m_overrideProfileName ? + *m_overrideProfileName : ""; - if (portableInstallIsLocked()) { - // force portable instance - return Instance(QDir(portablePath()), true, profile); - } + const QString name = m_overrideInstanceName ? + *m_overrideInstanceName : GlobalSettings::currentInstance(); - QString name; - if (m_overrideInstance) - name = m_overrideInstanceName; - else - name = GlobalSettings::currentInstance(); + if (!allowedToChangeInstance()) { + // force portable instance + 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() { setCurrentInstance(""); - m_overrideInstance = false; + m_overrideInstanceName = {}; } void InstanceManager::setCurrentInstance(const QString &name) @@ -350,30 +578,31 @@ 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 QString& instanceDir) const { - return instanceDir.filePath(QString::fromStdWString(AppConfig::iniFileName())); + return QDir(instanceDir).filePath( + QString::fromStdWString(AppConfig::iniFileName())); } -std::vector<QDir> InstanceManager::instancePaths() const +std::vector<QString> InstanceManager::globalInstancePaths() const { const std::set<QString> ignore = { "cache", "qtwebengine", }; - const QDir root(instancesPath()); + const QDir root(globalInstancesRootPath()); const auto dirs = root.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - std::vector<QDir> list; + std::vector<QString> list; for (auto&& d : dirs) { if (!ignore.contains(QFileInfo(d).fileName().toLower())) { @@ -384,24 +613,12 @@ std::vector<QDir> 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,23 +629,21 @@ 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( - const QDir& instanceDir, const PluginContainer& plugins) const + const QString& instanceDir, const PluginContainer& plugins) const { const QString ini = iniPath(instanceDir); + // reading ini Settings s(ini); if (s.iniStatus() != QSettings::NoError) @@ -437,6 +652,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 +664,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 +673,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 +685,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 +693,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<IPluginGame>()) { + if (game->looksValid(instanceDir)) { + return game; + } + } + + return nullptr; } @@ -485,6 +711,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 +726,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); } @@ -529,3 +756,198 @@ bool InstanceManager::validInstanceName(const QString& instanceName) const return (instanceName == sanitizeInstanceName(instanceName)); } + + + +std::optional<Instance> 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()) { + // 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 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<cid::GamePage>(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<cid::GamePage>()->select( + instance.gamePlugin(), instance.gameDirectory()); + + // only show the variant page + dlg.setSinglePage<cid::VariantsPage>(instance.name()); + + dlg.show(); + dlg.activateWindow(); + dlg.raise(); + + if (dlg.exec() != QDialog::Accepted) { + return SetupInstanceResults::Exit; + } + + // 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::Okay: + { + // 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())); + + return SetupInstanceResults::SelectAnother; + } + + 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())); + + 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 " + "may have been deleted by an antivirus. Select another instance.") + .arg(instance.name()).arg(instance.gameName())); + + return SetupInstanceResults::SelectAnother; + } + + 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 " + "the game plugin '%3' doesn't recognize it. Select the game managed " + "by this instance.") + .arg(instance.name()) + .arg(instance.gameDirectory()) + .arg(instance.gameName())); + + return selectGame(instance, pc); + } + + case Instance::SetupResults::MissingVariant: + { + // 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 79f1f30b..4f8b01c7 100644 --- a/src/instancemanager.h +++ b/src/instancemanager.h @@ -10,87 +10,363 @@ 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 { - Ok, + // instance is ready to be used + Okay, + + // 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 }; - Instance(QDir dir, bool portable, QString profileName={}); + // 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(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; this + // calls readFromIni() first + // + // 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, 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, 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; - QDir directory() const; + + // returns the instance directory as given in the constructor + // + 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 + // MOBase::IPluginGame* gamePlugin() const; + + // returns either: + // 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()) + // QString profileName() const; + + // returns the path to the INI file for this instance; the file may not + // exist + // QString iniPath() const; - bool isPortable() const; + + // whether this is the currently active instance in MO + // + 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<Object> 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; + // 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); + + // updates the ini with the given values and the ones found by setup() + // + void updateIni(); }; +// 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; + 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 + // 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<Instance> 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<QDir> 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<QString> 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 QString& instanceDir) const; private: InstanceManager(); - bool portableInstallIsLocked() const; private: - bool m_overrideInstance{false}; - QString m_overrideInstanceName; - bool m_overrideProfile{false}; - QString m_overrideProfileName; + std::optional<QString> m_overrideInstanceName; + std::optional<QString> m_overrideProfileName; }; + +// see setupInstance() +// +enum class SetupInstanceResults +{ + Okay, + 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 instance or empty if the +// user cancelled +// +std::optional<Instance> 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 Okay +// +SetupInstanceResults setupInstance(Instance& instance, PluginContainer& pc); + #endif // MODORGANIZER_INSTANCEMANAGER_INCLUDED diff --git a/src/instancemanagerdialog.cpp b/src/instancemanagerdialog.cpp index 3b6daeb8..b461d39d 100644 --- a/src/instancemanagerdialog.cpp +++ b/src/instancemanagerdialog.cpp @@ -13,292 +13,93 @@ using namespace MOBase; -class InstanceInfo +// 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) { -public: - struct Object - { - QString path; - bool mandatoryDelete; + const auto* game = InstanceManager::singleton() + .gamePluginForDirectory(i.directory(), pc); - 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; - } - }; + if (game) + return game->gameIcon(); + QPixmap empty(32, 32); + empty.fill(QColor(0, 0, 0, 0)); + return QIcon(empty); +} - InstanceInfo(QDir dir, bool isPortable) - : m_portable(isPortable) - { - setDir(dir); - } - - void setDir(const QDir& dir) - { - m_dir = dir; - m_settings.reset(new Settings(InstanceManager::iniPath(dir))); - } - - QString name() const - { - if (m_portable) { - return QObject::tr("Portable"); - } else { - return m_dir.dirName(); - } - } +// 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(); - QString gameName() const - { - if (auto n=m_settings->game().name()) { - if (auto e=m_settings->game().edition()) { - if (!e->isEmpty()) { - return *n + " (" + *e + ")"; - } - } + QDialog dlg(parent); + dlg.setWindowTitle(title); - return *n; - } else { - return {}; - } - } + auto* ly = new QVBoxLayout(&dlg); - QString gamePath() const - { - if (auto n=m_settings->game().directory()) { - return QDir::toNativeSeparators(*n); - } else { - return {}; - } - } + auto* bb = new QDialogButtonBox( + QDialogButtonBox::Cancel | QDialogButtonBox::Ok); - QString location() const - { - return QDir::toNativeSeparators(m_dir.path()); - } + auto* text = new QLineEdit(oldName); + text->selectAll(); - QString baseDirectory() const - { - return QDir::toNativeSeparators(m_settings->paths().base()); - } + auto* error = new QLabel; - QString iniFile() const - { - return InstanceManager::iniPath(m_dir); + if (!moreText.isEmpty()) { + auto* lb = new QLabel(moreText); + lb->setWordWrap(true); + ly->addWidget(lb); + ly->addSpacing(10); } - QIcon icon(const PluginContainer& plugins) const - { - const auto* game = InstanceManager::singleton().gamePluginForDirectory( - m_dir, plugins); - - if (game) - return game->gameIcon(); + auto* lb = new QLabel(label); + lb->setWordWrap(true); + ly->addWidget(lb); - QPixmap empty(32, 32); - empty.fill(QColor(0, 0, 0, 0)); - return QIcon(empty); - } + ly->addWidget(text); + ly->addWidget(error); + ly->addStretch(); + ly->addWidget(bb); - bool isPortable() const - { - return m_portable; - } + auto check = [&] { + bool okay = false; - bool isActive() const - { - auto& m = InstanceManager::singleton(); + 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 (auto i=m.currentInstance()) - { - if (m_portable) { - return i->isPortable(); + if ((name != oldName) && m.instanceExists(text->text())) { + error->setText(QObject::tr("An instance with this name already exists.")); } else { - return (i->name() == name()); - } - } - - return false; - } - - // returns a list of files and folders that must be deleted when deleting - // this instance - // - std::vector<Object> 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)); - }; - - - - - 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<Object> 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<Object> 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<Object> 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<Object> 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<Object> 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}); - } + okay = true; } } + error->setVisible(!okay); + bb->button(QDialogButtonBox::Ok)->setEnabled(okay); + }; - // contains all the directories and files to be deleted - std::vector<Object> all; - all.insert(all.end(), cleanDirs.begin(), cleanDirs.end()); - all.insert(all.end(), cleanFiles.begin(), cleanFiles.end()); + QObject::connect(text, &QLineEdit::textChanged, [&] { check(); }); + QObject::connect(bb, &QDialogButtonBox::accepted, [&]{ dlg.accept(); }); + QObject::connect(bb, &QDialogButtonBox::rejected, [&]{ dlg.reject(); }); - // mandatory on top - std::stable_sort(all.begin(), all.end()); + check(); - return all; + dlg.resize({400, 120}); + if (dlg.exec() != QDialog::Accepted) { + return {}; } -private: - const bool m_portable; - QDir m_dir; - std::unique_ptr<Settings> m_settings; -}; + return m.sanitizeInstanceName(text->text()); +} InstanceManagerDialog::~InstanceManagerDialog() = default; @@ -310,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); @@ -345,14 +146,40 @@ 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(); m_instances.clear(); - for (auto&& d : m.instancePaths()) { - m_instances.push_back(std::make_unique<InstanceInfo>(d, false)); + for (auto&& d : m.globalInstancePaths()) { + m_instances.push_back(std::make_unique<Instance>(d, false)); } // sort first, prepend portable after so it's always on top @@ -363,7 +190,12 @@ void InstanceManagerDialog::updateInstances() if (m.portableInstanceExists()) { m_instances.insert( m_instances.begin(), - std::make_unique<InstanceInfo>(m.portablePath(), true)); + std::make_unique<Instance>(m.portablePath(), true)); + } + + // read all inis, ignore errors + for (auto&& i : m_instances) { + i->readFromIni(); } } @@ -374,14 +206,14 @@ 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(); ++i) { 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); @@ -396,7 +228,7 @@ void InstanceManagerDialog::updateList() if (m_instances.empty()) { select(-1); } else { - if (sel == NoSel) { + if (sel == NoSelection) { if (prevSelIndex >= m_instances.size()) { sel = m_instances.size() - 1; } else { @@ -462,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) { @@ -475,74 +313,36 @@ void InstanceManagerDialog::openSelectedInstance() accept(); } -QString getInstanceName( - QWidget* parent, const QString& title, const QString& moreText, - const QString& label, const QString& oldName={}) +bool InstanceManagerDialog::confirmSwitch(const Instance& to) { - 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; + // 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 (!moreText.isEmpty()) { - auto* lb = new QLabel(moreText); - lb->setWordWrap(true); - ly->addWidget(lb); - ly->addSpacing(10); + // if there is are no settings, no instances are loaded and the confirmation + // wouldn't make sense + if (!s) { + return true; } - 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(); }); + if (!s->interface().showChangeGameConfirmation()) { + // user disabled confirmation + return true; + } - check(); + MOBase::TaskDialog dlg(this); - dlg.resize({400, 120}); - if (dlg.exec() != QDialog::Accepted) { - return {}; - } + 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 m.sanitizeInstanceName(text->text()); + return (r == QMessageBox::Ok); } void InstanceManagerDialog::rename() @@ -561,6 +361,8 @@ void InstanceManagerDialog::rename() return; } + + // getting new name const auto newName = getInstanceName( this, tr("Rename instance"), "", tr("Instance name"), i->name()); @@ -568,11 +370,16 @@ void InstanceManagerDialog::rename() return; } - const QString src = i->location(); + + // renaming + const QString src = i->directory(); const QString dest = QDir::toNativeSeparators( - QFileInfo(i->location()).dir().path() + "/" + newName); + 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"), @@ -582,15 +389,21 @@ void InstanceManagerDialog::rename() return; } + + // updating ui + auto newInstance = std::make_unique<Instance>(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 +417,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()); } } @@ -629,6 +442,8 @@ void InstanceManagerDialog::deleteInstance() return; } + // creating dialog + const auto Recycle = QMessageBox::Save; const auto Delete = QMessageBox::Yes; const auto Cancel = QMessageBox::Cancel; @@ -652,6 +467,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); @@ -676,6 +493,7 @@ void InstanceManagerDialog::deleteInstance() } + // gathering all the selected items QStringList selected; for (int i=0; i<list->count(); ++i) { @@ -691,10 +509,14 @@ void InstanceManagerDialog::deleteInstance() return; } + + // deleting if (!doDelete(selected, (r == Recycle))) { return; } + + // updating ui updateInstances(); updateList(); } @@ -707,6 +529,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; } @@ -756,7 +587,7 @@ void InstanceManagerDialog::createNew() updateInstances(); updateList(); - select(dlg.instanceName()); + select(dlg.creationInfo().instanceName); } std::size_t InstanceManagerDialog::singleSelectionIndex() const @@ -771,17 +602,7 @@ std::size_t InstanceManagerDialog::singleSelectionIndex() const return static_cast<std::size_t>(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 +612,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..86fe0dac 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,49 +21,126 @@ 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); + + // saves geometry + // + void done(int r) override; + +protected: + // restores geometry + // + void showEvent(QShowEvent* e) override; + private: static const std::size_t NoSelection = -1; std::unique_ptr<Ui::InstanceManagerDialog> ui; const PluginContainer& m_pc; - std::vector<std::unique_ptr<InstanceInfo>> m_instances; + std::vector<std::unique_ptr<Instance>> 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(); + // shows a confirmation to the user before switching + // + bool confirmSwitch(const Instance& to); + + + // 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..44f4ff1f 100644 --- a/src/instancemanagerdialog.ui +++ b/src/instancemanagerdialog.ui @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>689</width> - <height>413</height> + <width>884</width> + <height>539</height> </rect> </property> <property name="windowTitle"> @@ -56,7 +56,7 @@ <item> <widget class="LinkLabel" name="whatis"> <property name="text"> - <string><html><head/><body><p><a href="https://github.com/ModOrganizer2/modorganizer/wiki/Instance">What is an instance?</a></p></body></html></string> + <string><html><head/><body><p><a href="https://github.com/ModOrganizer2/modorganizer/wiki/Instances">What is an instance?</a></p></body></html></string> </property> <property name="openExternalLinks"> <bool>true</bool> @@ -330,7 +330,7 @@ <item> <widget class="QPushButton" name="deleteInstance"> <property name="text"> - <string>Delete instance</string> + <string>Delete instance...</string> </property> <property name="icon"> <iconset resource="resources.qrc"> 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/main.cpp b/src/main.cpp index a905df5d..554ed006 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,623 +1,53 @@ -/* -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 <http://www.gnu.org/licenses/>. -*/ - -#include "mainwindow.h" -#include "singleinstance.h" +#include "multiprocess.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 "env.h" +#include "thread_utils.h" #include "shared/util.h" -#include "shared/appconfig.h" - -#include <imoinfo.h> -#include <report.h> -#include <usvfs.h> #include <log.h> -#include <utility.h> - -// see addDllsToPath() below -#pragma comment(linker, "/manifestDependency:\"" \ - "name='dlls' " \ - "processorArchitecture='x86' " \ - "version='1.0.0.0' " \ - "type='win32' \"") - 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 - // 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; -LONG WINAPI onUnhandledException(_EXCEPTION_POINTERS* ptrs) -{ - const std::wstring& dumpPath = OrganizerCore::crashDumpsPath(); - - const int r = CreateMiniDump( - ptrs, OrganizerCore::getGlobalCrashDumpsType(), dumpPath.c_str()); - - if (r == 0) { - log::error("ModOrganizer has crashed, crash dump created."); - } else { - log::error( - "ModOrganizer has crashed, CreateMiniDump failed ({}, error {}).", - r, GetLastError()); - } - - if (g_prevExceptionFilter && ptrs) - return g_prevExceptionFilter(ptrs); - else - return EXCEPTION_CONTINUE_SEARCH; -} - -void onTerminate() noexcept -{ - __try - { - // force an exception to get a valid stack trace for this thread - *(int*)0 = 42; - } - __except - ( - onUnhandledException(GetExceptionInformation()), EXCEPTION_EXECUTE_HANDLER - ) - { - } - - if (g_prevTerminateHandler) { - g_prevTerminateHandler(); - } else { - std::abort(); - } -} - -void setExceptionHandlers() -{ - g_prevExceptionFilter = SetUnhandledExceptionFilter(onUnhandledException); - 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); -} - -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<QSplashScreen> 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<QSplashScreen>(image); - settings.geometry().centerOnMainWindowMonitor(splash.get()); - - splash->show(); - splash->activateWindow(); - - return splash; -} - -std::optional<int> 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 {}; -} - -std::optional<Instance> selectInstance() -{ - auto& m = InstanceManager::singleton(); - - NexusInterface ni(nullptr); - PluginContainer pc(nullptr); - pc.loadPlugins(); - - if (m.instancePaths().empty() && !m.portableInstanceExists()) { - // 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 -}; - +int forwardToPrimary(MOMultiProcess& multiProcess, const cl::CommandLine& cl); -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) +int main(int argc, char *argv[]) { - 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<cid::GamePage>(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); + TimeThis tt("main()"); - 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<cid::GamePage>(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<cid::GamePage>()->select( - instance.gamePlugin(), instance.gameDirectory()); - - dlg.setSinglePage<cid::VariantsPage>(instance.name()); - - dlg.show(); - dlg.activateWindow(); - dlg.raise(); - - if (dlg.exec() != QDialog::Accepted) { - return SetupInstanceResults::Exit; - } - - instance.setVariant(dlg.creationInfo().gameVariant); - - return SetupInstanceResults::TryAgain; - } + MOShared::SetThisThreadName("main"); + setExceptionHandlers(); - default: - { - return SetupInstanceResults::Exit; - } + cl::CommandLine cl; + if (auto r=cl.run(GetCommandLineW())) { + return *r; } -} - -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); - - if (InstanceManager::isPortablePath(dataPath)) { - log::debug("this is a portable instance"); - } + initLogging(); - log::info("working directory: {}", QDir::currentPath()); + QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + MOApplication app(cl, argc, argv); - if (!instance.secondary()) { - purgeOldFiles(); + MOMultiProcess multiProcess(cl.multiple()); + if (multiProcess.ephemeral()) { + return forwardToPrimary(multiProcess, cl); } - 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(); - sanityChecks(env); - - const auto moduleNotification = env.onModuleLoaded(qApp, [](auto&& m) { - log::debug("loaded module {}", m.toString()); - checkIncompatibleModule(m); - }); - - // this must outlive `organizer` - std::unique_ptr<PluginContainer> 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<PluginContainer>(&organizer); - pluginContainer->loadPlugins(); - - for (;;) - { - const auto setupResult = setupInstance(currentInstance, *pluginContainer); - - if (setupResult == SetupInstanceResults::Ok) { - break; - } else if (setupResult == SetupInstanceResults::TryAgain) { - continue; - } else if (setupResult == SetupInstanceResults::SelectAnother) { - InstanceManager::singleton().clearCurrentInstance(); - return RestartExitCode; - } else { - return 1; - } - } - - checkPathsForSanity(*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=handleCommandLine(cl, organizer)) { - return *r; - } - - auto splash = createSplash(settings, dataPath, currentInstance.gamePlugin()); - - QString apiKey; - if (GlobalSettings::nexusApiKey(apiKey)) { - ni.getAccessManager()->apiCheck(apiKey); - } - - log::debug("initializing tutorials"); - QString tutorialsPath = QCoreApplication::applicationDirPath() + "/resources/tutorials/"; - TutorialManager::init(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); - - 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(); - - if (splash) { - // don't pass mainwindow as it just waits half a second for it - // instead of proceding - splash->finish(nullptr); - } - - tt.stop(); - - res = application.exec(); - mainWindow.close(); - - ni.getAccessManager()->setTopLevelWidget(nullptr); - } - - settings.geometry().resetIfNeeded(); - return res; - } - catch (const std::exception &e) - { - reportError(e.what()); - } + tt.stop(); - return 1; + 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"), @@ -627,118 +57,55 @@ int forwardToPrimary(SingleInstance& instance, const cl::CommandLine& cl) return 0; } -void resetForRestart(cl::CommandLine& cl) +LONG WINAPI onUnhandledException(_EXCEPTION_POINTERS* ptrs) { - LogModel::instance().clear(); - ResetExitFlag(); + const auto path = OrganizerCore::getGlobalCoreDumpPath(); + const auto type = OrganizerCore::getGlobalCoreDumpType(); - // make sure the log file isn't locked in case MO was restarted and - // the previous instance gets deleted - log::getDefault().setFile({}); + const auto r = env::coredump(path.empty() ? nullptr : path.c_str(), type); - // don't reprocess command line - cl.clear(); + if (r) { + log::error("ModOrganizer has crashed, core dump created."); + } else { + log::error("ModOrganizer has crashed, core dump failed"); + } + + // 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; } -int doOneRun( - cl::CommandLine& cl, MOApplication& application, SingleInstance& instance) +void onTerminate() noexcept { - TimeThis tt("doOneRun() to runApplication()"); - - // resets things when MO is "restarted" - resetForRestart(cl); - - auto& m = InstanceManager::singleton(); - auto currentInstance = m.currentInstance(); - - if (!currentInstance) + __try { - currentInstance = selectInstance(); - if (!currentInstance) { - return 1; - } + // force an exception to get a valid stack trace for this thread + *(int*)0 = 42; } - else + __except + ( + onUnhandledException(GetExceptionInformation()), EXCEPTION_EXECUTE_HANDLER + ) { - if (!currentInstance->directory().exists()) { - // the previously used instance doesn't exist anymore - - if (m.instanceNames().empty() && !m.portableInstanceExists()) { - criticalOnTop(QObject::tr( - "Instance at '%1' not found. You must create a new instance") - .arg(currentInstance->directory().absolutePath())); - } else { - criticalOnTop(QObject::tr( - "Instance at '%1' not found. Select another instance.") - .arg(currentInstance->directory().absolutePath())); - } - - currentInstance = selectInstance(); - if (!currentInstance) { - return 1; - } - } } - const QString dataPath = currentInstance->directory().path(); - application.setProperty("dataPath", dataPath); - - setExceptionHandlers(); - - if (!setLogDirectory(dataPath)) { - reportError("Failed to create log folder"); - InstanceManager::singleton().clearCurrentInstance(); - return 1; + if (g_prevTerminateHandler) { + g_prevTerminateHandler(); + } else { + std::abort(); } - - log::debug("command line: '{}'", QString::fromWCharArray(GetCommandLineW())); - - tt.stop(); - - return runApplication(application, cl, instance, dataPath, *currentInstance); } -int main(int argc, char *argv[]) +void setExceptionHandlers() { - cl::CommandLine cl; - - if (auto r=cl.run(GetCommandLineW())) { - return *r; - } - - TimeThis tt("main() to doOneRun()"); - - SetThisThreadName("main"); - - initLogging(); - auto application = MOApplication::create(argc, argv); - addDllsToPath(); - - SingleInstance instance(cl.multiple()); - if (instance.ephemeral()) { - return forwardToPrimary(instance, cl); + if (g_prevExceptionFilter) { + // already called + return; } - 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; - } + g_prevExceptionFilter = SetUnhandledExceptionFilter(onUnhandledException); + g_prevTerminateHandler = std::set_terminate(onTerminate); } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 25a1314e..74885e28 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2224,6 +2224,8 @@ void MainWindow::readSettings() } s.widgets().restoreIndex(ui->groupCombo); + s.widgets().restoreIndex(ui->tabWidget); + m_Filters->restoreState(s); { @@ -2299,6 +2301,7 @@ void MainWindow::storeSettings() s.widgets().saveIndex(ui->groupCombo); s.widgets().saveIndex(ui->executablesListBox); + s.widgets().saveIndex(ui->tabWidget); m_Filters->saveState(s); m_DataTab->saveState(s); @@ -5084,7 +5087,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 +5174,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/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; } <normaloff>:/MO/gui/instance_switch</normaloff>:/MO/gui/instance_switch</iconset> </property> <property name="text"> - <string>&Change Game...</string> + <string>Manage Instan&ces...</string> </property> <property name="iconText"> - <string>&Change Game</string> + <string>Manage Instan&ces...</string> </property> <property name="toolTip"> - <string>Open the Instance selection dialog to manage a different Game</string> + <string>Open the instance manager window to manage a different instance</string> </property> <property name="statusTip"> - <string>Open the Instance selection dialog to manage a different Game</string> + <string>Open the instance manager window to manage a different instance</string> </property> </action> <action name="actionExit"> @@ -1878,6 +1878,11 @@ p, li { white-space: pre-wrap; } <layoutdefault spacing="6" margin="11"/> <customwidgets> <customwidget> + <class>LinkLabel</class> + <extends>QLabel</extends> + <header location="global">linklabel.h</header> + </customwidget> + <customwidget> <class>MOBase::LineEditClear</class> <extends>QLineEdit</extends> <header>lineeditclear.h</header> @@ -1917,11 +1922,6 @@ p, li { white-space: pre-wrap; } <extends>QStatusBar</extends> <header>statusbar.h</header> </customwidget> - <customwidget> - <class>LinkLabel</class> - <extends>QLabel</extends> - <header location="global">linklabel.h</header> - </customwidget> </customwidgets> <resources> <include location="resources.qrc"/> diff --git a/src/moapplication.cpp b/src/moapplication.cpp index 290666af..9139acbb 100644 --- a/src/moapplication.cpp +++ b/src/moapplication.cpp @@ -18,6 +18,22 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. */
#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 "multiprocess.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 <iplugingame.h>
#include <report.h>
#include <utility.h>
#include <log.h>
@@ -30,9 +46,15 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <QStyleOption>
#include <QDebug>
+// see addDllsToPath() below
+#pragma comment(linker, "/manifestDependency:\"" \
+ "name='dlls' " \
+ "processorArchitecture='x86' " \
+ "version='1.0.0.0' " \
+ "type='win32' \"")
using namespace MOBase;
-
+using namespace MOShared;
class ProxyStyle : public QProxyStyle {
public:
@@ -71,9 +93,50 @@ public: };
-MOApplication::MOApplication(int& argc, char** argv)
- : QApplication(argc, argv)
+// 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(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);
@@ -81,12 +144,328 @@ MOApplication::MOApplication(int& argc, char** argv) m_DefaultStyle = style()->objectName();
setStyle(new ProxyStyle(style()));
+ addDllsToPath();
}
-MOApplication MOApplication::create(int& argc, char** argv)
+int MOApplication::run(MOMultiProcess& multiProcess)
{
- QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
- return MOApplication(argc, argv);
+ TimeThis tt("MOApplication run() to doOneRun()");
+
+ 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());
+
+ // MO runs in a loop because it can be restarted in several ways, such as
+ // when switching instances or changing some settings
+ for (;;)
+ {
+ try
+ {
+ // resets things when MO is "restarted"
+ resetForRestart();
+
+ tt.stop();
+ const auto r = doOneRun(multiProcess);
+ if (r == RestartExitCode) {
+ continue;
+ }
+
+ return r;
+ }
+ catch (const std::exception &e)
+ {
+ reportError(e.what());
+ return 1;
+ }
+ }
+}
+
+int MOApplication::doOneRun(MOMultiProcess& multiProcess)
+{
+ TimeThis tt("MOApplication::doOneRun() instances");
+
+ // 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);
+
+ if (!setLogDirectory(dataPath)) {
+ reportError("Failed to create log folder");
+ InstanceManager::singleton().clearCurrentInstance();
+ return 1;
+ }
+
+ log::debug("command line: '{}'", QString::fromWCharArray(GetCommandLineW()));
+
+ log::info(
+ "starting Mod Organizer version {} revision {} in {}, usvfs: {}",
+ createVersionInfo().displayString(3), GITID,
+ QCoreApplication::applicationDirPath(), MOShared::getUsvfsVersionString());
+
+ if (multiProcess.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());
+
+
+ tt.start("MOApplication::doOneRun() settings");
+
+ // deleting old files, only for the main instance
+ if (!multiProcess.secondary()) {
+ purgeOldFiles();
+ }
+
+ QWindowsWindowFunctions::setWindowActivationBehavior(
+ QWindowsWindowFunctions::AlwaysActivateWindow);
+
+
+ // loading settings
+ Settings settings(currentInstance->iniPath(), true);
+ log::getDefault().setLevel(settings.diagnostics().logLevel());
+ log::debug("using ini at '{}'", settings.filename());
+
+ OrganizerCore::setGlobalCoreDumpType(settings.diagnostics().coreDumpType());
+
+
+ tt.start("MOApplication::doOneRun() log and checks");
+
+ // logging and checking
+ 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> 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");
+ InstanceManager::singleton().clearCurrentInstance();
+ return 1;
+ }
+
+ // plugins
+ tt.start("MOApplication::doOneRun() plugins");
+ log::debug("initializing plugins");
+
+ pluginContainer = std::make_unique<PluginContainer>(&organizer);
+ pluginContainer->loadPlugins();
+
+ // instance
+ if (auto r=setupInstanceLoop(*currentInstance, *pluginContainer)) {
+ return *r;
+ }
+
+ if (currentInstance->isPortable()) {
+ log::debug("this is a portable instance");
+ }
+
+ tt.start("MOApplication::doOneRun() OrganizerCore setup");
+
+ sanity::checkPaths(*currentInstance->gamePlugin(), settings);
+
+ // setting up organizer core
+ 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
+ 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)) {
+ ni.getAccessManager()->apiCheck(apiKey);
+ }
+
+ // 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("");
+ }
+
+
+ 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
+ MOShared::SetThisThreadName("main");
+
+ // the nexus interface can show dialogs, make sure they're parented to the
+ // main window
+ ni.getAccessManager()->setTopLevelWidget(&mainWindow);
+
+ QObject::connect(&mainWindow, SIGNAL(styleChanged(QString)), this,
+ SLOT(setStyleFile(QString)));
+
+ QObject::connect(&multiProcess, SIGNAL(messageSent(QString)), &organizer,
+ SLOT(externalMessage(QString)));
+
+
+ log::debug("displaying main window");
+ mainWindow.show();
+ mainWindow.activateWindow();
+ splash.close();
+
+ tt.stop();
+
+ res = exec();
+ mainWindow.close();
+
+ // main window is about to be destroyed
+ ni.getAccessManager()->setTopLevelWidget(nullptr);
+ }
+
+ // reset geometry if the flag was set from the settings dialog
+ settings.geometry().resetIfNeeded();
+
+ return res;
+}
+
+std::optional<Instance> 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;
+}
+
+std::optional<int> MOApplication::setupInstanceLoop(
+ Instance& currentInstance, PluginContainer& pc)
+{
+ for (;;)
+ {
+ const auto setupResult = setupInstance(currentInstance, pc);
+
+ 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()
+{
+ // 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)
@@ -112,7 +491,6 @@ bool MOApplication::setStyleFile(const QString& styleName) return true;
}
-
bool MOApplication::notify(QObject* receiver, QEvent* event)
{
try {
@@ -132,7 +510,6 @@ bool MOApplication::notify(QObject* receiver, QEvent* event) }
}
-
void MOApplication::updateStyle(const QString& fileName)
{
if (QStyleFactory::keys().contains(fileName)) {
@@ -147,3 +524,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..91b8023a 100644 --- a/src/moapplication.h +++ b/src/moapplication.h @@ -23,14 +23,26 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <QApplication>
#include <QFileSystemWatcher>
+class Settings;
+class MOMultiProcess;
+class Instance;
+class PluginContainer;
+
+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);
+
+ // sets up everything, creates the main window and runs it
+ //
+ int run(MOMultiProcess& multiProcess);
+
+ virtual bool notify(QObject* receiver, QEvent* event);
public slots:
bool setStyleFile(const QString& style);
@@ -41,9 +53,32 @@ private slots: private:
QFileSystemWatcher m_StyleWatcher;
QString m_DefaultStyle;
+ cl::CommandLine& m_cl;
- MOApplication(int& argc, char** argv);
+ int doOneRun(MOMultiProcess& multiProcess);
+
+ std::optional<Instance> getCurrentInstance();
+ std::optional<int> setupInstanceLoop(Instance& currentInstance, PluginContainer& pc);
+ void purgeOldFiles();
+ void resetForRestart();
};
+class MOSplash
+{
+public:
+ MOSplash(
+ const Settings& settings, const QString& dataPath,
+ const MOBase::IPluginGame* game);
+
+ void close();
+
+private:
+ std::unique_ptr<QSplashScreen> ss_;
+
+ QString getSplashPath(
+ const Settings& settings, const QString& dataPath,
+ const MOBase::IPluginGame* game) const;
+};
+
#endif // MOAPPLICATION_H
diff --git a/src/singleinstance.cpp b/src/multiprocess.cpp index bd7ccc43..4ce58718 100644 --- a/src/singleinstance.cpp +++ b/src/multiprocess.cpp @@ -1,23 +1,4 @@ -/*
-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 <http://www.gnu.org/licenses/>.
-*/
-
-#include "singleinstance.h"
+#include "multiprocess.h"
#include "utility.h"
#include <report.h>
#include <log.h>
@@ -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/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 <QObject>
+#include <QSharedMemory>
+#include <QLocalServer>
+
+
+/**
+ * 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/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 <typename InputIterator> 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 <imoinfo.h>
#include <iplugindiagnose.h>
#include <versioninfo.h>
@@ -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/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<IPluginDiagnose>(m_Plugins).push_back(this);
if (m_Organizer) {
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 <log.h> #include <utility.h> +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 diff --git a/src/settings.cpp b/src/settings.cpp index 99b41e1f..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) { @@ -2110,23 +2116,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<CrashDumpsType>(m_Settings, - "Settings", "crash_dumps_type", CrashDumpsType::Mini); + return get<env::CoreDumpTypes>(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<int>(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 c8325ba2..689067d7 100644 --- a/src/settings.h +++ b/src/settings.h @@ -21,6 +21,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #define SETTINGS_H #include "loadmechanism.h" +#include "envdump.h" #include <filterwidget.h> #include <lootcli/lootcli.h> #include <questionboxmemory.h> @@ -195,6 +196,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 @@ -647,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); @@ -671,10 +676,32 @@ 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(); - 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 // 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 @@ <property name="orientation"> <enum>Qt::Vertical</enum> </property> - <property name="handleWidth"> - <number>2</number> - </property> <widget class="QWidget" name="widget" native="true"> <property name="sizePolicy"> <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> @@ -1055,6 +1052,18 @@ </sizepolicy> </property> <layout class="QHBoxLayout" name="horizontalLayout_6"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> <item> <widget class="QSplitter" name="splitter_2"> <property name="sizePolicy"> @@ -1066,11 +1075,20 @@ <property name="orientation"> <enum>Qt::Horizontal</enum> </property> - <property name="handleWidth"> - <number>2</number> - </property> <widget class="QWidget" name="widget" native="true"> <layout class="QVBoxLayout" name="verticalLayout_15"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> <item> <widget class="QTreeWidget" name="pluginsList"> <column> @@ -1095,6 +1113,18 @@ </widget> <widget class="QWidget" name="widget" native="true"> <layout class="QVBoxLayout" name="verticalLayout_7" stretch="0,1"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> <item> <layout class="QFormLayout" name="formLayout_2"> <item row="0" column="0"> @@ -1182,22 +1212,18 @@ </item> </layout> </widget> - <widget class="QWidget" name="verticalWidget" native="true"> + <widget class="QGroupBox" name="verticalGroupBox"> <property name="sizePolicy"> <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> <horstretch>0</horstretch> <verstretch>1</verstretch> </sizepolicy> </property> + <property name="title"> + <string>Blacklisted Plugins (use <del> to remove):</string> + </property> <layout class="QVBoxLayout" name="verticalLayout_16"> <item> - <widget class="QLabel" name="label_18"> - <property name="text"> - <string>Blacklisted Plugins (use <del> to remove):</string> - </property> - </widget> - </item> - <item> <widget class="QListWidget" name="pluginBlacklist"/> </item> </layout> @@ -1614,15 +1640,15 @@ programs you are intentionally running.</string> </widget> <customwidgets> <customwidget> - <class>ColorTable</class> - <extends>QTableWidget</extends> - <header>colortable.h</header> - </customwidget> - <customwidget> <class>LinkLabel</class> <extends>QLabel</extends> <header location="global">linklabel.h</header> </customwidget> + <customwidget> + <class>ColorTable</class> + <extends>QTableWidget</extends> + <header>colortable.h</header> + </customwidget> </customwidgets> <tabstops> <tabstop>tabWidget</tabstop> 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<int>(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<int>( - settings().diagnostics().crashDumpsType()); + settings().diagnostics().coreDumpType()); for (int i=0; i<ui->dumpsTypeBox->count(); ++i) { if (ui->dumpsTypeBox->itemData(i) == current) { @@ -99,10 +99,10 @@ void DiagnosticsSettingsTab::update() settings().diagnostics().setLogLevel( static_cast<log::Levels>(ui->logLevelBox->currentData().toInt())); - settings().diagnostics().setCrashDumpsType( - static_cast<CrashDumpsType>(ui->dumpsTypeBox->currentData().toInt())); + settings().diagnostics().setCoreDumpType( + static_cast<env::CoreDumpTypes>(ui->dumpsTypeBox->currentData().toInt())); - settings().diagnostics().setCrashDumpsMax(ui->dumpsMaxEdit->value()); + settings().diagnostics().setMaxCoreDumps(ui->dumpsMaxEdit->value()); settings().diagnostics().setLootLogLevel( static_cast<lootcli::LogLevels>(ui->lootLogLevel->currentData().toInt())); diff --git a/src/settingsdialognexus.cpp b/src/settingsdialognexus.cpp index 1ed4c58f..90e0dff1 100644 --- a/src/settingsdialognexus.cpp +++ b/src/settingsdialognexus.cpp @@ -346,6 +346,8 @@ NexusSettingsTab::NexusSettingsTab(Settings& s, SettingsDialog& d) QObject::connect(ui->clearCacheButton, &QPushButton::clicked, [&]{ on_clearCacheButton_clicked(); }); QObject::connect(ui->associateButton, &QPushButton::clicked, [&]{ on_associateButton_clicked(); }); + + updateNexusData(); } void NexusSettingsTab::update() 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
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
diff --git a/src/singleinstance.h b/src/singleinstance.h deleted file mode 100644 index 5f6c3633..00000000 --- a/src/singleinstance.h +++ /dev/null @@ -1,90 +0,0 @@ -/*
-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 <http://www.gnu.org/licenses/>.
-*/
-
-#ifndef SINGLEINSTANCE_H
-#define SINGLEINSTANCE_H
-
-#include <QObject>
-#include <QSharedMemory>
-#include <QLocalServer>
-
-
-/**
- * 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
- **/
-class SingleInstance : public QObject
-{
-
- Q_OBJECT
-
-public:
- // `allowMultiple`: if another instance is running, run this one
- // disconnected from the shared memory
- explicit SingleInstance(bool allowMultiple, QObject *parent = 0);
-
- /**
- * @return true if this instance's job is to forward data to the primary
- * instance through shared memory
- **/
- bool ephemeral() const
- {
- return m_Ephemeral;
- }
-
- // returns true if this is not the primary instance, but was allowed because
- // of the AllowMultiple flag
- //
- bool secondary() const
- {
- return !m_Ephemeral && !m_OwnsSM;
- }
-
- /**
- * send a message to the primary instance. This can be used to transmit download urls
- *
- * @param message message to send
- **/
- void sendMessage(const QString &message);
-
-signals:
-
- /**
- * @brief emitted when an ephemeral instance 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 // SINGLEINSTANCE_H
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<CrashDumpsType>(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<milliseconds>(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<milliseconds>(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 <http://www.gnu.org/licenses/>. #include <usvfsparameters.h> #include <log.h> #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); |
