diff options
| author | Seth Riley <17361645+Qudix@users.noreply.github.com> | 2020-11-06 12:02:43 -0600 |
|---|---|---|
| committer | Seth Riley <17361645+Qudix@users.noreply.github.com> | 2020-11-06 12:02:43 -0600 |
| commit | ed1085d5673e0d307a6172eaaa1be1e9d70e2234 (patch) | |
| tree | 1e068a2f08e106c8ce15be7034f19336836cc17a | |
| parent | 34e1d05c28bf4676ed8d3e97e969187a05d215b6 (diff) | |
| parent | b909677c3fc6a7b7a1993d341a2bd420715e292a (diff) | |
Merge branch 'master' of https://github.com/ModOrganizer2/modorganizer into master
72 files changed, 8028 insertions, 1920 deletions
diff --git a/dump_running_process.bat b/dump_running_process.bat index 4697fe5e..1245c75e 100644 --- a/dump_running_process.bat +++ b/dump_running_process.bat @@ -1,2 +1,2 @@ pushd "%~dp0" -start ModOrganizer.exe --crashdump +start ModOrganizer.exe crashdump diff --git a/modorganizer.natvis b/modorganizer.natvis index fe4a7ce2..6e21e099 100644 --- a/modorganizer.natvis +++ b/modorganizer.natvis @@ -8,5 +8,102 @@ file={m_wsFile} loaded={m_loaded} expanded={m_expanded} }}</DisplayString></Type> + <Type Name="QFileInfoPrivate"> + <DisplayString Condition="0 == this"><null></DisplayString> + <DisplayString>{fileEntry}</DisplayString> + <StringView>fileEntry</StringView> + <Expand> + <Item Name="QSharedData">*((Qt5Core.dll!QSharedData *) this)</Item> + <Item Name="fileEntry">fileEntry</Item> + <!-- + <Item Name="metaData">metaData</Item> + <Item Name="fileListsInitialized">fileListsInitialized</Item> + <Item Name="fileEngine">fileEngine</Item> + <Item Name="fileNames">fileNames</Item> + <Item Name="fileOwners">fileOwners</Item> + <Item Name="cachedFlags">cachedFlags</Item> + <Item Name="isDefaultConstructed">isDefaultConstructed</Item> + <Item Name="cache_enabled">cache_enabled</Item> + <Item Name="fileFlags">fileFlags</Item> + <Item Name="fileSize">fileSize</Item> + <Item Name="fileTimes">fileTimes</Item> + --> + </Expand> + </Type> + + <Type Name="QFileInfo"> + <DisplayString>{*((Qt5Core.dll!QFileInfoPrivate *) d_ptr.d)}</DisplayString> + <StringView>*((Qt5Core.dll!QFileInfoPrivate *) d_ptr.d)</StringView> + <Expand> + <Item Name="QFileInfoPrivate">*((Qt5Core.dll!QFileInfoPrivate *) d_ptr.d)</Item> + </Expand> + </Type> + + + <Type Name="QFileSystemEntry"> + <DisplayString Condition="0 == this"><null></DisplayString> + <DisplayString>{m_filePath}</DisplayString> + <StringView>m_filePath</StringView> + <!-- + <Expand> + <Item Name="m_filePath">m_filePath</Item> + <Item Name="m_nativeFilePath">m_nativeFilePath</Item> + <Item Name="m_lastSeparator">m_lastSeparator</Item> + <Item Name="m_firstDotInFileName">m_firstDotInFileName</Item> + <Item Name="m_lastDotInFileName">m_lastDotInFileName</Item> + </Expand> + --> + </Type> + + + <Type Name="QDirPrivate"> + <DisplayString Condition="0 == this"><null></DisplayString> + <DisplayString>{dirEntry}</DisplayString> + <StringView>dirEntry</StringView> + <Expand> + <Item Name="QSharedData">*((Qt5Core.dll!QSharedData *) this)</Item> + <Item Name="dirEntry">dirEntry</Item> + <Item Name="nameFilters">nameFilters</Item> + <Item Name="absoluteDirEntry">absoluteDirEntry</Item> + <!-- + <Item Name="metaData">metaData</Item> + <Item Name="fileListsInitialized">fileListsInitialized</Item> + <Item Name="fileEngine">fileEngine</Item> + <Item Name="files">files</Item> + <Item Name="fileInfos">fileInfos</Item> + <Item Name="sort">sort</Item> + <Item Name="filters">filters</Item> + --> + </Expand> + </Type> + + + <Type Name="QDir"> + <DisplayString>{*((Qt5Core.dll!QDirPrivate *) d_ptr.d)}</DisplayString> + <StringView>*((Qt5Core.dll!QDirPrivate *) d_ptr.d)</StringView> + <Expand> + <Item Name="QDirPrivate">*((Qt5Core.dll!QDirPrivate *) d_ptr.d)</Item> + </Expand> + </Type> + + + <Type Name="QFilePrivate"> + <DisplayString Condition="0 == this"><null></DisplayString> + <DisplayString>{fileName}</DisplayString> + <StringView>fileName</StringView> + <Expand> + <Item Name="QFileDevice">*((Qt5Core.dll!QFileDevice *) this)</Item> + <Item Name="fileName">fileName</Item> + </Expand> + </Type> + + + <Type Name="QFile"> + <DisplayString>{*((Qt5Core.dll!QFilePrivate *) d_ptr.d)}</DisplayString> + <StringView>*((Qt5Core.dll!QFilePrivate *) d_ptr.d)</StringView> + <Expand> + <Item Name="QFilePrivate">*((Qt5Core.dll!QFilePrivate *) d_ptr.d)</Item> + </Expand> + </Type> </AutoVisualizer> diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 782d6589..f2c31d37 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,6 +11,7 @@ endif() add_filter(NAME src/application GROUPS iuserinterface + commandline main moapplication moshortcut @@ -28,11 +29,11 @@ add_filter(NAME src/core GROUPS categories archivefiletree installationmanager - instancemanager loadmechanism nexusinterface nxmaccessmanager organizercore + plugincontainer apiuseraccount processrunner qdirfiletree @@ -83,6 +84,13 @@ add_filter(NAME src/executables GROUPS editexecutablesdialog ) +add_filter(NAME src/instances GROUPS + createinstancedialog + createinstancedialogpages + instancemanager + instancemanagerdialog +) + add_filter(NAME src/loot GROUPS loot lootdialog @@ -129,7 +137,6 @@ add_filter(NAME src/modlist GROUPS ) add_filter(NAME src/plugins GROUPS - plugincontainer pluginlist pluginlistsortproxy pluginlistview diff --git a/src/commandline.cpp b/src/commandline.cpp new file mode 100644 index 00000000..3f8a6b1a --- /dev/null +++ b/src/commandline.cpp @@ -0,0 +1,593 @@ +#include "commandline.h" +#include "env.h" +#include "shared/util.h" + +namespace cl +{ + +std::string pad_right(std::string s, std::size_t n, char c=' ') +{ + if (s.size() < n) + s.append(n - s.size() , c); + + return s; +} + +std::string table( + const std::vector<std::pair<std::string, std::string>>& v, + std::size_t indent, std::size_t spacing) +{ + std::size_t longest = 0; + + for (auto&& p : v) + longest = std::max(longest, p.first.size()); + + std::string s; + + for (auto&& p : v) + { + if (!s.empty()) + s += "\n"; + + s += + std::string(indent, ' ') + + pad_right(p.first, longest) + " " + + std::string(spacing, ' ') + + p.second; + } + + return s; + +} + + +CommandLine::CommandLine() +{ + createOptions(); + m_commands.push_back(std::make_unique<ExeCommand>()); + m_commands.push_back(std::make_unique<RunCommand>()); + m_commands.push_back(std::make_unique<CrashDumpCommand>()); + m_commands.push_back(std::make_unique<LaunchCommand>()); +} + +std::optional<int> CommandLine::run(const std::wstring& line) +{ + try + { + auto args = po::split_winmain(line); + if (!args.empty()) { + // remove program name + args.erase(args.begin()); + } + + auto parsed = po::wcommand_line_parser(args) + .options(m_allOptions) + .positional(m_positional) + .allow_unregistered() + .run(); + + po::store(parsed, m_vm); + po::notify(m_vm); + + auto opts = po::collect_unrecognized( + parsed.options, po::include_positional); + + + if (m_vm.count("command")) { + const auto commandName = m_vm["command"].as<std::string>(); + + for (auto&& c : m_commands) { + if (c->name() == commandName) { + // remove the command name itself + opts.erase(opts.begin()); + + try + { + po::wcommand_line_parser parser(opts); + + auto co = c->allOptions(); + parser.options(co); + + if (c->allow_unregistered()) { + parser.allow_unregistered(); + } + + auto pos = c->positional(); + parser.positional(pos); + + parsed = parser.run(); + + po::store(parsed, m_vm); + po::notify(m_vm); + + if (m_vm.count("help")) { + env::Console console; + std::cout << usage(c.get()) << "\n"; + return 0; + } + + return c->run(line, m_vm, opts); + } + catch(po::error& e) + { + env::Console console; + + std::cerr + << e.what() << "\n" + << usage(c.get()) << "\n"; + + return 1; + } + } + } + } + + if (m_vm.count("help")) { + env::Console console; + std::cout << usage() << "\n"; + return 0; + } + + if (!opts.empty()) { + const auto qs = QString::fromStdWString(opts[0]); + m_shortcut = qs; + + if (!m_shortcut.isValid()) { + if (isNxmLink(qs)) { + m_nxmLink = qs; + } else { + m_executable = qs; + } + } + + // remove the shortcut/nxm/executable + opts.erase(opts.begin()); + + for (auto&& o : opts) { + m_untouched.push_back(QString::fromStdWString(o)); + } + } + + return {}; + } + catch(po::error& e) + { + env::Console console; + + std::cerr + << e.what() << "\n" + << usage() << "\n"; + + return 1; + } +} + +void CommandLine::clear() +{ + m_vm.clear(); + m_shortcut = {}; + m_nxmLink = {}; +} + +void CommandLine::createOptions() +{ + m_visibleOptions.add_options() + ("help", "show this message") + ("multiple", "allow multiple MO processes to run; see below") + ("instance,i", po::value<std::string>(), "use the given instance (defaults to last used)") + ("profile,p", po::value<std::string>(), "use the given profile (defaults to last used)"); + + po::options_description options; + options.add_options() + ("command", po::value<std::string>(), "command") + ("subargs", po::value<std::vector<std::string> >(), "args"); + + m_positional + .add("command", 1) + .add("subargs", -1); + + m_allOptions.add(m_visibleOptions); + m_allOptions.add(options); +} + +std::string CommandLine::usage(const Command* c) const +{ + std::ostringstream oss; + + oss + << "\n" + << "Usage:\n"; + + if (c) { + oss + << " ModOrganizer.exe [global-options] " << c->usageLine() << "\n" + << "\n" + << "Command options:\n" + << c->visibleOptions() << "\n"; + } else { + oss + << " ModOrganizer.exe [options] [[command] [command-options]]\n" + << "\n" + << "Commands:\n"; + + std::vector<std::pair<std::string, std::string>> v; + for (auto&& c : m_commands) { + v.push_back({c->name(), c->description()}); + } + + oss + << table(v, 2, 4) << "\n" + << "\n"; + } + + oss + << "Global options:\n" + << m_visibleOptions << "\n"; + + if (!c) { + oss << "\n" << more() << "\n"; + } + + return oss.str(); +} + +bool CommandLine::multiple() const +{ + return (m_vm.count("multiple") > 0); +} + +std::optional<QString> CommandLine::profile() const +{ + if (m_vm.count("profile")) { + return QString::fromStdString(m_vm["profile"].as<std::string>()); + } + + return {}; +} + +std::optional<QString> CommandLine::instance() const +{ + if (m_shortcut.isValid() && m_shortcut.hasInstance()) { + return m_shortcut.instance(); + } else if (m_vm.count("instance")) { + return QString::fromStdString(m_vm["instance"].as<std::string>()); + } + + return {}; +} + +const MOShortcut& CommandLine::shortcut() const +{ + return m_shortcut; +} + +std::optional<QString> CommandLine::nxmLink() const +{ + return m_nxmLink; +} + +std::optional<QString> CommandLine::executable() const +{ + return m_executable; +} + +const QStringList& CommandLine::untouched() const +{ + return m_untouched; +} + +std::string CommandLine::more() const +{ + return + "Multiple processes\n" + " A note on terminology: 'instance' can either mean an MO process\n" + " that's running on the system, or a set of mods and profiles managed\n" + " by MO. To avoid confusion, the term 'process' is used below for the\n" + " former.\n" + " \n" + " --multiple can be used to allow multiple MO processes to run\n" + " simultaneously. This is unsupported and can create all sorts of weird\n" + " problems. To minimize these:\n" + " \n" + " 1) Never have multiple MO processes running that manage the same\n" + " game instance.\n" + " 2) If an executable is launched from an MO process, only this\n" + " process may launch executables until all processes are \n" + " terminated.\n" + " \n" + " It is recommended to close _all_ MO processes as soon as multiple\n" + " processes become unnecessary."; +} + + +std::string Command::name() const +{ + return meta().name; +} + +std::string Command::description() const +{ + return meta().description; +} + +std::string Command::usageLine() const +{ + return name() + " " + getUsageLine(); +} + +bool Command::allow_unregistered() const +{ + return false; +} + +po::options_description Command::allOptions() const +{ + po::options_description d; + + d.add(visibleOptions()); + d.add(getInternalOptions()); + + return d; +} + +po::options_description Command::visibleOptions() const +{ + po::options_description d(getVisibleOptions()); + + d.add_options() + ("help", "shows this message"); + + return d; +} + +po::positional_options_description Command::positional() const +{ + return getPositional(); +} + +std::string Command::getUsageLine() const +{ + return "[options]"; +} + +po::options_description Command::getVisibleOptions() const +{ + // no-op + return {}; +} + +po::options_description Command::getInternalOptions() const +{ + // no-op + return {}; +} + +po::positional_options_description Command::getPositional() const +{ + // no-op + return {}; +} + + +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, + std::vector<std::wstring> untouched) +{ + m_original = originalLine; + m_vm = vm; + m_untouched = untouched; + + return doRun(); +} + +const std::wstring& Command::originalCmd() const +{ + return m_original; +} + +const po::variables_map& Command::vm() const +{ + return m_vm; +} + +const std::vector<std::wstring>& Command::untouched() const +{ + return m_untouched; +} + + +po::options_description CrashDumpCommand::getVisibleOptions() const +{ + po::options_description d; + + d.add_options() + ("type", po::value<std::string>()->default_value("mini"), "mini|data|full"); + + return d; +} + +Command::Meta CrashDumpCommand::meta() const +{ + return {"crashdump", "writes a crashdump for a running process of MO"}; +} + +std::optional<int> CrashDumpCommand::doRun() +{ + env::Console console; + + const auto typeString = vm()["type"].as<std::string>(); + const auto type = env::coreDumpTypeFromString(typeString); + + // dump + const auto b = env::coredumpOther(type); + if (!b) { + std::wcerr << L"\n>>>> a minidump file was not written\n\n"; + } + + std::wcerr << L"Press enter to continue..."; + std::wcin.get(); + + return (b ? 0 : 1); +} + + +bool LaunchCommand::allow_unregistered() const +{ + return true; +} + +Command::Meta LaunchCommand::meta() const +{ + return {"launch", "(internal, do not use)"}; +} + +std::optional<int> LaunchCommand::doRun() +{ + // needs at least the working directory and process name + if (untouched().size() < 2) { + return 1; + } + + std::vector<std::wstring> arg; + auto args = UntouchedCommandLineArguments(2, arg); + + return SpawnWaitProcess(arg[1].c_str(), args); +} + +int LaunchCommand::SpawnWaitProcess(LPCWSTR workingDirectory, LPCWSTR commandLine) +{ + PROCESS_INFORMATION pi{ 0 }; + STARTUPINFO si{ 0 }; + si.cb = sizeof(si); + std::wstring commandLineCopy = commandLine; + + if (!CreateProcessW(NULL, &commandLineCopy[0], NULL, NULL, FALSE, 0, NULL, workingDirectory, &si, &pi)) { + // A bit of a problem where to log the error message here, at least this way you can get the message + // using a either DebugView or a live debugger: + std::wostringstream ost; + ost << L"CreateProcess failed: " << commandLine << ", " << GetLastError(); + OutputDebugStringW(ost.str().c_str()); + return -1; + } + + WaitForSingleObject(pi.hProcess, INFINITE); + + DWORD exitCode = (DWORD)-1; + ::GetExitCodeProcess(pi.hProcess, &exitCode); + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + return static_cast<int>(exitCode); +} + +// Parses the first parseArgCount arguments of the current process command line and returns +// them in parsedArgs, the rest of the command line is returned untouched. +LPCWSTR LaunchCommand::UntouchedCommandLineArguments( + int parseArgCount, std::vector<std::wstring>& parsedArgs) +{ + LPCWSTR cmd = GetCommandLineW(); + LPCWSTR arg = nullptr; // to skip executable name + for (; parseArgCount >= 0 && *cmd; ++cmd) + { + if (*cmd == '"') { + int escaped = 0; + for (++cmd; *cmd && (*cmd != '"' || escaped % 2 != 0); ++cmd) + escaped = *cmd == '\\' ? escaped + 1 : 0; + } + if (*cmd == ' ') { + if (arg) + if (cmd-1 > arg && *arg == '"' && *(cmd-1) == '"') + parsedArgs.push_back(std::wstring(arg+1, cmd-1)); + else + parsedArgs.push_back(std::wstring(arg, cmd)); + arg = cmd + 1; + --parseArgCount; + } + } + return cmd; +} + + +std::string ExeCommand::getUsageLine() const +{ + return "[options] exe-name"; +} + +po::options_description ExeCommand::getVisibleOptions() const +{ + po::options_description d; + + d.add_options() + ("arguments,a", po::value<std::string>()->default_value(""), "override arguments") + ("cwd,c", po::value<std::string>()->default_value(""), "override working directory"); + + return d; +} + +po::options_description ExeCommand::getInternalOptions() const +{ + po::options_description d; + + d.add_options() + ("exe-name", po::value<std::string>()->required(), "executable name"); + + return d; +} + +po::positional_options_description ExeCommand::getPositional() const +{ + po::positional_options_description d; + + d.add("exe-name", 1); + + return d; +} + +Command::Meta ExeCommand::meta() const +{ + return {"exe", "launches a configured executable"}; +} + +std::optional<int> ExeCommand::doRun() +{ + const auto exe = vm()["exe-name"].as<std::string>(); + const auto args = vm()["arguments"].as<std::string>(); + const auto cwd = vm()["cwd"].as<std::string>(); + + + + return 0; +} + + +po::options_description RunCommand::getOptions() const +{ + return {}; +} + +Command::Meta RunCommand::meta() const +{ + return {"run", "launches an arbitrary program"}; +} + +std::optional<int> RunCommand::doRun() +{ + return {}; +} + +} // namespace diff --git a/src/commandline.h b/src/commandline.h new file mode 100644 index 00000000..72018ba3 --- /dev/null +++ b/src/commandline.h @@ -0,0 +1,153 @@ +#ifndef MODORGANIZER_COMMANDLINE_INCLUDED +#define MODORGANIZER_COMMANDLINE_INCLUDED +#include "moshortcut.h" +#include <vector> +#include <memory> + +namespace cl +{ + +namespace po = boost::program_options; + + +class Command +{ +public: + virtual ~Command() = default; + + std::string name() const; + std::string description() const; + std::string usageLine() const; + + po::options_description allOptions() const; + po::options_description visibleOptions() const; + po::positional_options_description positional() const; + std::string usage() const; + + virtual bool allow_unregistered() const; + + std::optional<int> run( + const std::wstring& originalLine, + po::variables_map vm, + std::vector<std::wstring> untouched); + +protected: + struct Meta + { + std::string name, description; + }; + + virtual std::string getUsageLine() const; + virtual po::options_description getVisibleOptions() const; + virtual po::options_description getInternalOptions() const; + virtual po::positional_options_description getPositional() const; + + virtual Meta meta() const = 0; + virtual std::optional<int> doRun() = 0; + + const std::wstring& originalCmd() const; + const po::variables_map& vm() const; + const std::vector<std::wstring>& untouched() const; + +private: + std::wstring m_original; + po::variables_map m_vm; + std::vector<std::wstring> m_untouched; +}; + + +class CrashDumpCommand : public Command +{ +protected: + po::options_description getVisibleOptions() const override; + Meta meta() const override; + std::optional<int> doRun() override; +}; + + +// this is the `launch` command used when starting a process from within the +// virtualized directory, see processrunner.cpp +// +// it has its own parsing of the command line to extract the argument after +// `launch` and use it as the cwd of the process, but pass the remaining +// arguments verbatim +// +// this is very old code that should probably never be changed +// +// note that it's actually buggy; in particular, it doesn't handle multiple +// whitespace between arguments +// +class LaunchCommand : public Command +{ +public: + bool allow_unregistered() const override; + +protected: + Meta meta() const override; + std::optional<int> doRun() override; + + int SpawnWaitProcess(LPCWSTR workingDirectory, LPCWSTR commandLine); + + LPCWSTR UntouchedCommandLineArguments( + int parseArgCount, std::vector<std::wstring>& parsedArgs); +}; + + +class ExeCommand : public Command +{ +protected: + std::string getUsageLine() const override; + po::options_description getVisibleOptions() const override; + po::options_description getInternalOptions() const override; + po::positional_options_description getPositional() const override; + Meta meta() const override; + std::optional<int> doRun() override; +}; + + +class RunCommand : public Command +{ +protected: + po::options_description getOptions() const; + Meta meta() const override; + std::optional<int> doRun() override; +}; + + +class CommandLine +{ +public: + CommandLine(); + + std::optional<int> run(const std::wstring& line); + void clear(); + + std::string usage(const Command* c=nullptr) const; + + bool multiple() const; + std::optional<QString> profile() const; + std::optional<QString> instance() const; + + const MOShortcut& shortcut() const; + std::optional<QString> nxmLink() const; + std::optional<QString> executable() const; + + const QStringList& untouched() const; + +private: + po::options_description m_visibleOptions, m_allOptions; + po::positional_options_description m_positional; + std::vector<std::unique_ptr<Command>> m_commands; + po::variables_map m_vm; + MOShortcut m_shortcut; + std::optional<QString> m_nxmLink; + std::optional<QString> m_executable; + QStringList m_untouched; + + void createOptions(); + std::string more() const; +}; + +} // namespace + +#endif // MODORGANIZER_COMMANDLINE_INCLUDED diff --git a/src/createinstancedialog.cpp b/src/createinstancedialog.cpp new file mode 100644 index 00000000..2ff8adf3 --- /dev/null +++ b/src/createinstancedialog.cpp @@ -0,0 +1,509 @@ +#include "createinstancedialog.h" +#include "ui_createinstancedialog.h" +#include "createinstancedialogpages.h" +#include "instancemanager.h" +#include "settings.h" +#include "shared/util.h" +#include "shared/appconfig.h" +#include <iplugingame.h> +#include <utility.h> + +using namespace MOBase; + +CreateInstanceDialog::CreateInstanceDialog( + const PluginContainer& pc, Settings* s, QWidget *parent) : + QDialog(parent), ui(new Ui::CreateInstanceDialog), m_pc(pc), m_settings(s), + m_switching(false), m_singlePage(false) +{ + using namespace cid; + + ui->setupUi(this); + m_originalNext = ui->next->text(); + + m_pages.push_back(std::make_unique<IntroPage>(*this)); + m_pages.push_back(std::make_unique<TypePage>(*this)); + m_pages.push_back(std::make_unique<GamePage>(*this)); + m_pages.push_back(std::make_unique<VariantsPage>(*this)); + m_pages.push_back(std::make_unique<NamePage>(*this)); + m_pages.push_back(std::make_unique<PathsPage>(*this)); + m_pages.push_back(std::make_unique<NexusPage>(*this)); + m_pages.push_back(std::make_unique<ConfirmationPage>(*this)); + + ui->pages->setCurrentIndex(0); + ui->launch->setChecked(true); + + if (!m_settings) + { + // first run of MO, there are no instances yet, force launch + ui->launch->setEnabled(false); + } + + if (m_pages[0]->skip()) { + next(); + } + + updateNavigation(); + + connect(ui->next, &QPushButton::clicked, [&]{ next(); }); + connect(ui->back, &QPushButton::clicked, [&]{ back(); }); + connect(ui->cancel, &QPushButton::clicked, [&]{ reject(); }); +} + +CreateInstanceDialog::~CreateInstanceDialog() = default; + +Ui::CreateInstanceDialog* CreateInstanceDialog::getUI() +{ + return ui.get(); +} + +const PluginContainer& CreateInstanceDialog::pluginContainer() +{ + return m_pc; +} + +Settings* CreateInstanceDialog::settings() +{ + return m_settings; +} + +bool CreateInstanceDialog::isOnLastPage() const +{ + for (int i=ui->pages->currentIndex() + 1; i < ui->pages->count(); ++i) { + if (!m_pages[i]->skip()) { + return false; + } + } + + return true; +} + +void CreateInstanceDialog::next() +{ + const auto i = ui->pages->currentIndex(); + const auto last = isOnLastPage(); + + if (last) { + if (m_singlePage) { + // just close the dialog + accept(); + } else { + finish(); + } + } else { + changePage(+1); + } +} + +void CreateInstanceDialog::back() +{ + changePage(-1); +} + +void CreateInstanceDialog::setSinglePageImpl(const QString& instanceName) +{ + m_singlePage = true; + + if (m_pages[ui->pages->currentIndex()]->skip()) { + next(); + } + + // don't show the "create a new instance" title for single pages, this is + // when the instance already exists but some info is missing + ui->title->setText(tr("Setting up instance %1").arg(instanceName)); + setWindowTitle(tr("Setting up an instance %1").arg(instanceName)); +} + +void CreateInstanceDialog::changePage(int d) +{ + std::size_t i = static_cast<std::size_t>(ui->pages->currentIndex()); + + // goes back or forwards until an unskippable page is reached, or the + // first/last page + + if (d > 0) { + for (;;) { + ++i; + + if (i >= m_pages.size()) { + break; + } + + if (!m_pages[i]->skip()) { + break; + } + } + } else { + for (;;) { + if (i == 0) { + break; + } + + --i; + + if (!m_pages[i]->skip()) { + break; + } + } + } + + if (i < m_pages.size()) { + selectPage(i); + } +} + + +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(); + logCreation(tr("Creating instance...")); + + const auto& m = InstanceManager::singleton(); + const auto ci = creationInfo(); + + auto logger = [&](QString s) { + logCreation(s); + }; + + auto createDir = [&](QString path) { + return DirectoryCreator::create(path, logger); + }; + + + try + { + std::vector<std::unique_ptr<DirectoryCreator>> dirs; + + 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))); + dirs.push_back(createDir(PathSettings::resolve(ci.paths.mods, ci.paths.base))); + dirs.push_back(createDir(PathSettings::resolve(ci.paths.profiles, ci.paths.base))); + dirs.push_back(createDir(PathSettings::resolve(ci.paths.overwrite, ci.paths.base))); + + + Settings s(ci.iniPath); + s.game().setName(ci.game->gameName()); + s.game().setDirectory(ci.gameLocation); + + if (!ci.gameVariant.isEmpty()) { + s.game().setEdition(ci.gameVariant); + } + + if (ci.type == Global) { + if (ci.paths.base != ci.dataPath) { + s.paths().setBase(ci.paths.base); + } + + if (ci.paths.downloads != cid::makeDefaultPath(AppConfig::downloadPath())) { + s.paths().setDownloads(ci.paths.downloads); + } + + if (ci.paths.mods != cid::makeDefaultPath(AppConfig::modsPath())) { + s.paths().setMods(ci.paths.mods); + } + + if (ci.paths.profiles != cid::makeDefaultPath(AppConfig::profilesPath())) { + s.paths().setProfiles(ci.paths.profiles); + } + + if (ci.paths.overwrite != cid::makeDefaultPath(AppConfig::overwritePath())) { + s.paths().setOverwrite(ci.paths.overwrite); + } + } + + + logCreation(tr("Writing %1...").arg(ci.iniPath)); + + const auto r = s.sync(); + if (r != QSettings::NoError) { + switch (r) + { + case QSettings::AccessError: + logCreation(formatSystemMessage(ERROR_ACCESS_DENIED)); + break; + + case QSettings::FormatError: + logCreation(tr("Format error.")); + break; + + default: + logCreation(tr("Error %1.").arg(static_cast<int>(r))); + break; + } + + throw Failed(); + } + + for (auto& d : dirs) { + d->commit(); + } + + logCreation(tr("Done.")); + + if (ui->hideIntro->isChecked()) { + GlobalSettings::setHideCreateInstanceIntro(true); + } + + if (ui->launch->isChecked()) { + InstanceManager::singleton().setCurrentInstance(ci.instanceName); + + if (m_settings) { + // don't restart without settings, it happens on startup when there are + // no instances + ExitModOrganizer(Exit::Restart); + } + + m_switching = true; + } + + accept(); + } + catch(Failed&) + { + } +} + +void CreateInstanceDialog::logCreation(const QString& s) +{ + ui->creationLog->insertPlainText(s + "\n"); +} + +void CreateInstanceDialog::logCreation(const std::wstring& s) +{ + logCreation(QString::fromStdWString(s)); +} + +void CreateInstanceDialog::selectPage(std::size_t i) +{ + if (i >= m_pages.size()) { + return; + } + + ui->pages->setCurrentIndex(static_cast<int>(i)); + m_pages[i]->activated(); + + updateNavigation(); +} + +void CreateInstanceDialog::updateNavigation() +{ + const auto i = ui->pages->currentIndex(); + const auto last = isOnLastPage(); + + ui->next->setEnabled(canNext()); + ui->back->setEnabled(canBack()); + + if (last) { + ui->next->setText(tr("Finish")); + } else { + ui->next->setText(m_originalNext); + } +} + +bool CreateInstanceDialog::canNext() const +{ + const auto i = ui->pages->currentIndex(); + return m_pages[i]->ready(); +} + +bool CreateInstanceDialog::canBack() const +{ + auto i = ui->pages->currentIndex(); + + for (;;) + { + if (i == 0) { + break; + } + + --i; + + if (!m_pages[i]->skip()) { + return true; + } + } + + return false; +} + +CreateInstanceDialog::Types CreateInstanceDialog::instanceType() const +{ + return getSelected(&cid::Page::selectedInstanceType); +} + +MOBase::IPluginGame* CreateInstanceDialog::game() const +{ + return getSelected(&cid::Page::selectedGame); +} + +QString CreateInstanceDialog::gameLocation() const +{ + return getSelected(&cid::Page::selectedGameLocation); +} + +QString CreateInstanceDialog::gameVariant() const +{ + return getSelected(&cid::Page::selectedGameVariant); +} + +QString CreateInstanceDialog::instanceName() const +{ + return getSelected(&cid::Page::selectedInstanceName); +} + +QString CreateInstanceDialog::dataPath() const +{ + QString s; + + if (instanceType() == Portable) { + s = QDir(InstanceManager::portablePath()).absolutePath(); + } else { + s = InstanceManager::singleton().instancePath(instanceName()); + } + + return QDir::toNativeSeparators(s); +} + +CreateInstanceDialog::Paths CreateInstanceDialog::paths() const +{ + return getSelected(&cid::Page::selectedPaths); +} + +bool CreateInstanceDialog::switching() const +{ + return m_switching; +} + +void fixVarDir(QString& path, const std::wstring& defaultDir) +{ + if (path.isEmpty()) { + path = cid::makeDefaultPath(defaultDir); + } else if (!path.contains(PathSettings::BaseDirVariable)) { + path = QDir(path).absolutePath(); + } + + path = QDir::toNativeSeparators(path); +} + +void fixDirPath(QString& path) +{ + path = QDir::toNativeSeparators(QDir(path).absolutePath()); +} + +void fixFilePath(QString& path) +{ + path = QDir::toNativeSeparators(QFileInfo(path).absolutePath()); +} + +CreateInstanceDialog::CreationInfo CreateInstanceDialog::creationInfo() const +{ + const auto iniFilename = QString::fromStdWString(AppConfig::iniFileName()); + + 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; + + fixDirPath(ci.paths.base); + fixFilePath(ci.paths.ini); + + fixVarDir(ci.paths.downloads, AppConfig::downloadPath()); + fixVarDir(ci.paths.mods, AppConfig::modsPath()); + fixVarDir(ci.paths.profiles, AppConfig::profilesPath()); + fixVarDir(ci.paths.overwrite, AppConfig::overwritePath()); + + return ci; +} diff --git a/src/createinstancedialog.h b/src/createinstancedialog.h new file mode 100644 index 00000000..25e383eb --- /dev/null +++ b/src/createinstancedialog.h @@ -0,0 +1,138 @@ +#ifndef MODORGANIZER_CREATEINSTANCEDIALOG_INCLUDED +#define MODORGANIZER_CREATEINSTANCEDIALOG_INCLUDED + +#include <QDialog> + +namespace MOBase { class IPluginGame; } +namespace Ui { class CreateInstanceDialog; }; +namespace cid { class Page; } + +class PluginContainer; +class Settings; + +class CreateInstanceDialog : public QDialog +{ + Q_OBJECT + +public: + enum Types + { + NoType = 0, + Global, + Portable + }; + + struct Paths + { + QString base; + QString downloads; + QString mods; + QString profiles; + QString overwrite; + QString ini; + + auto operator<=>(const Paths&) const = default; + }; + + struct CreationInfo + { + Types type; + MOBase::IPluginGame* game; + QString gameLocation; + QString gameVariant; + QString instanceName; + QString dataPath; + QString iniPath; + Paths paths; + }; + + + CreateInstanceDialog( + const PluginContainer& pc, Settings* s, QWidget *parent = nullptr); + + ~CreateInstanceDialog(); + + Ui::CreateInstanceDialog* getUI(); + + const PluginContainer& pluginContainer(); + Settings* settings(); + + template <class Page> + void setSinglePage(const QString& instanceName) + { + for (auto&& p : m_pages) { + if (auto* tp=dynamic_cast<Page*>(p.get())) { + tp->setSkip(false); + } else { + p->setSkip(true); + } + } + + setSinglePageImpl(instanceName); + } + + template <class Page> + Page* getPage() + { + for (auto&& p : m_pages) { + if (auto* tp=dynamic_cast<Page*>(p.get())) { + return tp; + } + } + + return nullptr; + } + + void next(); + void back(); + void selectPage(std::size_t i); + void changePage(int d); + void finish(); + + void updateNavigation(); + 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; + bool switching() const; + + CreationInfo creationInfo() const; + +private: + std::unique_ptr<Ui::CreateInstanceDialog> ui; + const PluginContainer& m_pc; + Settings* m_settings; + std::vector<std::unique_ptr<cid::Page>> m_pages; + QString m_originalNext; + bool m_switching; + bool m_singlePage; + + + void setSinglePageImpl(const QString& instanceName); + + template <class T> + T getSelected(T (cid::Page::*mf)() const) const + { + for (auto&& p : m_pages) { + const auto t = (p.get()->*mf)(); + if (t != T()) { + return t; + } + } + + 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 new file mode 100644 index 00000000..b10d74a7 --- /dev/null +++ b/src/createinstancedialog.ui @@ -0,0 +1,1233 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>CreateInstanceDialog</class> + <widget class="QDialog" name="CreateInstanceDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>493</width> + <height>444</height> + </rect> + </property> + <property name="windowTitle"> + <string>Creating an instance</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <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="QLabel" name="title"> + <property name="font"> + <font> + <pointsize>14</pointsize> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string>Creating a new instance</string> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer_5"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Fixed</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>10</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QStackedWidget" name="pages"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="page_10"> + <layout class="QVBoxLayout" name="verticalLayout_21"> + <item> + <widget class="QLabel" name="label_15"> + <property name="text"> + <string><h3>What is an instance?</h3> +<p>An instance is a full set of mods, downloads, profiles and configuration for a game. Each game must be managed in its own instance. Mod Organizer can freely switch between instances.</p></string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <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> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer_6"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QCheckBox" name="hideIntro"> + <property name="text"> + <string>Never show this page again</string> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="page"> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QWidget" name="widget_7" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_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="QLabel" name="label_2"> + <property name="text"> + <string><h3>Select the type of instance to create.</h3></string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QWidget" name="widget_3" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <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="QWidget" name="widget_15" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_16"> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QCommandLinkButton" name="createGlobal"> + <property name="text"> + <string>Create a global instance</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="description"> + <string>Global instances are stored in %1, but some paths can be changed to be on a different drive if necessary. A single installation of Mod Organizer can manage multiple global instances.</string> + </property> + </widget> + </item> + <item> + <widget class="QCommandLinkButton" name="createPortable"> + <property name="text"> + <string>Create a portable instance</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="description"> + <string>A portable instance stores everything in Mod Organizer's installation folder, currently %1. There can only be one portable instance per installation of Mod Organizer.</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QLabel" name="portableExistsLabel"> + <property name="text"> + <string>A portable instance already exists.</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="page_2"> + <layout class="QVBoxLayout" name="verticalLayout_7"> + <item> + <widget class="QWidget" name="widget_5" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <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="QLabel" name="label_3"> + <property name="text"> + <string><h3>Select the game to manage.</h3></string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QWidget" name="widget_6" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_25"> + <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="QWidget" name="widget_23" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <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="QScrollArea" name="scrollArea_3"> + <property name="widgetResizable"> + <bool>true</bool> + </property> + <widget class="QWidget" name="games"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>455</width> + <height>286</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout_26"> + <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> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QWidget" name="widget_21" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout_6" stretch="0,0,0"> + <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="QCheckBox" name="showAllGames"> + <property name="text"> + <string>Show all supported games</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_5"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>175</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLineEdit" name="gamesFilter"> + <property name="placeholderText"> + <string>Filter</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="page_9"> + <layout class="QVBoxLayout" name="verticalLayout_19"> + <item> + <widget class="QWidget" name="widget_16" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_18"> + <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="QLabel" name="label_4"> + <property name="text"> + <string><h3>Select the game edition.</h3></string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_14"> + <property name="text"> + <string>This game has multiple variants. The correct one must be selected or Mod Organizer will not be able to launch the game properly.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QWidget" name="widget_17" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_20"> + <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="QScrollArea" name="scrollArea_2"> + <property name="widgetResizable"> + <bool>true</bool> + </property> + <widget class="QWidget" name="scrollAreaWidgetContents"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>455</width> + <height>278</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout_17"> + <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="QDialogButtonBox" name="editions"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::NoButton</set> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="page_5"> + <layout class="QVBoxLayout" name="verticalLayout_9"> + <item> + <widget class="QWidget" name="widget_8" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <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="QLabel" name="instanceNameLabel"> + <property name="text"> + <string><h3>Customize the name for this <span style="white-space: nowrap;">%1</span> instance.</h3></string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QWidget" name="widget_9" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_8"> + <property name="spacing"> + <number>9</number> + </property> + <item> + <widget class="QLineEdit" name="instanceName"> + <property name="placeholderText"> + <string>Instance name</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="instanceNameExists"> + <property name="text"> + <string>There is already an instance with this name.</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="instanceNameInvalid"> + <property name="text"> + <string>The name contains invalid characters. It must be a valid folder name.</string> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="page_6"> + <layout class="QVBoxLayout" name="verticalLayout_14"> + <item> + <widget class="QWidget" name="widget_10" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_10"> + <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="QLabel" name="label_5"> + <property name="text"> + <string><h3>Select a folder where the data should be stored.</h3></string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="pathsLabel"> + <property name="text"> + <string>This includes downloads, mods, profiles and overwrite for your <b>%1</b> instance. If there is enough space on this drive, you should use the default folder.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QWidget" name="widget_13" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_11"> + <item> + <widget class="QStackedWidget" name="pathPages"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="page_7"> + <layout class="QVBoxLayout" name="verticalLayout_12"> + <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="QWidget" name="widget_11" native="true"> + <layout class="QGridLayout" name="gridLayout"> + <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 row="0" column="2"> + <widget class="QToolButton" name="browseLocation"> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>Location</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <spacer name="verticalSpacer_3"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item row="2" column="0" colspan="3"> + <widget class="QLabel" name="locationExists"> + <property name="text"> + <string>Warning: This folder already exists.</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="location"> + <property name="placeholderText"> + <string>Folder</string> + </property> + </widget> + </item> + <item row="1" column="0" colspan="3"> + <widget class="QLabel" name="locationInvalid"> + <property name="text"> + <string>Warning: The folder contains invalid characters.</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="page_8"> + <layout class="QVBoxLayout" name="verticalLayout_13"> + <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="QWidget" name="widget_12" native="true"> + <layout class="QGridLayout" name="gridLayout_2"> + <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 row="4" column="3"> + <widget class="QToolButton" name="browseOverwrite"> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + <item row="2" column="3"> + <widget class="QToolButton" name="browseMods"> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_10"> + <property name="text"> + <string>Mods</string> + </property> + </widget> + </item> + <item row="4" column="2"> + <widget class="QLineEdit" name="overwrite"> + <property name="placeholderText"> + <string>Folder</string> + </property> + </widget> + </item> + <item row="2" column="2"> + <widget class="QLineEdit" name="mods"> + <property name="placeholderText"> + <string>Folder</string> + </property> + </widget> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_13"> + <property name="text"> + <string>Overwrite</string> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label_8"> + <property name="text"> + <string>Base directory</string> + </property> + </widget> + </item> + <item row="1" column="3"> + <widget class="QToolButton" name="browseDownloads"> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + <item row="3" column="2"> + <widget class="QLineEdit" name="profiles"> + <property name="placeholderText"> + <string>Folder</string> + </property> + </widget> + </item> + <item row="5" column="0" colspan="4"> + <widget class="QLabel" name="advancedDirExists"> + <property name="text"> + <string>Warning: The folder %1 already exists.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_9"> + <property name="text"> + <string>Downloads</string> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="QLineEdit" name="downloads"> + <property name="placeholderText"> + <string>Folder</string> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QLineEdit" name="base"> + <property name="placeholderText"> + <string>Folder</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_12"> + <property name="text"> + <string>Profiles</string> + </property> + </widget> + </item> + <item row="8" column="0" colspan="4"> + <widget class="QLabel" name="label_11"> + <property name="text"> + <string>Use %BASE_DIR% to refer to the Base Directory.</string> + </property> + </widget> + </item> + <item row="0" column="3"> + <widget class="QToolButton" name="browseBase"> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + <item row="3" column="3"> + <widget class="QToolButton" name="browseProfiles"> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + <item row="7" column="0"> + <spacer name="verticalSpacer_4"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item row="6" column="0" colspan="4"> + <widget class="QLabel" name="advancedDirInvalid"> + <property name="text"> + <string>Warning: The folder %1 contains invalid characters.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <widget class="QWidget" name="widget_14" native="true"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <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> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>292</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QCheckBox" name="advancedPathOptions"> + <property name="text"> + <string>Show advanced options</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="page_3"> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QWidget" name="widget_4" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_29"> + <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="QLabel" name="label_7"> + <property name="text"> + <string><h3>Link Mod Organizer with your Nexus account</h3></string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_18"> + <property name="text"> + <string>Linking with Nexus allows you to download mods directly from Mod Organizer and automatically check for updates. This is optional.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QWidget" name="nexusBox" native="true"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>100</height> + </size> + </property> + <layout class="QVBoxLayout" name="verticalLayout_30"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>9</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QWidget" name="nexusBox_2" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_27"> + <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="QWidget" name="widget_22" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout_7"> + <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="QPushButton" name="nexusConnect"> + <property name="text"> + <string>Connect to Nexus</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="nexusManual"> + <property name="text"> + <string>Enter API Key Manually</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QListWidget" name="nexusLog"/> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer_8"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="page_11"> + <layout class="QVBoxLayout" name="verticalLayout_22"> + <item> + <widget class="QWidget" name="widget_18" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_24"> + <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="QLabel" name="label_16"> + <property name="text"> + <string><h3>Confirmation</h3></string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_17"> + <property name="text"> + <string>The instance is about to be created. Review the information below and press 'Finish'.</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QWidget" name="widget_19" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_23"> + <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="QTextEdit" name="review"> + <property name="lineWrapMode"> + <enum>QTextEdit::NoWrap</enum> + </property> + </widget> + </item> + <item> + <widget class="QTextEdit" name="creationLog"> + <property name="lineWrapMode"> + <enum>QTextEdit::NoWrap</enum> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item> + <widget class="QWidget" name="widget_20" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout_5"> + <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> + <spacer name="horizontalSpacer_4"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>304</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QCheckBox" name="launch"> + <property name="text"> + <string>Launch the new instance</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <widget class="QWidget" name="widget_2" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <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> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="back"> + <property name="text"> + <string>< Back</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="next"> + <property name="text"> + <string>Next ></string> + </property> + <property name="default"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="cancel"> + <property name="text"> + <string>Cancel</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>LinkLabel</class> + <extends>QLabel</extends> + <header location="global">linklabel.h</header> + </customwidget> + </customwidgets> + <resources> + <include location="resources.qrc"/> + </resources> + <connections/> +</ui> diff --git a/src/createinstancedialogpages.cpp b/src/createinstancedialogpages.cpp new file mode 100644 index 00000000..26f2f61d --- /dev/null +++ b/src/createinstancedialogpages.cpp @@ -0,0 +1,1084 @@ +#include "createinstancedialogpages.h" +#include "ui_createinstancedialog.h" +#include "instancemanager.h" +#include "settings.h" +#include "plugincontainer.h" +#include "settingsdialognexus.h" +#include "shared/appconfig.h" +#include <iplugingame.h> +#include <report.h> +#include <utility.h> + +namespace cid +{ + +using namespace MOBase; +using MOBase::TaskDialog; + +QString makeDefaultPath(const std::wstring& dir) +{ + return QDir::toNativeSeparators(PathSettings::makeDefaultPath( + QString::fromStdWString(dir))); +} + + + +PlaceholderLabel::PlaceholderLabel(QLabel* label) + : m_label(label), m_original(label->text()) +{ +} + +void PlaceholderLabel::setText(const QString& arg) +{ + if (m_original.contains("%1")) { + m_label->setText(m_original.arg(arg)); + } +} + +void PlaceholderLabel::setVisible(bool b) +{ + m_label->setVisible(b); +} + + + +Page::Page(CreateInstanceDialog& dlg) + : ui(dlg.getUI()), m_dlg(dlg), m_pc(dlg.pluginContainer()), m_skip(false) +{ +} + +bool Page::ready() const +{ + return true; +} + +bool Page::skip() const +{ + return m_skip || doSkip(); +} + +bool Page::doSkip() const +{ + return false; +} + +void Page::activated() +{ + // no-op +} + +void Page::setSkip(bool b) +{ + m_skip = b; +} + +void Page::updateNavigation() +{ + m_dlg.updateNavigation(); +} + +void Page::next() +{ + m_dlg.next(); +} + + +CreateInstanceDialog::Types Page::selectedInstanceType() const +{ + // no-op + return CreateInstanceDialog::NoType; +} + +IPluginGame* Page::selectedGame() const +{ + // no-op + return nullptr; +} + +QString Page::selectedGameLocation() const +{ + // no-op + return {}; +} + +QString Page::selectedGameVariant() const +{ + // no-op + return {}; +} + +QString Page::selectedInstanceName() const +{ + // no-op + return {}; +} + +CreateInstanceDialog::Paths Page::selectedPaths() const +{ + // no-op + return {}; +} + + +IntroPage::IntroPage(CreateInstanceDialog& dlg) + : Page(dlg) +{ +} + +bool IntroPage::doSkip() const +{ + return GlobalSettings::hideCreateInstanceIntro(); +} + + +TypePage::TypePage(CreateInstanceDialog& dlg) + : Page(dlg), m_type(CreateInstanceDialog::NoType) +{ + ui->createGlobal->setDescription( + ui->createGlobal->description() + .arg(InstanceManager::singleton().instancesPath())); + + ui->createPortable->setDescription( + ui->createPortable->description() + .arg(InstanceManager::portablePath())); + + if (InstanceManager::singleton().portableInstanceExists()) { + ui->createPortable->setEnabled(false); + ui->portableExistsLabel->setVisible(true); + } else { + ui->createPortable->setEnabled(true); + ui->portableExistsLabel->setVisible(false); + } + + QObject::connect( + ui->createGlobal, &QAbstractButton::clicked, [&]{ global(); }); + + QObject::connect( + ui->createPortable, &QAbstractButton::clicked, [&]{ portable(); }); +} + +bool TypePage::ready() const +{ + return (m_type != CreateInstanceDialog::NoType); +} + +CreateInstanceDialog::Types TypePage::selectedInstanceType() const +{ + return m_type; +} + +void TypePage::global() +{ + m_type = CreateInstanceDialog::Global; + + ui->createGlobal->setChecked(true); + ui->createPortable->setChecked(false); + + next(); +} + +void TypePage::portable() +{ + m_type = CreateInstanceDialog::Portable; + + ui->createGlobal->setChecked(false); + ui->createPortable->setChecked(true); + + next(); +} + + +GamePage::Game::Game(IPluginGame* g) + : game(g), installed(g->isInstalled()) +{ + if (installed) { + dir = game->gameDirectory().path(); + } +} + + +GamePage::GamePage(CreateInstanceDialog& dlg) + : Page(dlg), m_selection(nullptr) +{ + createGames(); + fillList(); + + m_filter.setEdit(ui->gamesFilter); + + QObject::connect(&m_filter, &FilterWidget::changed, [&]{ fillList(); }); + QObject::connect(ui->showAllGames, &QCheckBox::clicked, [&]{ fillList(); }); +} + +bool GamePage::ready() const +{ + return (m_selection != nullptr); +} + +IPluginGame* GamePage::selectedGame() const +{ + if (!m_selection) { + return nullptr; + } + + return m_selection->game; +} + +QString GamePage::selectedGameLocation() const +{ + if (!m_selection) { + return {}; + } + + return QDir::toNativeSeparators(m_selection->dir); +} + +void GamePage::select(IPluginGame* game, const QString& dir) +{ + Game* checked = findGame(game); + + if (checked) { + if (!checked->installed) { + if (dir.isEmpty()) { + const auto path = QFileDialog::getExistingDirectory( + &m_dlg, QObject::tr("Find game installation")); + + if (path.isEmpty()) { + checked = nullptr; + } else { + checked = checkInstallation(path, checked); + } + } else { + checked->dir = dir; + checked->installed = true; + } + } + } + + m_selection = checked; + selectButton(checked); + updateNavigation(); + + if (checked) { + next(); + } +} + +void GamePage::selectCustom() +{ + const auto path = QFileDialog::getExistingDirectory( + &m_dlg, QObject::tr("Find game installation")); + + if (path.isEmpty()) { + // reselect the previous button + selectButton(m_selection); + return; + } + + for (auto& g : m_games) { + if (g->game->looksValid(path)) { + g->dir = path; + g->installed = true; + select(g->game); + updateButton(g.get()); + return; + } + } + + warnUnrecognized(path); + selectButton(m_selection); +} + +void GamePage::warnUnrecognized(const QString& path) +{ + QString supportedGames; + for (auto* game : sortedGamePlugins()) { + supportedGames += game->gameName() + "\n"; + } + + QMessageBox dlg(&m_dlg); + + dlg.setWindowTitle(QObject::tr("Unrecognized game")); + dlg.setText(QObject::tr( + "The folder %1 does not seem to contain a game Mod Organizer can " + "manage.").arg(path)); + dlg.setInformativeText( + QObject::tr("See details for the list of supported games.")); + dlg.setDetailedText(supportedGames); + dlg.setIcon(QMessageBox::Warning); + dlg.setStandardButtons(QMessageBox::Ok); + + dlg.exec(); +} + +std::vector<IPluginGame*> GamePage::sortedGamePlugins() const +{ + std::vector<IPluginGame*> v; + + for (auto* game : m_pc.plugins<IPluginGame>()) { + v.push_back(game); + } + + std::sort(v.begin(), v.end(), [](auto* a, auto* b) { + return (naturalCompare(a->gameName(), b->gameName()) < 0); + }); + + return v; +} + +GamePage::Game* GamePage::findGame(IPluginGame* game) +{ + for (auto& g : m_games) { + if (g->game == game) { + return g.get(); + } + } + + return nullptr; +} + +void GamePage::createGames() +{ + m_games.clear(); + + for (auto* game : sortedGamePlugins()) { + m_games.push_back(std::make_unique<Game>(game)); + } +} + +void GamePage::updateButton(Game* g) +{ + if (!g || !g->button) { + return; + } + + g->button->setText(g->game->gameName()); + g->button->setIcon(g->game->gameIcon()); + + if (g->installed) { + g->button->setDescription(g->dir); + } else { + g->button->setDescription(QObject::tr("No installation found")); + } +} + +void GamePage::selectButton(Game* g) +{ + // go through each game, set the button that is for game `g` as active; + // some button might not exist, which happens when selecting a custom + // folder for a game that was considered uninstalled + + for (const auto& gg : m_games) { + if (!g) { + // nothing should be selected + if (gg->button) { + gg->button->setChecked(false); + } + + continue; + } + + if (gg->game == g->game) { + // this is the button that should be selected + + if (!gg->button) { + // this happens when the button wasn't visible because the game + // was not installed; create it and show it + // and it has a button, just check it + createGameButton(gg.get()); + addButton(gg->button); + } + + gg->button->setChecked(true); + gg->button->setFocus(); + } else { + // this is not the button you're looking for + if (gg->button) { + gg->button->setChecked(false); + } + } + } +} + +QCommandLinkButton* GamePage::createCustomButton() +{ + auto* b = new QCommandLinkButton; + + b->setText(QObject::tr("Browse...")); + b->setDescription( + QObject::tr("The folder must contain a valid game installation")); + + QObject::connect(b, &QAbstractButton::clicked, [&] { + selectCustom(); + }); + + 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; + + if (!showAll && !g->installed) { + // not installed + continue; + } + + if (!m_filter.matches(g->game->gameName())) { + continue; + } + + createGameButton(g.get()); + addButton(g->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); +} + +GamePage::Game* GamePage::checkInstallation(const QString& path, Game* g) +{ + if (g->game->looksValid(path)) { + // okay + return g; + } + + // the selected game can't use that folder, find another one + auto* otherGame = findAnotherGame(path); + if (otherGame == g->game) { + // shouldn't happen, but okay + return g; + } + + if (otherGame) { + auto* confirmedGame = confirmOtherGame(path, g->game, otherGame); + + if (!confirmedGame) { + // cancelled + return nullptr; + } + + // make it look like the user clicked that button instead + g = findGame(confirmedGame); + if (!g) { + return nullptr; + } + } else { + // nothing can manage this, but the user can override + if (!confirmUnknown(path, g->game)) { + // cancelled + return nullptr; + } + } + + // remember this path + g->dir = path; + g->installed = true; + + updateButton(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) + .title(QObject::tr("Unrecognized game")) + .main(QObject::tr("Unrecognized game")) + .content(QObject::tr( + "The folder %1 does not seem to contain an installation for " + "<span style=\"white-space: nowrap; font-weight: bold;\">%2</span> or " + "for any other game Mod Organizer can manage.") + .arg(path) + .arg(game->gameName())) + .button({ + QObject::tr("Use this folder for %1").arg(game->gameName()), + QObject::tr("I know what I'm doing"), + QMessageBox::Ignore}) + .button({ + QObject::tr("Cancel"), + QMessageBox::Cancel}) + .exec(); + + return (r == QMessageBox::Ignore); +} + +IPluginGame* GamePage::confirmOtherGame( + const QString& path, + IPluginGame* selectedGame, IPluginGame* guessedGame) +{ + const auto r = TaskDialog(&m_dlg) + .title(QObject::tr("Incorrect game")) + .main(QObject::tr("Incorrect game")) + .content(QObject::tr( + "The folder %1 seems to contain an installation for " + "<span style=\"white-space: nowrap; font-weight: bold;\">%2</span>, " + "not " + "<span style=\"white-space: nowrap; font-weight: bold;\">%3</span>.") + .arg(path) + .arg(guessedGame->gameName()) + .arg(selectedGame->gameName())) + .button({ + QObject::tr("Manage %1 instead").arg(guessedGame->gameName()), + QMessageBox::Ok}) + .button({ + QObject::tr("Use this folder for %1").arg(selectedGame->gameName()), + QObject::tr("I know what I'm doing"), + QMessageBox::Ignore}) + .button({ + QObject::tr("Cancel"), + QMessageBox::Cancel}) + .exec(); + + switch (r) + { + case QMessageBox::Ok: + return guessedGame; + + case QMessageBox::Ignore: + return selectedGame; + + case QMessageBox::Cancel: + default: + return nullptr; + } +} + + +VariantsPage::VariantsPage(CreateInstanceDialog& dlg) + : Page(dlg), m_previousGame(nullptr) +{ +} + +bool VariantsPage::ready() const +{ + return !m_selection.isEmpty(); +} + +bool VariantsPage::doSkip() const +{ + auto* g = m_dlg.game(); + if (!g) { + // shouldn't happen + return true; + } + + const auto variants = g->gameVariants(); + return (variants.size() < 2); +} + +void VariantsPage::activated() +{ + auto* g = m_dlg.game(); + + if (m_previousGame != g) { + m_previousGame = g; + m_selection = ""; + fillList(); + } +} + +void VariantsPage::select(const QString& variant) +{ + for (auto* b : m_buttons) { + if (b->text() == variant) { + m_selection = variant; + b->setChecked(true); + } else { + b->setChecked(false); + } + } + + updateNavigation(); + + if (!m_selection.isEmpty()) { + next(); + } +} + +QString VariantsPage::selectedGameVariant() const +{ + auto* g = m_dlg.game(); + if (!g) { + // shouldn't happen + return {}; + } + + const auto variants = g->gameVariants(); + if (variants.size() < 2) { + return {}; + } else { + return m_selection; + } +} + +void VariantsPage::fillList() +{ + ui->editions->clear(); + m_buttons.clear(); + + auto* g = m_dlg.game(); + if (!g) { + // shouldn't happen + return; + } + + const auto variants = g->gameVariants(); + for (auto& v : variants) { + auto* b = new QCommandLinkButton(v); + b->setCheckable(true); + + QObject::connect(b, &QAbstractButton::clicked, [v, this] { + select(v); + }); + + ui->editions->addButton(b, QDialogButtonBox::AcceptRole); + m_buttons.push_back(b); + } +} + + +NamePage::NamePage(CreateInstanceDialog& dlg) : + Page(dlg), m_modified(false), m_okay(false), + m_label(ui->instanceNameLabel), m_exists(ui->instanceNameExists), + m_invalid(ui->instanceNameInvalid) +{ + QObject::connect( + ui->instanceName, &QLineEdit::textEdited, [&]{ onChanged(); }); +} + +bool NamePage::ready() const +{ + return m_okay; +} + +bool NamePage::doSkip() const +{ + return (m_dlg.instanceType() == CreateInstanceDialog::Portable); +} + +void NamePage::activated() +{ + auto* g = m_dlg.game(); + if (!g) { + // shouldn't happen, next should be disabled + return; + } + + m_label.setText(g->gameName()); + + if (!m_modified || ui->instanceName->text().isEmpty()) { + const auto n = InstanceManager::singleton().makeUniqueName(g->gameName()); + ui->instanceName->setText(n); + m_modified = false; + } + + updateWarnings(); +} + +QString NamePage::selectedInstanceName() const +{ + if (!m_okay) { + return {}; + } + + const auto text = ui->instanceName->text().trimmed(); + return InstanceManager::singleton().sanitizeInstanceName(text); +} + +void NamePage::onChanged() +{ + m_modified = true; + updateWarnings(); +} + +void NamePage::updateWarnings() +{ + const auto root = InstanceManager::singleton().instancesPath(); + + m_okay = checkName(root, ui->instanceName->text()); + updateNavigation(); +} + +bool NamePage::checkName(QString parentDir, QString name) +{ + bool exists = false; + bool invalid = false; + bool empty = false; + + name = name.trimmed(); + + if (name.isEmpty()) { + empty = true; + } else { + if (InstanceManager::singleton().validInstanceName(name)) { + exists = QDir(parentDir).exists(name); + } else { + invalid = true; + } + } + + bool okay = false; + + if (exists) { + m_exists.setVisible(true); + m_exists.setText(QDir(parentDir).filePath(name)); + m_invalid.setVisible(false); + } else if (invalid) { + m_exists.setVisible(false); + m_invalid.setVisible(true); + m_invalid.setText(name); + } else { + okay = !empty; + m_exists.setVisible(false); + m_invalid.setVisible(false); + } + + return okay; +} + + +PathsPage::PathsPage(CreateInstanceDialog& dlg) : + Page(dlg), m_lastType(CreateInstanceDialog::NoType), + m_label(ui->pathsLabel), + m_simpleExists(ui->locationExists), m_simpleInvalid(ui->locationInvalid), + m_advancedExists(ui->advancedDirExists), + m_advancedInvalid(ui->advancedDirInvalid) +{ + 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(); }); + + QObject::connect( + ui->advancedPathOptions, &QCheckBox::clicked, [&]{ onAdvanced(); }); + + ui->pathPages->setCurrentIndex(0); +} + + +bool PathsPage::ready() const +{ + return checkPaths(); +} + +void PathsPage::activated() +{ + const auto name = m_dlg.instanceName(); + const auto type = m_dlg.instanceType(); + + const bool changed = (m_lastInstanceName != name) || (m_lastType != type); + + setPaths(name, changed); + checkPaths(); + updateNavigation(); + + m_label.setText(m_dlg.game()->gameName()); + m_lastInstanceName = name; + m_lastType = type; +} + +CreateInstanceDialog::Paths PathsPage::selectedPaths() const +{ + CreateInstanceDialog::Paths p; + + if (ui->advancedPathOptions->isChecked()) { + p.base = ui->base->text(); + p.downloads = ui->downloads->text(); + p.mods = ui->mods->text(); + p.profiles = ui->profiles->text(); + p.overwrite = ui->overwrite->text(); + } else { + p.base = ui->location->text(); + } + + return p; +} + +void PathsPage::onChanged() +{ + checkPaths(); + updateNavigation(); +} + +bool PathsPage::checkPaths() const +{ + if (ui->advancedPathOptions->isChecked()) { + return + 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); + } +} + +bool PathsPage::checkAdvancedPath(const QString& path) const +{ + return checkPath(path, m_advancedExists, m_advancedInvalid); +} + +QString PathsPage::resolve(const QString& path) const +{ + return PathSettings::resolve(path, ui->base->text()); +} + +void PathsPage::onAdvanced() +{ + if (ui->advancedPathOptions->isChecked()) { + ui->base->setText(ui->location->text()); + ui->pathPages->setCurrentIndex(1); + } else { + ui->location->setText(ui->base->text()); + ui->pathPages->setCurrentIndex(0); + } + + checkPaths(); +} + +void PathsPage::setPaths(const QString& name, bool force) +{ + QString path; + + if (m_dlg.instanceType() == CreateInstanceDialog::Portable) { + path = InstanceManager::portablePath(); + } else { + const auto root = InstanceManager::singleton().instancesPath(); + path = root + "/" + name; + } + + path = QDir::toNativeSeparators(QDir::cleanPath(path)); + + setIfEmpty(ui->location, path, force); + setIfEmpty(ui->base, path, force); + + setIfEmpty(ui->downloads, makeDefaultPath(AppConfig::downloadPath()), force); + setIfEmpty(ui->mods, makeDefaultPath(AppConfig::modsPath()), force); + setIfEmpty(ui->profiles, makeDefaultPath(AppConfig::profilesPath()), force); + setIfEmpty(ui->overwrite, makeDefaultPath(AppConfig::overwritePath()), force); +} + +void PathsPage::setIfEmpty(QLineEdit* e, const QString& path, bool force) +{ + if (e->text().isEmpty() || force) { + e->setText(path); + } +} + +bool PathsPage::checkPath( + QString path, + PlaceholderLabel& existsLabel, PlaceholderLabel& invalidLabel) const +{ + bool exists = false; + bool invalid = false; + bool empty = false; + + path = QDir::toNativeSeparators(path.trimmed()); + + if (path.isEmpty()) { + empty = true; + } else { + const QDir d(path); + + if (InstanceManager::singleton().validInstanceName(d.dirName())) { + if (m_dlg.instanceType() == 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()) { + exists = QDir(path).exists(); + } + } else { + exists = QDir(path).exists(); + } + } else { + invalid = true; + } + } + + bool okay = true; + + if (invalid) { + okay = false; + existsLabel.setVisible(false); + invalidLabel.setVisible(true); + invalidLabel.setText(path); + } else if (empty) { + okay = false; + existsLabel.setVisible(false); + invalidLabel.setVisible(false); + } else if (exists) { + // this is just a warning + existsLabel.setVisible(true); + existsLabel.setText(path); + invalidLabel.setVisible(false); + } else { + okay = true; + existsLabel.setVisible(false); + invalidLabel.setVisible(false); + } + + return okay; +} + + +NexusPage::NexusPage(CreateInstanceDialog& dlg) + : Page(dlg), m_skip(false) +{ + m_connectionUI.reset(new NexusConnectionUI( + &m_dlg, + dlg.settings(), + ui->nexusConnect, + nullptr, + ui->nexusManual, + ui->nexusLog)); + + // just check it once, or connecting and then going back and forth would skip + // the page, which would be unexpected + m_skip = GlobalSettings::hasNexusApiKey(); +} + +NexusPage::~NexusPage() = default; + +bool NexusPage::ready() const +{ + return true; +} + +bool NexusPage::doSkip() const +{ + return m_skip; +} + +void NexusPage::activated() +{ +} + + +ConfirmationPage::ConfirmationPage(CreateInstanceDialog& dlg) + : Page(dlg) +{ +} + +void ConfirmationPage::activated() +{ + 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())); + + if (m_dlg.instanceType() != CreateInstanceDialog::Portable) { + lines.push_back(QObject::tr("Instance name: %1").arg(m_dlg.instanceName())); + } + + if (paths.downloads.isEmpty()) { + // simple settings + if (paths.base != m_dlg.dataPath()) { + lines.push_back(QObject::tr("Base directory: %1").arg(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)); + } + + // game + QString name = m_dlg.game()->gameName(); + if (!m_dlg.gameVariant().isEmpty()) { + name += " (" + m_dlg.gameVariant() + ")"; + } + + lines.push_back(QObject::tr("Game: %1").arg(name)); + lines.push_back(QObject::tr("Game location: %1").arg(m_dlg.gameLocation())); + + return lines.join("\n"); +} + +QString ConfirmationPage::dirLine(const QString& caption, const QString& path) const +{ + return QString(" - %1: %2").arg(caption).arg(path); +} + +} // namespace diff --git a/src/createinstancedialogpages.h b/src/createinstancedialogpages.h new file mode 100644 index 00000000..e239c196 --- /dev/null +++ b/src/createinstancedialogpages.h @@ -0,0 +1,250 @@ +#ifndef MODORGANIZER_CREATEINSTANCEDIALOGPAGES_INCLUDED +#define MODORGANIZER_CREATEINSTANCEDIALOGPAGES_INCLUDED + +#include "createinstancedialog.h" +#include <filterwidget.h> + +#include <QLabel> +#include <QLineEdit> +#include <QCommandLinkButton> + +namespace MOBase { class IPluginGame; } +class NexusConnectionUI; + + +namespace cid +{ + +QString makeDefaultPath(const std::wstring& dir); + + +class PlaceholderLabel +{ +public: + PlaceholderLabel(QLabel* label); + void setText(const QString& arg); + void setVisible(bool b); + +private: + QLabel* m_label; + QString m_original; +}; + + +class Page +{ +public: + Page(CreateInstanceDialog& dlg); + + virtual bool ready() const; + virtual void activated(); + + void setSkip(bool b); + bool skip() const; + + void updateNavigation(); + void next(); + + virtual CreateInstanceDialog::Types selectedInstanceType() const; + virtual MOBase::IPluginGame* selectedGame() const; + virtual QString selectedGameLocation() const; + virtual QString selectedGameVariant() const; + virtual QString selectedInstanceName() const; + virtual CreateInstanceDialog::Paths selectedPaths() const; + +protected: + Ui::CreateInstanceDialog* ui; + CreateInstanceDialog& m_dlg; + const PluginContainer& m_pc; + bool m_skip; + + virtual bool doSkip() const; +}; + + +class IntroPage : public Page +{ +public: + IntroPage(CreateInstanceDialog& dlg); + +protected: + bool doSkip() const override; +}; + + +class TypePage : public Page +{ +public: + TypePage(CreateInstanceDialog& dlg); + + bool ready() const override; + CreateInstanceDialog::Types selectedInstanceType() const override; + + void global(); + void portable(); + +private: + CreateInstanceDialog::Types m_type; +}; + + +class GamePage : public Page +{ +public: + GamePage(CreateInstanceDialog& dlg); + + bool ready() const override; + MOBase::IPluginGame* selectedGame() const override; + QString selectedGameLocation() const override; + + void select(MOBase::IPluginGame* game, const QString& dir={}); + void selectCustom(); + + void warnUnrecognized(const QString& path); + +private: + struct Game + { + MOBase::IPluginGame* game = nullptr; + QCommandLinkButton* button = nullptr; + QString dir; + bool installed = false; + + Game(MOBase::IPluginGame* g); + Game(const Game&) = delete; + Game& operator=(const Game&) = delete; + }; + + std::vector<std::unique_ptr<Game>> m_games; + Game* m_selection; + MOBase::FilterWidget m_filter; + + std::vector<MOBase::IPluginGame*> sortedGamePlugins() const; + Game* findGame(MOBase::IPluginGame* game); + void createGames(); + void updateButton(Game* g); + void selectButton(Game* g); + void clearButtons(); + void addButton(QAbstractButton* b); + QCommandLinkButton* createCustomButton(); + void createGameButton(Game* g); + void fillList(); + void onFilter(); + Game* checkInstallation(const QString& path, Game* g); + MOBase::IPluginGame* findAnotherGame(const QString& path); + bool confirmUnknown(const QString& path, MOBase::IPluginGame* game); + MOBase::IPluginGame* confirmOtherGame( + const QString& path, + MOBase::IPluginGame* selectedGame, MOBase::IPluginGame* guessedGame); +}; + + +class VariantsPage : public Page +{ +public: + VariantsPage(CreateInstanceDialog& dlg); + + bool ready() const override; + void activated() override; + QString selectedGameVariant() const override; + + void select(const QString& variant); + +protected: + bool doSkip() const override; + +private: + MOBase::IPluginGame* m_previousGame; + std::vector<QCommandLinkButton*> m_buttons; + QString m_selection; + + void fillList(); +}; + + +class NamePage : public Page +{ +public: + NamePage(CreateInstanceDialog& dlg); + + bool ready() const override; + void activated() override; + QString selectedInstanceName() const override; + +protected: + bool doSkip() const override; + +private: + mutable PlaceholderLabel m_label, m_exists, m_invalid; + bool m_modified; + bool m_okay; + + void onChanged(); + void updateWarnings(); + bool checkName(QString parentDir, QString name); +}; + + +class PathsPage : public Page +{ +public: + PathsPage(CreateInstanceDialog& dlg); + + bool ready() const override; + void activated() override; + + CreateInstanceDialog::Paths selectedPaths() const override; + +private: + QString m_lastInstanceName; + CreateInstanceDialog::Types m_lastType; + PlaceholderLabel m_label; + mutable PlaceholderLabel m_simpleExists, m_simpleInvalid; + mutable PlaceholderLabel m_advancedExists, m_advancedInvalid; + + void onChanged(); + bool checkPaths() const; + bool checkAdvancedPath(const QString& path) const; + QString resolve(const QString& path) const; + void onAdvanced(); + void setPaths(const QString& name, bool force); + void setIfEmpty(QLineEdit* e, const QString& path, bool force); + bool checkPath( + QString path, + PlaceholderLabel& existsLabel, PlaceholderLabel& invalidLabel) const; +}; + + +class NexusPage : public Page +{ +public: + NexusPage(CreateInstanceDialog& dlg); + ~NexusPage(); + + bool ready() const override; + void activated() override; + +protected: + bool doSkip() const override; + +private: + std::unique_ptr<NexusConnectionUI> m_connectionUI; + bool m_skip; +}; + + +class ConfirmationPage : public Page +{ +public: + ConfirmationPage(CreateInstanceDialog& dlg); + + void activated() override; + + QString toLocalizedString(CreateInstanceDialog::Types t) const; + QString makeReview() const; + QString dirLine(const QString& caption, const QString& path) const; +}; + +} // namespace + +#endif // MODORGANIZER_CREATEINSTANCEDIALOGPAGES_INCLUDED diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp index 5db0cbdf..2927a972 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -42,7 +42,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <QCoreApplication> #include <QTextDocument> -#include <boost/bind.hpp> +#include <boost/bind/bind.hpp> #include <regex> @@ -1841,6 +1841,8 @@ int DownloadManager::indexByInfo(const DownloadInfo* info) const void DownloadManager::nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID) { + using namespace boost::placeholders; + std::set<int>::iterator idIter = m_RequestIDs.find(requestID); if (idIter == m_RequestIDs.end()) { return; diff --git a/src/env.cpp b/src/env.cpp index 4e5ad196..5bed84c1 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -16,9 +16,15 @@ using namespace MOBase; Console::Console() : m_hasConsole(false), m_in(nullptr), m_out(nullptr), m_err(nullptr) { - // open a console - if (!AllocConsole()) { - // failed, ignore + // try to attach to parent + if (!AttachConsole(ATTACH_PARENT_PROCESS)) { + if (GetLastError() != ERROR_ACCESS_DENIED) { + // parent has no console, create one + if (!AllocConsole()) { + // failed, ignore + return; + } + } } m_hasConsole = true; @@ -394,18 +400,18 @@ QString path() return get("PATH"); } -QString prependToPath(const QString& s) +QString appendToPath(const QString& s) { auto old = path(); - set("PATH", s + ";" + old); + set("PATH", old + ";" + s); return old; } -QString addPath(const QString& s) +QString prependToPath(const QString& s) { auto old = path(); - set("PATH", get("PATH") + ";" + s); - return old; + set("PATH", s + ";" + old); + return old; } QString setPath(const QString& s) @@ -840,6 +846,166 @@ Association getAssociation(const QFileInfo& targetInfo) } +struct RegistryKeyCloser +{ + using pointer = HKEY; + + void operator()(HKEY key) + { + if (key != 0) { + ::RegCloseKey(key); + } + } +}; + +using RegistryKeyPtr = std::unique_ptr<HKEY, RegistryKeyCloser>; + +RegistryKeyPtr openRegistryKey(HKEY parent, const wchar_t* name) +{ + HKEY subkey = 0; + + auto r = ::RegOpenKeyExW( + parent, name, + 0, KEY_SET_VALUE|KEY_ENUMERATE_SUB_KEYS|KEY_QUERY_VALUE, + &subkey); + + if (r != ERROR_SUCCESS) { + return {}; + } + + return RegistryKeyPtr(subkey); +} + +bool keyHasValues(HKEY key) +{ + auto name = std::make_unique<wchar_t[]>(1000 + 1); + DWORD nameSize = 1000; + + // note that RegEnumValueW() also enumerates the default value if it exists + auto r = ::RegEnumValueW( + key, 0, name.get(), &nameSize, nullptr, nullptr, nullptr, nullptr); + + if (r != ERROR_NO_MORE_ITEMS) { + return true; + } + + // no values, no default, it's empty + return false; +} + +bool forEachSubKey(HKEY key, std::function<bool (const wchar_t* name)> f) +{ + auto name = std::make_unique<wchar_t[]>(1000 + 1); + DWORD nameSize = 1000; + + DWORD i = 0; + + // something would be really wrong if it had more than 100 keys + while (i < 100) + { + auto r = ::RegEnumKeyExW( + key, i, name.get(), &nameSize, nullptr, nullptr, nullptr, nullptr); + + if (r == ERROR_NO_MORE_ITEMS) { + // no more subkeys + break; + } + + if (r == ERROR_SUCCESS) { + // a subkey exists + auto subkey = openRegistryKey(key, name.get()); + + if (!subkey) { + // can't open it, stop + return false; + } + + // fire callback + if (!f(name.get())) { + return false; + } + } else { + // something went wrong, stop + return false; + } + + ++i; + } + + return true; +} + +bool isKeyEmpty(HKEY key) +{ + // check for any values + if (keyHasValues(key)) { + return false; + } + + auto r = forEachSubKey(key, [&](const wchar_t* name) { + // a subkey exists, recursively check if it's empty + auto subkey = openRegistryKey(key, name); + + if (!subkey) { + // can't open, stop + return false; + } + + if (!isKeyEmpty(subkey.get())) { + // not empty, stop + return false; + } + + // empty, go on + return true; + }); + + if (!r) { + // something went wrong or some subkey has values + return false; + } + + // key has no values and has either no subkeys or all subkeys are empty + return true; +} + +void deleteRegistryKeyIfEmpty(const QString& name) +{ + if (name.isEmpty()) { + return; + } + + auto key = openRegistryKey(HKEY_CURRENT_USER, name.toStdWString().c_str()); + if (!key) { + return; + } + + if (!isKeyEmpty(key.get())) { + return; + } + + ::RegDeleteTreeW(HKEY_CURRENT_USER, name.toStdWString().c_str()); +} + +bool registryValueExists(const QString& keyName, const QString& valueName) +{ + auto key = openRegistryKey( + HKEY_CURRENT_USER, keyName.toStdWString().c_str()); + + if (!key) { + return false; + } + + DWORD type = 0; + + auto r = ::RegQueryValueExW( + key.get(), valueName.toStdWString().c_str(), + nullptr, &type, nullptr, nullptr); + + return (r == ERROR_SUCCESS); +} + + // returns the filename of the given process or the current one // std::wstring processFilename(HANDLE process=INVALID_HANDLE_VALUE) @@ -1039,6 +1205,36 @@ HandlePtr dumpFile() return {}; } + +CoreDumpTypes coreDumpTypeFromString(const std::string& s) +{ + if (s == "data") + return env::CoreDumpTypes::Data; + else if (s == "full") + return env::CoreDumpTypes::Full; + else + return env::CoreDumpTypes::Mini; +} + +std::string toString(CoreDumpTypes type) +{ + switch (type) + { + case CoreDumpTypes::Mini: + return "mini"; + + case CoreDumpTypes::Data: + return "data"; + + case CoreDumpTypes::Full: + return "full"; + + default: + return "?"; + } +} + + bool createMiniDump(HANDLE process, CoreDumpTypes type) { const DWORD pid = GetProcessId(process); @@ -236,8 +236,8 @@ QString get(const QString& name); QString set(const QString& name, const QString& value); QString path(); +QString appendToPath(const QString& s); QString prependToPath(const QString& s); -QString addPath(const QString& s); QString setPath(const QString& s); @@ -301,6 +301,15 @@ struct Association Association getAssociation(const QFileInfo& file); +// returns whether the given value exists +// +bool registryValueExists(const QString& key, const QString& value); + +// deletes a registry key if it's empty or only contains empty keys +// +void deleteRegistryKeyIfEmpty(const QString& name); + + enum class CoreDumpTypes { Mini = 1, @@ -308,6 +317,10 @@ enum class CoreDumpTypes Full }; + +CoreDumpTypes coreDumpTypeFromString(const std::string& s); +std::string toString(CoreDumpTypes type); + // creates a minidump file for this process // bool coredump(CoreDumpTypes type); diff --git a/src/envshortcut.cpp b/src/envshortcut.cpp index 99495c39..b13a7d9f 100644 --- a/src/envshortcut.cpp +++ b/src/envshortcut.cpp @@ -149,7 +149,7 @@ Shortcut::Shortcut(const Executable& exe) m_target = QFileInfo(qApp->applicationFilePath()).absoluteFilePath(); m_arguments = QString("\"moshortcut://%1:%2\"") - .arg(InstanceManager::instance().currentInstance()) + .arg(InstanceManager::singleton().currentInstance()->name()) .arg(exe.title()); m_description = QString("Run %1 with ModOrganizer").arg(exe.title()); diff --git a/src/instancemanager.cpp b/src/instancemanager.cpp index 6c7f797e..8163149b 100644 --- a/src/instancemanager.cpp +++ b/src/instancemanager.cpp @@ -20,9 +20,15 @@ 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 "shared/util.h" +#include <report.h> +#include <iplugingame.h> #include <utility.h> #include <log.h> -#include "shared/appconfig.h" + #include <QCoreApplication> #include <QDir> #include <QStandardPaths> @@ -32,258 +38,375 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. using namespace MOBase; -static const char COMPANY_NAME[] = "Tannin"; -static const char APPLICATION_NAME[] = "Mod Organizer"; -static const char INSTANCE_KEY[] = "CurrentInstance"; +Instance::Instance(QDir dir, bool portable, QString profileName) : + m_dir(std::move(dir)), m_portable(portable), m_plugin(nullptr), + m_profile(std::move(profileName)) +{ +} +QString Instance::name() const +{ + if (isPortable()) + return QObject::tr("Portable"); + else + return m_dir.dirName(); +} -InstanceManager::InstanceManager() - : m_AppSettings(COMPANY_NAME, APPLICATION_NAME) +QString Instance::gameName() const { + return m_gameName; } -InstanceManager &InstanceManager::instance() +QString Instance::gameDirectory() const { - static InstanceManager s_Instance; - return s_Instance; + return m_gameDir; } -void InstanceManager::overrideInstance(const QString& instanceName) +QDir Instance::directory() const { - m_overrideInstanceName = instanceName; - m_overrideInstance = true; + return m_dir; } +MOBase::IPluginGame* Instance::gamePlugin() const +{ + return m_plugin; +} -QString InstanceManager::currentInstance() const +QString Instance::profileName() const { - if (m_overrideInstance) - return m_overrideInstanceName; - else - return m_AppSettings.value(INSTANCE_KEY, "").toString(); + return m_profile; } -void InstanceManager::clearCurrentInstance() +QString Instance::iniPath() const { - setCurrentInstance(""); - m_Reset = true; - m_overrideInstance = false; + return InstanceManager::iniPath(m_dir); } -void InstanceManager::setCurrentInstance(const QString &name) +bool Instance::isPortable() const { - m_AppSettings.setValue(INSTANCE_KEY, name); + return m_portable; } -bool InstanceManager::deleteLocalInstance(const QString &instanceId) const +Instance::SetupResults Instance::setup(PluginContainer& plugins) { - bool result = true; - QString instancePath = QDir::fromNativeSeparators(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + instanceId); + Settings s(iniPath()); - if (QMessageBox::warning(nullptr, QObject::tr("Deleting folder"), - QObject::tr("I'm about to delete the following folder: \"%1\". Proceed?").arg(instancePath), QMessageBox::No | QMessageBox::Yes, QMessageBox::No) == QMessageBox::No ){ - return false; + if (s.iniStatus() != QSettings::NoError) { + log::error("can't read ini {}", iniPath()); + return SetupResults::BadIni; } - if (!MOBase::shellDelete(QStringList(instancePath),true)) - { - log::warn( - "Failed to shell-delete \"{}\" (errorcode {}), trying regular delete", - instancePath, ::GetLastError()); + if (m_gameName.isEmpty()) { + if (auto v=s.game().name()) + m_gameName = *v; + } - if (!MOBase::removeDir(instancePath)) - { - log::warn("regular delete failed too"); - result = false; + if (m_gameDir.isEmpty()) { + if (auto v=s.game().directory()) + 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; } } - return result; + if (m_gameVariant.isEmpty() && m_plugin->gameVariants().size() > 1) { + return SetupResults::MissingVariant; + } else { + m_plugin->setGameVariant(m_gameVariant); + } + + getProfile(s); + + s.game().setName(m_gameName); + s.game().setDirectory(m_gameDir); + s.game().setSelectedProfileName(m_profile); + + if (!m_gameVariant.isEmpty()) + s.game().setEdition(m_gameVariant); + + m_plugin->setGamePath(m_gameDir); + + return SetupResults::Ok; } -QString InstanceManager::manageInstances(const QStringList &instanceList) const +void Instance::setGame(const QString& name, const QString& dir) { - SelectionDialog selection( - QString("<h3>%1</h3><br>%2") - .arg(QObject::tr("Choose Instance to Delete")) - .arg(QObject::tr("Be Careful! Deleting an Instance will remove all your files for that Instance (mods, downloads, profiles, configuration, ...). Custom paths outside of the instance folder for downloads, mods, etc. will be left untoched.")), - nullptr); - for (const QString &instance : instanceList) - { - selection.addChoice(QIcon(":/MO/gui/multiply_red"), instance, "", instance); - } + m_gameName = name; + m_gameDir = dir; +} - if (selection.exec() == QDialog::Rejected) { - return(chooseInstance(instances())); - } - else { - QString choice = selection.getChoiceData().toString(); - { - if (QMessageBox::warning(nullptr, QObject::tr("Are you sure?"), - QObject::tr("Are you really sure you want to delete the Instance \"%1\" with all its files?").arg(choice), QMessageBox::No | QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes) - { - if (!deleteLocalInstance(choice)) - { - QMessageBox::warning(nullptr, QObject::tr("Failed to delete Instance"), - QObject::tr("Could not delete Instance \"%1\". \nIf the folder was still in use, restart MO and try again.").arg(choice), QMessageBox::Ok); - } - } - } - } - return(manageInstances(instances())); +void Instance::setVariant(const QString& name) +{ + m_gameVariant = name; } -QString InstanceManager::queryInstanceName(const QStringList &instanceList) const +Instance::SetupResults Instance::getGamePlugin(PluginContainer& plugins) { - QString instanceId; - QString dialogText; - while (instanceId.isEmpty()) { - QInputDialog dialog; + if (!m_gameName.isEmpty() && !m_gameDir.isEmpty()) + { + // normal case: both the name and dir are in the ini - dialog.setWindowTitle(QObject::tr("Enter a Name for the new Instance")); - dialog.setLabelText(QObject::tr("Enter a new name or select one from the suggested list: \n" - "(This is just a name for the Instance and can be whatever you wish,\n" - " the actual game selection will happen on the next screen regardless of chosen name)")); - // would be neat if we could take the names from the game plugins but - // the required initialization order requires the ini file to be - // available *before* we load plugins - dialog.setComboBoxItems({ "NewName", "Fallout 4", "SkyrimSE", "Skyrim", "SkyrimVR", "Fallout 3", - "Fallout NV", "TTW", "FO4VR", "Oblivion", "Morrowind", "Enderal" }); - dialog.setComboBoxEditable(true); + // find the plugin by name + for (IPluginGame* game : plugins.plugins<IPluginGame>()) { + if (m_gameName.compare(game->gameName(), Qt::CaseInsensitive) == 0) { + // plugin found, check if the game directory is valid - if (dialog.exec() == QDialog::Rejected) { - throw MOBase::MyException(QObject::tr("Canceled")); - } - dialogText = dialog.textValue(); - instanceId = sanitizeInstanceName(dialogText); - if (instanceId != dialogText) { - if (QMessageBox::question( nullptr, - QObject::tr("Invalid instance name"), - QObject::tr("The instance name \"%1\" is invalid. Use the name \"%2\" instead?").arg(dialogText,instanceId), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { - instanceId=""; - continue; + if (!game->looksValid(m_gameDir)) { + // the directory from the ini is not valid anymore + log::warn( + "game plugin {} says dir {} from ini {} is not valid", + game->gameName(), m_gameDir, iniPath()); + + // note that some plugins return true for isInstalled() if a path + // is found in the registry, but without actually checking if it's + // valid + + if (game->isInstalled() && game->looksValid(game->gameDirectory())) { + // bad game directory but the plugin reports there's a valid one + // somewhere; take it instead + log::warn( + "game plugin {} found a game at {}, taking it", + game->gameName(), game->gameDirectory().absolutePath()); + + m_gameDir = game->gameDirectory().absolutePath(); + } else { + // game seems to be gone completely + log::warn("game plugin {} found no game installation at all", game->gameName()); + return SetupResults::GameGone; + } } + + m_plugin = game; + return SetupResults::Ok; + } } - bool alreadyExists=false; - for (const QString &instance : instanceList) { - if(instanceId==instance) - alreadyExists=true; + log::warn("game plugin {} not found", m_gameName); + return SetupResults::PluginGone; + } + else if (m_gameName.isEmpty() && !m_gameDir.isEmpty()) + { + // the name is missing, but there's a directory; find a plugin that can + // handle it + + log::warn( + "game name is missing from ini {} but dir {} is available", + iniPath(), m_gameDir); + + for (IPluginGame* game : plugins.plugins<IPluginGame>()) { + if (game->looksValid(m_gameDir)) { + // take it + log::warn("found plugin {} that can use dir {}", game->gameName(), m_gameDir); + + m_plugin = game; + m_gameName = game->gameName(); + + return SetupResults::Ok; + } } - if(alreadyExists) - { - QMessageBox msgBox; - msgBox.setText( QObject::tr("The instance \"%1\" already exists.").arg(instanceId) ); - msgBox.setInformativeText(QObject::tr("Please choose a different instance name, like: \"%1 1\" .").arg(instanceId)); - msgBox.exec(); - instanceId=""; + + log::error("no plugins can use dir {}", m_gameDir); + return SetupResults::GameGone; + } + else if (!m_gameName.isEmpty() && m_gameDir.isEmpty()) + { + // dir is missing, find a plugin with the correct name and use the install + // dir it detected + + log::warn( + "game dir is missing from ini {} but name {} is available", + iniPath(), m_gameName); + + for (IPluginGame* game : plugins.plugins<IPluginGame>()) { + if (m_gameName.compare(game->gameName(), Qt::CaseInsensitive) == 0) { + // plugin found, use its detected installation dir + + if (game->isInstalled()) { + log::warn( + "found plugin {} that matches name in ini {}, using auto detected " + "game dir {}", + game->gameName(), iniPath(), game->gameDirectory().absolutePath()); + + m_plugin = game; + m_gameDir = game->gameDirectory().absolutePath(); + + return SetupResults::Ok; + } else { + log::warn( + "found plugin {} that matches name in ini {}, but no game install " + "detected by plugin", + game->gameName(), iniPath()); + + return SetupResults::GameGone; + } + } } + + // plugin seems to be gone + log::error("no plugin matches name {}", m_gameName); + return SetupResults::PluginGone; + } + else + { + // can't do anything with these two missing + log::error("both game name and dir are missing from ini {}", iniPath()); + return SetupResults::IniMissingGame; } - return instanceId; } -QString InstanceManager::chooseInstance(const QStringList &instanceList) const +void Instance::getProfile(const Settings& s) { - if (portableInstallIsLocked()) { - return QString(); + if (!m_profile.isEmpty()) { + // there's already a profile set up, probably an override + return; } - enum class Special : uint8_t { - NewInstance, - Portable, - Manage - }; - - SelectionDialog selection( - QString("<h3>%1</h3><br>%2") - .arg(QObject::tr("Choose Instance")) - .arg(QObject::tr( - "Each Instance is a full set of MO data files (mods, " - "downloads, profiles, configuration, ...). You can use multiple " - "instances for different games. Instances are stored in Appdata and can be accessed by all MO installations. " - "If your MO folder is writable, you can also store a single instance locally (called " - "a Portable install, and all the MO data files will be inside the installation folder).")), - nullptr); - selection.disableCancel(); - for (const QString &instance : instanceList) { - selection.addChoice(instance, "", instance); + if (auto name=s.game().selectedProfileName()) { + // use last profile + m_profile = *name; + return; } - selection.addChoice(QIcon(":/MO/gui/add"), QObject::tr("New"), - QObject::tr("Create a new instance."), - static_cast<uint8_t>(Special::NewInstance)); + // profile missing from ini, use the default + m_profile = QString::fromStdWString(AppConfig::defaultProfileName()); - if (QFileInfo(qApp->applicationDirPath()).isWritable()) { - selection.addChoice(QIcon(":/MO/gui/package"), QObject::tr("Portable"), - QObject::tr("Use MO folder for data."), - static_cast<uint8_t>(Special::Portable)); - } + log::warn( + "no profile found in ini {}, using default '{}'", + iniPath(), m_profile); +} + + +InstanceManager::InstanceManager() +{ + GlobalSettings::updateRegistryKey(); +} + +InstanceManager &InstanceManager::singleton() +{ + static InstanceManager s_Instance; + return s_Instance; +} - selection.addChoice(QIcon(":/MO/gui/remove"), QObject::tr("Manage Instances"), - QObject::tr("Delete an Instance."), - static_cast<uint8_t>(Special::Manage)); +void InstanceManager::overrideInstance(const QString& instanceName) +{ + m_overrideInstanceName = instanceName; + m_overrideInstance = true; +} - selection.setWindowFlags(selection.windowFlags() | Qt::WindowStaysOnTopHint); +void InstanceManager::overrideProfile(const QString& profileName) +{ + m_overrideProfileName = profileName; + m_overrideProfile = true; +} - if (selection.exec() == QDialog::Rejected) { - log::debug("rejected"); - throw MOBase::MyException(QObject::tr("Canceled")); +std::optional<Instance> InstanceManager::currentInstance() const +{ + const QString profile = m_overrideProfile ? m_overrideProfileName : ""; + + if (portableInstallIsLocked()) { + // force portable instance + return Instance(QDir(portablePath()), true, profile); } - QVariant choice = selection.getChoiceData(); + QString name; - if (choice.type() == QVariant::String) { - return choice.toString(); - } else { - switch (static_cast<Special>(choice.value<uint8_t>())) { - case Special::NewInstance: return queryInstanceName(instanceList); - case Special::Portable: return QString(); - case Special::Manage: { + if (m_overrideInstance) + name = m_overrideInstanceName; + else + name = GlobalSettings::currentInstance(); - return(manageInstances(instances())); - } - default: throw std::runtime_error("invalid selection"); + if (name.isEmpty()) { + if (portableInstanceExists()) { + // use portable + return Instance(QDir(portablePath()), true, profile); + } else { + // no instance set + return {}; } } + + return Instance(QDir(instancePath(name)), false, profile); } +void InstanceManager::clearCurrentInstance() +{ + setCurrentInstance(""); + m_overrideInstance = false; +} + +void InstanceManager::setCurrentInstance(const QString &name) +{ + GlobalSettings::setCurrentInstance(name); +} + +QString InstanceManager::instancePath(const QString& instanceName) const +{ + return QDir::fromNativeSeparators(instancesPath() + "/" + instanceName); +} -QString InstanceManager::instancePath() const +QString InstanceManager::instancesPath() const { return QDir::fromNativeSeparators( QStandardPaths::writableLocation(QStandardPaths::DataLocation)); } +QString InstanceManager::iniPath(const QDir& instanceDir) +{ + return instanceDir.filePath(QString::fromStdWString(AppConfig::iniFileName())); +} -QStringList InstanceManager::instances() const +std::vector<QDir> InstanceManager::instancePaths() const { const std::set<QString> ignore = { "cache", "qtwebengine", }; - const auto dirs = QDir(instancePath()) - .entryList(QDir::Dirs | QDir::NoDotAndDotDot); + const QDir root(instancesPath()); + const auto dirs = root.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - QStringList list; + std::vector<QDir> list; for (auto&& d : dirs) { if (!ignore.contains(QFileInfo(d).fileName().toLower())) { - list.push_back(d); + list.push_back(root.filePath(d)); } } 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) { - return (dataPath == qApp->applicationDirPath()); + return (dataPath == portablePath()); +} + +QString InstanceManager::portablePath() +{ + return qApp->applicationDirPath(); } -bool InstanceManager::portableInstall() const +bool InstanceManager::portableInstanceExists() const { return QFile::exists(qApp->applicationDirPath() + "/" + QString::fromStdWString(AppConfig::iniFileName())); @@ -296,67 +419,89 @@ bool InstanceManager::portableInstallIsLocked() const QString::fromStdWString(AppConfig::portableLockFileName())); } - bool InstanceManager::allowedToChangeInstance() const { return !portableInstallIsLocked(); } - -void InstanceManager::createDataPath(const QString &dataPath) const +const MOBase::IPluginGame* InstanceManager::gamePluginForDirectory( + const QDir& instanceDir, const PluginContainer& plugins) const { - if (!QDir(dataPath).exists()) { - if (!QDir().mkpath(dataPath)) { - throw MOBase::MyException( - QObject::tr("failed to create %1").arg(dataPath)); - } else { - QMessageBox::information( - nullptr, QObject::tr("Data directory created"), - QObject::tr("New data directory created at %1. If you don't want to " - "store a lot of data there, reconfigure the storage " - "directories via settings.").arg(dataPath)); - } + const QString ini = iniPath(instanceDir); + + Settings s(ini); + + if (s.iniStatus() != QSettings::NoError) + { + log::error("failed to load settings from {}", ini); + return nullptr; } -} + const auto instanceGameName = s.game().name(); -QString InstanceManager::determineDataPath() -{ - QString instanceId = currentInstance(); - if (portableInstallIsLocked()) + if (instanceGameName && !instanceGameName->isEmpty()) { - instanceId.clear(); + for (const IPluginGame* game : plugins.plugins<IPluginGame>()) { + if (instanceGameName->compare(game->gameName(), Qt::CaseInsensitive) == 0) { + return game; + } + } + + log::error( + "no plugin reports game name '{}' found in ini {}", + *instanceGameName, ini); } - if (instanceId.isEmpty() && !m_Reset && (m_overrideInstance || portableInstall())) + else { - // startup, apparently using portable mode before - return qApp->applicationDirPath(); + log::error("no game name found in ini {}", ini); } - QString dataPath = QDir::fromNativeSeparators( - QStandardPaths::writableLocation(QStandardPaths::DataLocation) - + "/" + instanceId); + log::error("falling back on looksValid check"); - if (!m_overrideInstance && (instanceId.isEmpty() || !QFileInfo::exists(dataPath))) { - instanceId = chooseInstance(instances()); - setCurrentInstance(instanceId); - if (!instanceId.isEmpty()) { - dataPath = QDir::fromNativeSeparators( - QStandardPaths::writableLocation(QStandardPaths::DataLocation) - + "/" + instanceId); + const auto gameDir = s.game().directory(); + + if (gameDir && !gameDir->isEmpty()) + { + for (const IPluginGame* game : plugins.plugins<IPluginGame>()) { + if (game->looksValid(*gameDir)) { + return game; + } } + + log::error( + "no plugins appear to support game directory '{}' from ini {}", + *gameDir, ini); + } + else + { + log::error("no game directory found in ini {}", ini); } - if (instanceId.isEmpty()) { - return qApp->applicationDirPath(); - } else { - createDataPath(dataPath); + return nullptr; +} + +QString InstanceManager::makeUniqueName(const QString& instanceName) const +{ + const QString sanitized = sanitizeInstanceName(instanceName); + + QString name = sanitized; + for (int i=2; i<100; ++i) { + if (!instanceExists(name)) { + return name; + } - return dataPath; + name = QString("%1 (%2)").arg(sanitized).arg(i); } + + return {}; } +bool InstanceManager::instanceExists(const QString& instanceName) const +{ + const QDir root = instancesPath(); + return root.exists(instanceName); +} QString InstanceManager::sanitizeInstanceName(const QString &name) const { @@ -375,3 +520,12 @@ QString InstanceManager::sanitizeInstanceName(const QString &name) const } return new_name; } + +bool InstanceManager::validInstanceName(const QString& instanceName) const +{ + if (instanceName.isEmpty()) { + return false; + } + + return (instanceName == sanitizeInstanceName(instanceName)); +} diff --git a/src/instancemanager.h b/src/instancemanager.h index 649c2195..79f1f30b 100644 --- a/src/instancemanager.h +++ b/src/instancemanager.h @@ -1,72 +1,96 @@ -/* -Copyright (C) 2016 Sebastian Herbord. All rights reserved. +#ifndef MODORGANIZER_INSTANCEMANAGER_INCLUDED +#define MODORGANIZER_INSTANCEMANAGER_INCLUDED -This file is part of Mod Organizer. +#include <QString> +#include <QSettings> -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. +namespace MOBase { class IPluginGame; } -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. +class Settings; +class PluginContainer; -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. -*/ +class Instance +{ +public: + enum class SetupResults + { + Ok, + BadIni, + IniMissingGame, + PluginGone, + GameGone, + MissingVariant + }; -#pragma once + Instance(QDir dir, bool portable, QString profileName={}); + SetupResults setup(PluginContainer& plugins); -#include <QString> -#include <QSettings> + void setGame(const QString& name, const QString& dir); + void setVariant(const QString& name); + QString name() const; + QString gameName() const; + QString gameDirectory() const; + QDir directory() const; + MOBase::IPluginGame* gamePlugin() const; + QString profileName() const; + QString iniPath() const; + bool isPortable() const; -class InstanceManager { +private: + QDir m_dir; + bool m_portable; + QString m_gameName, m_gameDir, m_gameVariant; + MOBase::IPluginGame* m_plugin; + QString m_profile; -public: + SetupResults getGamePlugin(PluginContainer& plugins); + void getProfile(const Settings& s); +}; - static InstanceManager &instance(); - QString determineDataPath(); - void clearCurrentInstance(); +class InstanceManager +{ +public: + static InstanceManager& singleton(); void overrideInstance(const QString& instanceName); + void overrideProfile(const QString& profileName); - QString currentInstance() const; + const MOBase::IPluginGame* gamePluginForDirectory( + const QDir& dir, const PluginContainer& plugins) const; + + void clearCurrentInstance(); + std::optional<Instance> currentInstance() const; + void setCurrentInstance(const QString &name); bool allowedToChangeInstance() const; static bool isPortablePath(const QString& dataPath); + static QString portablePath(); + bool portableInstanceExists() const; -private: - - InstanceManager(); - - QString instancePath() const; - - QStringList instances() const; - - bool deleteLocalInstance(const QString &instanceId) const; - - QString manageInstances(const QStringList &instanceList) const; + QString instancesPath() const; + QStringList instanceNames() const; + std::vector<QDir> instancePaths() const; QString sanitizeInstanceName(const QString &name) const; - void setCurrentInstance(const QString &name); - - QString queryInstanceName(const QStringList &instanceList) const; - QString chooseInstance(const QStringList &instanceList) const; + QString makeUniqueName(const QString& instanceName) const; + bool instanceExists(const QString& instanceName) const; + bool validInstanceName(const QString& instanceName) const; + QString instancePath(const QString& instanceName) const; + static QString iniPath(const QDir& instanceDir); - void createDataPath(const QString &dataPath) const; - bool portableInstall() const; +private: + InstanceManager(); bool portableInstallIsLocked() const; private: - - QSettings m_AppSettings; - bool m_Reset {false}; bool m_overrideInstance{false}; QString m_overrideInstanceName; + bool m_overrideProfile{false}; + QString m_overrideProfileName; }; + +#endif // MODORGANIZER_INSTANCEMANAGER_INCLUDED diff --git a/src/instancemanagerdialog.cpp b/src/instancemanagerdialog.cpp new file mode 100644 index 00000000..3b6daeb8 --- /dev/null +++ b/src/instancemanagerdialog.cpp @@ -0,0 +1,853 @@ +#include "instancemanagerdialog.h" +#include "ui_instancemanagerdialog.h" +#include "instancemanager.h" +#include "createinstancedialog.h" +#include "settings.h" +#include "selectiondialog.h" +#include "plugincontainer.h" +#include "shared/appconfig.h" +#include "shared/util.h" +#include <utility.h> +#include <report.h> +#include <iplugingame.h> + +using namespace MOBase; + +class InstanceInfo +{ +public: + struct Object + { + QString path; + bool mandatoryDelete; + + Object(QString p, bool d=false) + : path(std::move(p)), mandatoryDelete(d) + { + } + + bool operator<(const Object& o) const + { + if (mandatoryDelete && !o.mandatoryDelete) { + return true; + } else if (!mandatoryDelete && o.mandatoryDelete) { + return false; + } + + return false; + } + }; + + + InstanceInfo(QDir dir, bool isPortable) + : m_portable(isPortable) + { + setDir(dir); + } + + void setDir(const QDir& dir) + { + m_dir = dir; + m_settings.reset(new Settings(InstanceManager::iniPath(dir))); + } + + QString name() const + { + if (m_portable) { + return QObject::tr("Portable"); + } else { + return m_dir.dirName(); + } + } + + QString gameName() const + { + if (auto n=m_settings->game().name()) { + if (auto e=m_settings->game().edition()) { + if (!e->isEmpty()) { + return *n + " (" + *e + ")"; + } + } + + return *n; + } else { + return {}; + } + } + + QString gamePath() const + { + if (auto n=m_settings->game().directory()) { + return QDir::toNativeSeparators(*n); + } else { + return {}; + } + } + + QString location() const + { + return QDir::toNativeSeparators(m_dir.path()); + } + + QString baseDirectory() const + { + return QDir::toNativeSeparators(m_settings->paths().base()); + } + + QString iniFile() const + { + return InstanceManager::iniPath(m_dir); + } + + QIcon icon(const PluginContainer& plugins) const + { + const auto* game = InstanceManager::singleton().gamePluginForDirectory( + m_dir, plugins); + + if (game) + return game->gameIcon(); + + QPixmap empty(32, 32); + empty.fill(QColor(0, 0, 0, 0)); + return QIcon(empty); + } + + bool isPortable() const + { + return m_portable; + } + + bool isActive() const + { + auto& m = InstanceManager::singleton(); + + if (auto i=m.currentInstance()) + { + if (m_portable) { + return i->isPortable(); + } else { + return (i->name() == name()); + } + } + + return false; + } + + // returns a list of files and folders that must be deleted when deleting + // this instance + // + std::vector<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}); + } + } + } + + + // 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; + } + +private: + const bool m_portable; + QDir m_dir; + std::unique_ptr<Settings> m_settings; +}; + + +InstanceManagerDialog::~InstanceManagerDialog() = default; + +InstanceManagerDialog::InstanceManagerDialog( + const PluginContainer& pc, QWidget *parent) : + QDialog(parent), ui(new Ui::InstanceManagerDialog), m_pc(pc), + m_model(nullptr), m_restartOnSelect(true) +{ + ui->setupUi(this); + + ui->splitter->setSizes({200, 1}); + ui->splitter->setStretchFactor(0, 0); + ui->splitter->setStretchFactor(1, 1); + + m_model = new QStandardItemModel; + ui->list->setModel(m_model); + + m_filter.setEdit(ui->filter); + m_filter.setList(ui->list); + m_filter.setUpdateDelay(false); + m_filter.setFilteredBorder(false); + + updateInstances(); + updateList(); + selectActiveInstance(); + + connect(ui->createNew, &QPushButton::clicked, [&]{ createNew(); }); + + connect(ui->list->selectionModel(), &QItemSelectionModel::selectionChanged, [&]{ onSelection(); }); + connect(ui->list, &QListView::activated, [&]{ openSelectedInstance(); }); + + connect(ui->rename, &QPushButton::clicked, [&]{ rename(); }); + connect(ui->exploreLocation, &QPushButton::clicked, [&]{ exploreLocation(); }); + connect(ui->exploreBaseDirectory, &QPushButton::clicked, [&]{ exploreBaseDirectory(); }); + connect(ui->exploreGame, &QPushButton::clicked, [&]{ exploreGame(); }); + + connect(ui->convertToGlobal, &QPushButton::clicked, [&]{ convertToGlobal(); }); + connect(ui->convertToPortable, &QPushButton::clicked, [&]{ convertToPortable(); }); + connect(ui->openINI, &QPushButton::clicked, [&]{ openINI(); }); + connect(ui->deleteInstance, &QPushButton::clicked, [&]{ deleteInstance(); }); + + connect(ui->switchToInstance, &QPushButton::clicked, [&]{ openSelectedInstance(); }); + connect(ui->close, &QPushButton::clicked, [&]{ close(); }); +} + +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)); + } + + // sort first, prepend portable after so it's always on top + std::sort(m_instances.begin(), m_instances.end(), [](auto&& a, auto&& b) { + return (MOBase::naturalCompare(a->name(), b->name()) < 0); + }); + + if (m.portableInstanceExists()) { + m_instances.insert( + m_instances.begin(), + std::make_unique<InstanceInfo>(m.portablePath(), true)); + } +} + +void InstanceManagerDialog::updateList() +{ + const auto prevSelIndex = singleSelectionIndex(); + const auto* prevSel = singleSelection(); + + m_model->clear(); + + const std::size_t NoSel = -1; + std::size_t sel = NoSel; + + 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)); + + m_model->appendRow(item); + + if (&ii == prevSel) { + sel = i; + } + } + + // keep current selection or select the next one if there was a selection; + // there's no selection when opening the dialog, that's handled in the ctor + if (prevSel) { + if (m_instances.empty()) { + select(-1); + } else { + if (sel == NoSel) { + if (prevSelIndex >= m_instances.size()) { + sel = m_instances.size() - 1; + } else { + sel = prevSelIndex; + } + } + + select(sel); + } + } +} + +void InstanceManagerDialog::select(std::size_t i) +{ + if (i < m_instances.size()) { + const auto& ii = m_instances[i]; + fillData(*ii); + + ui->list->selectionModel()->select( + m_filter.mapFromSource(m_filter.sourceModel()->index(i, 0)), + QItemSelectionModel::ClearAndSelect); + } else { + clearData(); + } +} + +void InstanceManagerDialog::select(const QString& name) +{ + for (std::size_t i=0; i<m_instances.size(); ++i) { + if (m_instances[i]->name() == name) { + select(i); + return; + } + } + + log::error("can't select instance {}, not in list", name); +} + +void InstanceManagerDialog::selectActiveInstance() +{ + const auto active = InstanceManager::singleton().currentInstance(); + + if (active) { + for (std::size_t i=0; i<m_instances.size(); ++i) { + if (m_instances[i]->name() == active->name()) { + select(i); + + ui->list->scrollTo( + m_filter.mapFromSource(m_filter.sourceModel()->index(i, 0))); + + return; + } + } + } + + select(0); +} + +void InstanceManagerDialog::openSelectedInstance() +{ + const auto i = singleSelectionIndex(); + if (i == NoSelection) { + return; + } + + if (m_instances[i]->isPortable()) { + InstanceManager::singleton().setCurrentInstance(""); + } else { + InstanceManager::singleton().setCurrentInstance(m_instances[i]->name()); + } + + if (m_restartOnSelect) { + ExitModOrganizer(Exit::Restart); + } + + accept(); +} + +QString getInstanceName( + QWidget* parent, const QString& title, const QString& moreText, + const QString& label, const QString& oldName={}) +{ + auto& m = InstanceManager::singleton(); + + QDialog dlg(parent); + dlg.setWindowTitle(title); + + auto* ly = new QVBoxLayout(&dlg); + + auto* bb = new QDialogButtonBox( + QDialogButtonBox::Cancel | QDialogButtonBox::Ok); + + auto* text = new QLineEdit(oldName); + text->selectAll(); + + auto* error = new QLabel; + + if (!moreText.isEmpty()) { + auto* lb = new QLabel(moreText); + lb->setWordWrap(true); + ly->addWidget(lb); + ly->addSpacing(10); + } + + auto* lb = new QLabel(label); + lb->setWordWrap(true); + ly->addWidget(lb); + + ly->addWidget(text); + ly->addWidget(error); + ly->addStretch(); + ly->addWidget(bb); + + auto check = [&] { + bool okay = false; + + if (text->text().isEmpty()) { + error->setText(""); + } else if (!m.validInstanceName(text->text())) { + error->setText(QObject::tr("The instance name must be a valid folder name.")); + } else { + const auto name = m.sanitizeInstanceName(text->text()); + + if ((name != oldName) && m.instanceExists(text->text())) { + error->setText(QObject::tr("An instance with this name already exists.")); + } else { + okay = true; + } + } + + error->setVisible(!okay); + bb->button(QDialogButtonBox::Ok)->setEnabled(okay); + }; + + QObject::connect(text, &QLineEdit::textChanged, [&] { check(); }); + QObject::connect(bb, &QDialogButtonBox::accepted, [&]{ dlg.accept(); }); + QObject::connect(bb, &QDialogButtonBox::rejected, [&]{ dlg.reject(); }); + + check(); + + dlg.resize({400, 120}); + if (dlg.exec() != QDialog::Accepted) { + return {}; + } + + return m.sanitizeInstanceName(text->text()); +} + +void InstanceManagerDialog::rename() +{ + auto* i = singleSelection(); + if (!i) { + return; + } + + const auto selIndex = singleSelectionIndex(); + + auto& m = InstanceManager::singleton(); + if (i->isActive()) { + QMessageBox::information(this, + tr("Rename instance"), tr("The active instance cannot be renamed.")); + return; + } + + const auto newName = getInstanceName( + this, tr("Rename instance"), "", tr("Instance name"), i->name()); + + if (newName.isEmpty()) { + return; + } + + const QString src = i->location(); + const QString dest = QDir::toNativeSeparators( + QFileInfo(i->location()).dir().path() + "/" + newName); + + const auto r = shell::Rename(src, dest, false); + if (!r) { + QMessageBox::critical( + this, tr("Error"), + tr("Failed to rename \"%1\" to \"%2\": %3") + .arg(src).arg(dest).arg(r.toString())); + + return; + } + + m_model->item(selIndex)->setText(newName); + i->setDir(dest); + fillData(*i); +} + +void InstanceManagerDialog::exploreLocation() +{ + if (const auto* i=singleSelection()) { + shell::Explore(i->location()); + } +} + +void InstanceManagerDialog::exploreBaseDirectory() +{ + if (const auto* i=singleSelection()) { + shell::Explore(i->baseDirectory()); + } +} + +void InstanceManagerDialog::exploreGame() +{ + if (const auto* i=singleSelection()) { + shell::Explore(i->gamePath()); + } +} + +void InstanceManagerDialog::openINI() +{ + if (const auto* i=singleSelection()) { + shell::Open(i->iniFile()); + } +} + +void InstanceManagerDialog::deleteInstance() +{ + const auto* i = singleSelection(); + if (!i) { + return; + } + + auto& m = InstanceManager::singleton(); + if (i->isActive()) { + QMessageBox::information(this, + tr("Deleting instance"), tr("The active instance cannot be deleted.")); + return; + } + + const auto Recycle = QMessageBox::Save; + const auto Delete = QMessageBox::Yes; + const auto Cancel = QMessageBox::Cancel; + + const auto files = i->objectsForDeletion(); + + MOBase::TaskDialog dlg(this); + + dlg + .title(tr("Deleting instance")) + .main(tr("These files and folders will be deleted")) + .content(tr("All checked items will be deleted.")) + .icon(QMessageBox::Warning) + .button({tr("Move to the recycle bin"), Recycle}) + .button({tr("Delete permanently"), Delete}) + .button({tr("Cancel"), Cancel}); + + + auto* list = new QListWidget(); + list->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + list->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + list->setMaximumHeight(160); + + for (const auto& f : files) { + auto* item = new QListWidgetItem(f.path); + + if (f.mandatoryDelete) { + item->setFlags(item->flags() & (~Qt::ItemIsEnabled)); + } else { + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + } + + item->setCheckState(Qt::Checked); + list->addItem(item); + } + + dlg.addContent(list); + dlg.setWidth(600); + + + const auto r = dlg.exec(); + + if (r != Recycle && r != Delete) { + return; + } + + + QStringList selected; + + for (int i=0; i<list->count(); ++i) { + if (list->item(i)->checkState() == Qt::Checked) { + selected.append(list->item(i)->text()); + } + } + + if (selected.isEmpty()) { + QMessageBox::information( + this, tr("Deleting instance"), tr("Nothing to delete.")); + + return; + } + + if (!doDelete(selected, (r == Recycle))) { + return; + } + + updateInstances(); + updateList(); +} + + +void InstanceManagerDialog::setRestartOnSelect(bool b) +{ + m_restartOnSelect = b; +} + +bool InstanceManagerDialog::doDelete(const QStringList& files, bool recycle) +{ + if (MOBase::shellDelete(files, recycle, this)) { + return true; + } + + const auto e = GetLastError(); + if (e == ERROR_CANCELLED) { + log::debug("deletion cancelled by user"); + } else { + log::error("failed to delete, {}", formatSystemMessage(e)); + } + + return false; +} + +void InstanceManagerDialog::convertToGlobal() +{ + // not implemented +} + +void InstanceManagerDialog::convertToPortable() +{ + // not implemented +} + +void InstanceManagerDialog::onSelection() +{ + const auto i = singleSelectionIndex(); + if (i == NoSelection) { + return; + } + + select(i); +} + +void InstanceManagerDialog::createNew() +{ + CreateInstanceDialog dlg(m_pc, &Settings::instance(), this); + if (dlg.exec() != QDialog::Accepted) { + return; + } + + if (dlg.switching()) { + // restarting MO + return; + } + + updateInstances(); + updateList(); + + select(dlg.instanceName()); +} + +std::size_t InstanceManagerDialog::singleSelectionIndex() const +{ + const auto sel = m_filter.mapSelectionToSource( + ui->list->selectionModel()->selection()); + + if (sel.size() != 1) { + return NoSelection; + } + + 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 auto i = singleSelectionIndex(); + if (i == NoSelection) { + return nullptr; + } + + return m_instances[i].get(); +} + +void InstanceManagerDialog::fillData(const InstanceInfo& ii) +{ + ui->name->setText(ii.name()); + ui->location->setText(ii.location()); + ui->baseDirectory->setText(ii.baseDirectory()); + ui->gameName->setText(ii.gameName()); + ui->gameDir->setText(ii.gamePath()); + setButtonsEnabled(true); + + const auto& m = InstanceManager::singleton(); + + ui->rename->setEnabled(!ii.isPortable()); + + if (ii.isPortable()) { + ui->convertToPortable->setVisible(false); + ui->convertToGlobal->setVisible(true); + ui->convertToGlobal->setEnabled(true); + } else { + ui->convertToPortable->setVisible(true); + ui->convertToGlobal->setVisible(false); + + if (m.portableInstanceExists()) { + ui->convertToPortable->setEnabled(false); + ui->convertToPortable->setToolTip(tr("A portable instance already exists.")); + } else { + ui->convertToPortable->setEnabled(false); + ui->convertToPortable->setToolTip(""); + } + } + + // not implemented, hide the buttons + ui->convertToPortable->setVisible(false); + ui->convertToGlobal->setVisible(false); +} + +void InstanceManagerDialog::clearData() +{ + ui->name->clear(); + ui->location->clear(); + ui->baseDirectory->clear(); + ui->gameName->clear(); + ui->gameDir->clear(); + + setButtonsEnabled(false); + + ui->convertToPortable->setVisible(false); + ui->convertToGlobal->setVisible(false); +} + +void InstanceManagerDialog::setButtonsEnabled(bool b) +{ + ui->rename->setEnabled(b); + ui->exploreLocation->setEnabled(b); + ui->exploreBaseDirectory->setEnabled(b); + ui->exploreGame->setEnabled(b); + ui->convertToPortable->setEnabled(b); + ui->convertToGlobal->setEnabled(b); + ui->deleteInstance->setEnabled(b); + ui->switchToInstance->setEnabled(b); +} diff --git a/src/instancemanagerdialog.h b/src/instancemanagerdialog.h new file mode 100644 index 00000000..5b08ffc2 --- /dev/null +++ b/src/instancemanagerdialog.h @@ -0,0 +1,68 @@ +#ifndef MODORGANIZER_INSTANCEMANAGERDIALOG_INCLUDED +#define MODORGANIZER_INSTANCEMANAGERDIALOG_INCLUDED + +#include <filterwidget.h> +#include <QDialog> + +namespace Ui { class InstanceManagerDialog; }; + +class InstanceInfo; +class PluginContainer; + +class InstanceManagerDialog : public QDialog +{ + Q_OBJECT + +public: + explicit InstanceManagerDialog( + const PluginContainer& pc, QWidget *parent = nullptr); + + ~InstanceManagerDialog(); + + void select(std::size_t i); + void select(const QString& name); + void selectActiveInstance(); + void openSelectedInstance(); + + void rename(); + void exploreLocation(); + void exploreBaseDirectory(); + void exploreGame(); + + void convertToGlobal(); + void convertToPortable(); + void openINI(); + void deleteInstance(); + + void setRestartOnSelect(bool b); + +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; + MOBase::FilterWidget m_filter; + QStandardItemModel* m_model; + bool m_restartOnSelect; + + void updateInstances(); + + void onSelection(); + void createNew(); + + std::size_t singleSelectionIndex() const; + InstanceInfo* singleSelection(); + const InstanceInfo* singleSelection() const; + + void updateList(); + void fillData(const InstanceInfo& ii); + void clearData(); + void setButtonsEnabled(bool b); + + bool deletePortable(const InstanceInfo& ii); + bool deleteGlobal(const InstanceInfo& ii); + bool doDelete(const QStringList& files, bool recycle); +}; + +#endif // MODORGANIZER_INSTANCEMANAGERDIALOG_INCLUDED diff --git a/src/instancemanagerdialog.ui b/src/instancemanagerdialog.ui new file mode 100644 index 00000000..410b58c1 --- /dev/null +++ b/src/instancemanagerdialog.ui @@ -0,0 +1,444 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>InstanceManagerDialog</class> + <widget class="QDialog" name="InstanceManagerDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>689</width> + <height>413</height> + </rect> + </property> + <property name="windowTitle"> + <string>Instance manager</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <item> + <widget class="QWidget" name="widget_8" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <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>5</number> + </property> + <item> + <widget class="QPushButton" name="createNew"> + <property name="text"> + <string>Create new instance</string> + </property> + <property name="icon"> + <iconset resource="resources.qrc"> + <normaloff>:/MO/gui/add</normaloff>:/MO/gui/add</iconset> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <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> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_5" stretch="1"> + <item> + <widget class="QWidget" name="widget_4" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <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"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <widget class="QWidget" name="widget_2" native="true"> + <layout class="QVBoxLayout" name="verticalLayout"> + <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="QListView" name="list"> + <property name="editTriggers"> + <set>QAbstractItemView::NoEditTriggers</set> + </property> + <property name="verticalScrollMode"> + <enum>QAbstractItemView::ScrollPerPixel</enum> + </property> + <property name="horizontalScrollMode"> + <enum>QAbstractItemView::ScrollPerPixel</enum> + </property> + <property name="resizeMode"> + <enum>QListView::Adjust</enum> + </property> + <property name="uniformItemSizes"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QWidget" name="widget_5" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <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="QLineEdit" name="filter"> + <property name="placeholderText"> + <string>Filter</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="widget_3" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,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> + <widget class="QWidget" name="widget_7" native="true"> + <layout class="QGridLayout" name="gridLayout" columnstretch="0,0,0"> + <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 row="0" column="1"> + <widget class="QLineEdit" name="name"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Name</string> + </property> + </widget> + </item> + <item row="4" column="2"> + <widget class="QPushButton" name="exploreLocation"> + <property name="text"> + <string>Explore</string> + </property> + </widget> + </item> + <item row="6" column="0"> + <widget class="QLabel" name="label_8"> + <property name="text"> + <string>Game</string> + </property> + </widget> + </item> + <item row="6" column="1"> + <widget class="QLineEdit" name="gameName"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QLineEdit" name="location"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Location</string> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QPushButton" name="rename"> + <property name="text"> + <string>Rename</string> + </property> + </widget> + </item> + <item row="7" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Game location</string> + </property> + </widget> + </item> + <item row="5" column="0"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>Base folder</string> + </property> + </widget> + </item> + <item row="5" column="1"> + <widget class="QLineEdit" name="baseDirectory"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="5" column="2"> + <widget class="QPushButton" name="exploreBaseDirectory"> + <property name="text"> + <string>Explore</string> + </property> + </widget> + </item> + <item row="7" column="1"> + <widget class="QLineEdit" name="gameDir"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="7" column="2"> + <widget class="QPushButton" name="exploreGame"> + <property name="text"> + <string>Explore</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>10</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QWidget" name="widget_9" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <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="QPushButton" name="convertToPortable"> + <property name="text"> + <string>Convert to portable</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="convertToGlobal"> + <property name="text"> + <string>Convert to global</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="openINI"> + <property name="text"> + <string>Open INI</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="deleteInstance"> + <property name="text"> + <string>Delete instance</string> + </property> + <property name="icon"> + <iconset resource="resources.qrc"> + <normaloff>:/MO/gui/remove</normaloff>:/MO/gui/remove</iconset> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QWidget" name="widget" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <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> + <spacer name="horizontalSpacer_4"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="switchToInstance"> + <property name="text"> + <string>Switch to this instance</string> + </property> + <property name="icon"> + <iconset resource="resources.qrc"> + <normaloff>:/MO/gui/next</normaloff>:/MO/gui/next</iconset> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="close"> + <property name="text"> + <string>Close</string> + </property> + <property name="default"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>LinkLabel</class> + <extends>QLabel</extends> + <header location="global">linklabel.h</header> + </customwidget> + </customwidgets> + <resources> + <include location="resources.qrc"/> + </resources> + <connections/> +</ui> diff --git a/src/loglist.cpp b/src/loglist.cpp index a4868ed4..fad4678c 100644 --- a/src/loglist.cpp +++ b/src/loglist.cpp @@ -262,3 +262,96 @@ void LogList::onContextMenu(const QPoint& pos) auto* menu = createMenu(this);
menu->popup(viewport()->mapToGlobal(pos));
}
+
+
+log::Levels convertQtLevel(QtMsgType t)
+{
+ switch (t)
+ {
+ case QtDebugMsg:
+ return log::Debug;
+
+ case QtWarningMsg:
+ return log::Warning;
+
+ case QtCriticalMsg: // fall-through
+ case QtFatalMsg:
+ return log::Error;
+
+ case QtInfoMsg: // fall-through
+ default:
+ return log::Info;
+ }
+}
+
+void qtLogCallback(
+ QtMsgType type, const QMessageLogContext& context, const QString& message)
+{
+ std::string_view file = "";
+
+ if (type != QtDebugMsg) {
+ if (context.file) {
+ file = context.file;
+
+ const auto lastSep = file.find_last_of("/\\");
+ if (lastSep != std::string_view::npos) {
+ file = {context.file + lastSep + 1};
+ }
+ }
+ }
+
+ if (file.empty()) {
+ log::log(
+ convertQtLevel(type), "{}",
+ message.toStdString());
+ } else {
+ log::log(
+ convertQtLevel(type), "[{}:{}] {}",
+ file, context.line, message.toStdString());
+ }
+}
+
+void initLogging()
+{
+ LogModel::create();
+
+ log::LoggerConfiguration conf;
+ conf.maxLevel = MOBase::log::Debug;
+ conf.pattern = "%^[%Y-%m-%d %H:%M:%S.%e %L] %v%$";
+ conf.utc = true;
+
+ log::createDefault(conf);
+
+ log::getDefault().setCallback(
+ [](log::Entry e){ LogModel::instance().add(e); });
+
+ qInstallMessageHandler(qtLogCallback);
+}
+
+bool createAndMakeWritable(const std::wstring &subPath) {
+ QString const dataPath = qApp->property("dataPath").toString();
+ QString fullPath = dataPath + "/" + QString::fromStdWString(subPath);
+
+ if (!QDir(fullPath).exists() && !QDir().mkdir(fullPath)) {
+ QMessageBox::critical(nullptr, QObject::tr("Error"),
+ QObject::tr("Failed to create \"%1\". Your user "
+ "account probably lacks permission.")
+ .arg(fullPath));
+ return false;
+ } else {
+ return true;
+ }
+}
+
+bool setLogDirectory(const QString& dir)
+{
+ const auto logFile = dir + "/logs/mo_interface.log";
+
+ if (!createAndMakeWritable(AppConfig::logPath())) {
+ return false;
+ }
+
+ log::getDefault().setFile(MOBase::log::File::single(logFile.toStdWString()));
+
+ return true;
+}
diff --git a/src/loglist.h b/src/loglist.h index 6b3aa4d5..0745ed3e 100644 --- a/src/loglist.h +++ b/src/loglist.h @@ -82,4 +82,8 @@ private: void onNewEntry();
};
+
+void initLogging();
+bool setLogDirectory(const QString& dir);
+
#endif // LOGBUFFER_H
diff --git a/src/main.cpp b/src/main.cpp index 6b1ec482..bfd23103 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,136 +17,85 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. */ - -#ifdef LEAK_CHECK_WITH_VLD -#include <wchar.h> -#include <vld.h> -#endif // LEAK_CHECK_WITH_VLD - -#define WIN32_LEAN_AND_MEAN -#include <windows.h> -#include <DbgHelp.h> - -#include "shared/appconfig.h" -#include <utility.h> -#include <scopeguard.h> #include "mainwindow.h" -#include <report.h> -#include "modlist.h" -#include "profile.h" -#include "spawn.h" -#include "executableslist.h" #include "singleinstance.h" -#include "utility.h" #include "loglist.h" #include "selectiondialog.h" #include "moapplication.h" #include "tutorialmanager.h" #include "nxmaccessmanager.h" #include "instancemanager.h" -#include "moshortcut.h" +#include "instancemanagerdialog.h" +#include "createinstancedialog.h" +#include "createinstancedialogpages.h" #include "organizercore.h" #include "env.h" #include "envmodule.h" +#include "commandline.h" #include "shared/util.h" +#include "shared/appconfig.h" -#include <eh.h> -#include "shared/windows_error.h" +#include <imoinfo.h> +#include <report.h> #include <usvfs.h> #include <log.h> +#include <utility.h> -#include <QApplication> -#include <QPushButton> -#include <QListWidget> -#include <QComboBox> -#include <QCheckBox> -#include <QDir> -#include <QFileInfo> -#include <QWhatsThis> -#include <QToolBar> -#include <QFileDialog> -#include <QDesktopServices> -#include <QMessageBox> -#include <QSharedMemory> -#include <QBuffer> -#include <QSplashScreen> -#include <QDirIterator> -#include <QDesktopServices> -#include <QLibraryInfo> -#include <QSslSocket> -#include <QtPlatformHeaders/QWindowsWindowFunctions> - -#include <boost/scoped_array.hpp> - -#include <ShellAPI.h> - -#include <cstdarg> -#include <iostream> -#include <sstream> -#include <stdexcept> - - -#pragma comment(linker, "/manifestDependency:\"name='dlls' processorArchitecture='x86' version='1.0.0.0' type='win32' \"") +// 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); -bool createAndMakeWritable(const std::wstring &subPath) { - QString const dataPath = qApp->property("dataPath").toString(); - QString fullPath = dataPath + "/" + QString::fromStdWString(subPath); - - if (!QDir(fullPath).exists() && !QDir().mkdir(fullPath)) { - QMessageBox::critical(nullptr, QObject::tr("Error"), - QObject::tr("Failed to create \"%1\". Your user " - "account probably lacks permission.") - .arg(fullPath)); - return false; - } else { - return true; - } -} - -bool bootstrap() +void purgeOldFiles() { - // remove the temporary backup directory in case we're restarting after an update + // 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); - - return true; + removeOldFiles( + qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath()), + "usvfs*.log", 5, QDir::Name); } -thread_local LPTOP_LEVEL_EXCEPTION_FILTER prevUnhandledExceptionFilter = nullptr; -thread_local std::terminate_handler prevTerminateHandler = nullptr; +thread_local LPTOP_LEVEL_EXCEPTION_FILTER g_prevExceptionFilter = nullptr; +thread_local std::terminate_handler g_prevTerminateHandler = nullptr; -LONG WINAPI MyUnhandledExceptionFilter(struct _EXCEPTION_POINTERS *exceptionPtrs) +LONG WINAPI onUnhandledException(_EXCEPTION_POINTERS* ptrs) { const std::wstring& dumpPath = OrganizerCore::crashDumpsPath(); - int dumpRes = - CreateMiniDump(exceptionPtrs, OrganizerCore::getGlobalCrashDumpsType(), dumpPath.c_str()); - if (!dumpRes) + + 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 {}).", dumpRes, GetLastError()); + } else { + log::error( + "ModOrganizer has crashed, CreateMiniDump failed ({}, error {}).", + r, GetLastError()); + } - if (prevUnhandledExceptionFilter && exceptionPtrs) - return prevUnhandledExceptionFilter(exceptionPtrs); + if (g_prevExceptionFilter && ptrs) + return g_prevExceptionFilter(ptrs); else return EXCEPTION_CONTINUE_SEARCH; } -void terminateHandler() noexcept +void onTerminate() noexcept { __try { @@ -155,361 +104,383 @@ void terminateHandler() noexcept } __except ( - MyUnhandledExceptionFilter(GetExceptionInformation()), EXCEPTION_EXECUTE_HANDLER + onUnhandledException(GetExceptionInformation()), EXCEPTION_EXECUTE_HANDLER ) { } - if (prevTerminateHandler) { - prevTerminateHandler(); + if (g_prevTerminateHandler) { + g_prevTerminateHandler(); } else { std::abort(); } } -void setUnhandledExceptionHandler() +void setExceptionHandlers() { - prevUnhandledExceptionFilter = SetUnhandledExceptionFilter(MyUnhandledExceptionFilter); - prevTerminateHandler = std::set_terminate(terminateHandler); + g_prevExceptionFilter = SetUnhandledExceptionFilter(onUnhandledException); + g_prevTerminateHandler = std::set_terminate(onTerminate); } -// Parses the first parseArgCount arguments of the current process command line and returns -// them in parsedArgs, the rest of the command line is returned untouched. -LPCWSTR UntouchedCommandLineArguments(int parseArgCount, std::vector<std::wstring>& parsedArgs) +// 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() { - LPCWSTR cmd = GetCommandLineW(); - LPCWSTR arg = nullptr; // to skip executable name - for (; parseArgCount >= 0 && *cmd; ++cmd) - { - if (*cmd == '"') { - int escaped = 0; - for (++cmd; *cmd && (*cmd != '"' || escaped % 2 != 0); ++cmd) - escaped = *cmd == '\\' ? escaped + 1 : 0; - } - if (*cmd == ' ') { - if (arg) - if (cmd-1 > arg && *arg == '"' && *(cmd-1) == '"') - parsedArgs.push_back(std::wstring(arg+1, cmd-1)); - else - parsedArgs.push_back(std::wstring(arg, cmd)); - arg = cmd + 1; - --parseArgCount; - } - } - return cmd; -} + const auto dllsPath = QDir::toNativeSeparators( + QCoreApplication::applicationDirPath() + "/dlls"); + QCoreApplication::setLibraryPaths( + QStringList(dllsPath) + QCoreApplication::libraryPaths()); -static int SpawnWaitProcess(LPCWSTR workingDirectory, LPCWSTR commandLine) { - PROCESS_INFORMATION pi{ 0 }; - STARTUPINFO si{ 0 }; - si.cb = sizeof(si); - std::wstring commandLineCopy = commandLine; + env::prependToPath(dllsPath); +} - if (!CreateProcessW(NULL, &commandLineCopy[0], NULL, NULL, FALSE, 0, NULL, workingDirectory, &si, &pi)) { - // A bit of a problem where to log the error message here, at least this way you can get the message - // using a either DebugView or a live debugger: - std::wostringstream ost; - ost << L"CreateProcess failed: " << commandLine << ", " << GetLastError(); - OutputDebugStringW(ost.str().c_str()); - return -1; +QString getSplashPath( + const Settings& settings, const QString& dataPath, + const MOBase::IPluginGame* game) +{ + if (!settings.useSplash()) { + return {}; } - WaitForSingleObject(pi.hProcess, INFINITE); + // 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; + } + } - DWORD exitCode = (DWORD)-1; - ::GetExitCodeProcess(pi.hProcess, &exitCode); - CloseHandle(pi.hThread); - CloseHandle(pi.hProcess); - return static_cast<int>(exitCode); -} + // 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; + } + } -static DWORD WaitForProcess() { + // try default splash from resource + QString defaultSplash = ":/MO/gui/splash"; + if (QFile::exists(defaultSplash)) { + QImage image(defaultSplash); + if (!image.isNull()) { + return defaultSplash; + } + } + return splashPath; } -static bool HaveWriteAccess(const std::wstring &path) +std::unique_ptr<QSplashScreen> createSplash( + const Settings& settings, const QString& dataPath, + const MOBase::IPluginGame* game) { - bool writable = false; + 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 {}; + } - const static SECURITY_INFORMATION requestedFileInformation = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION; + auto splash = std::make_unique<QSplashScreen>(image); + settings.geometry().centerOnMainWindowMonitor(splash.get()); - DWORD length = 0; - if (!::GetFileSecurityW(path.c_str(), requestedFileInformation, nullptr, 0UL, &length) - && (::GetLastError() == ERROR_INSUFFICIENT_BUFFER)) { - std::string tempBuffer; - tempBuffer.reserve(length); - PSECURITY_DESCRIPTOR security = (PSECURITY_DESCRIPTOR)tempBuffer.data(); - if (security - && ::GetFileSecurity(path.c_str(), requestedFileInformation, security, length, &length)) { - HANDLE token = nullptr; - const static DWORD tokenDesiredAccess = TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_DUPLICATE | STANDARD_RIGHTS_READ; - if (!::OpenThreadToken(::GetCurrentThread(), tokenDesiredAccess, TRUE, &token)) { - if (!::OpenProcessToken(::GetCurrentProcess(), tokenDesiredAccess, &token)) { - throw std::runtime_error("Unable to get any thread or process token"); - } - } + splash->show(); + splash->activateWindow(); - HANDLE impersonatedToken = nullptr; - if (::DuplicateToken(token, SecurityImpersonation, &impersonatedToken)) { - GENERIC_MAPPING mapping = { 0xFFFFFFFF }; - mapping.GenericRead = FILE_GENERIC_READ; - mapping.GenericWrite = FILE_GENERIC_WRITE; - mapping.GenericExecute = FILE_GENERIC_EXECUTE; - mapping.GenericAll = FILE_ALL_ACCESS; + return splash; +} - DWORD genericAccessRights = FILE_GENERIC_WRITE; - ::MapGenericMask(&genericAccessRights, &mapping); +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(); - PRIVILEGE_SET privileges = { 0 }; - DWORD grantedAccess = 0; - DWORD privilegesLength = sizeof(privileges); - BOOL result = 0; - if (::AccessCheck(security, impersonatedToken, genericAccessRights, &mapping, &privileges, &privilegesLength, &grantedAccess, &result)) { - writable = result != 0; - } - ::CloseHandle(impersonatedToken); + 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); - ::CloseHandle(token); + 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 writable; -} + return {}; +} -QString determineProfile(QStringList &arguments, const Settings &settings) +std::optional<Instance> selectInstance() { - auto selectedProfileName = settings.game().selectedProfileName(); + auto& m = InstanceManager::singleton(); + + NexusInterface ni(nullptr); + PluginContainer pc(nullptr); + pc.loadPlugins(); - { // see if there is a profile on the command line - int profileIndex = arguments.indexOf("-p", 1); - if ((profileIndex != -1) && (profileIndex < arguments.size() - 1)) { - log::debug("profile overwritten on command line"); - selectedProfileName = arguments.at(profileIndex + 1); + if (m.instancePaths().empty() && !m.portableInstanceExists()) { + // no instances configured + CreateInstanceDialog dlg(pc, nullptr); + if (dlg.exec() != QDialog::Accepted) { + return {}; } - arguments.removeAt(profileIndex); - arguments.removeAt(profileIndex); - } - if (!selectedProfileName) { - log::debug("no configured profile"); - selectedProfileName = "Default"; + return m.currentInstance(); } - return *selectedProfileName; -} -MOBase::IPluginGame *selectGame( - Settings &settings, QDir const &gamePath, MOBase::IPluginGame *game) -{ - settings.game().setName(game->gameName()); + InstanceManagerDialog dlg(pc); + dlg.setRestartOnSelect(false); - QString gameDir = gamePath.absolutePath(); - game->setGamePath(gameDir); + dlg.show(); + dlg.activateWindow(); + dlg.raise(); - settings.game().setDirectory(gameDir); + if (dlg.exec() != QDialog::Accepted) { + return {}; + } - return game; + return m.currentInstance(); } +enum class SetupInstanceResults +{ + Ok, + TryAgain, + SelectAnother, + Exit +}; + -MOBase::IPluginGame *determineCurrentGame( - QString const &moPath, Settings &settings, PluginContainer const &plugins) +void criticalOnTop(const QString& message) { - //Determine what game we are running where. Be very paranoid in case the - //user has done something odd. + QMessageBox mb(QMessageBox::Critical, QObject::tr("Mod Organizer"), message); - //If the game name has been set up, try to use that. - const auto gameName = settings.game().name(); - const bool gameConfigured = (gameName.has_value() && *gameName != ""); + mb.show(); + mb.activateWindow(); + mb.raise(); + mb.exec(); +} - if (gameConfigured) { - MOBase::IPluginGame *game = plugins.managedGame(*gameName); - if (game == nullptr) { - reportError( - QObject::tr("Plugin to handle %1 no longer installed. An antivirus might have deleted files.") - .arg(*gameName)); - return nullptr; - } +SetupInstanceResults setupInstance(Instance& instance, PluginContainer& pc) +{ + const auto setupResult = instance.setup(pc); - auto gamePath = settings.game().directory(); - if (!gamePath || *gamePath == "") { - gamePath = game->gameDirectory().absolutePath(); + switch (setupResult) + { + case Instance::SetupResults::Ok: + { + return SetupInstanceResults::Ok; } - QDir gameDir(*gamePath); - QFileInfo directoryInfo(gameDir.path()); + case Instance::SetupResults::BadIni: + { + criticalOnTop( + QObject::tr("Cannot open instance '%1', failed to read INI file %2.") + .arg(instance.name()).arg(instance.iniPath())); - if (directoryInfo.isSymLink()) { - reportError(QObject::tr("The configured path to the game directory (%1) appears to be a symbolic (or other) link. " - "This setup is incompatible with MO2's VFS and will not run correctly.").arg(*gamePath)); + return SetupInstanceResults::SelectAnother; } - if (game->looksValid(gameDir)) { - return selectGame(settings, gameDir, game); - } - } + 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())); - //If we've made it this far and the instance is already configured for a game, something has gone wrong. - //Tell the user about it. - if (gameConfigured) { - const auto gamePath = settings.game().directory(); + CreateInstanceDialog dlg(pc, nullptr); + dlg.setSinglePage<cid::GamePage>(instance.name()); - reportError( - QObject::tr("Could not use configuration settings for game \"%1\", path \"%2\".") - .arg(*gameName).arg(gamePath ? *gamePath : "")); - } + dlg.show(); + dlg.activateWindow(); + dlg.raise(); - SelectionDialog selection(gameConfigured ? - QObject::tr("Please select the installation of %1 to manage").arg(*gameName) : - QObject::tr("Please select the game to manage"), nullptr, QSize(32, 32)); + if (dlg.exec() != QDialog::Accepted) { + return SetupInstanceResults::Exit; + } - for (IPluginGame *game : plugins.plugins<IPluginGame>()) { - //If a game is already configured, skip any plugins that are not for that game - if (gameConfigured && gameName->compare(game->gameName(), Qt::CaseInsensitive) != 0) - continue; + instance.setGame( + dlg.creationInfo().game->gameName(), + dlg.creationInfo().gameLocation); - //Only add games that are installed - if (game->isInstalled()) { - QString path = game->gameDirectory().absolutePath(); - selection.addChoice(game->gameIcon(), game->gameName(), path, QVariant::fromValue(game)); + return SetupInstanceResults::TryAgain; } - } - selection.addChoice(QString("Browse..."), QString(), QVariant::fromValue(static_cast<IPluginGame *>(nullptr))); + 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())); - while (selection.exec() != QDialog::Rejected) { - IPluginGame * game = selection.getChoiceData().value<IPluginGame *>(); - QString gamePath = selection.getChoiceDescription(); - QFileInfo directoryInfo(gamePath); - if (directoryInfo.isSymLink()) { - reportError(QObject::tr("The configured path to the game directory (%1) appears to be a symbolic (or other) link. " - "This setup is incompatible with MO2's VFS and will not run correctly.").arg(gamePath)); - } - if (game != nullptr) { - return selectGame(settings, game->gameDirectory(), game); + return SetupInstanceResults::SelectAnother; } - gamePath = QFileDialog::getExistingDirectory(nullptr, gameConfigured ? - QObject::tr("Please select the installation of %1 to manage").arg(*gameName) : - QObject::tr("Please select the game to manage"), - QString(), QFileDialog::ShowDirsOnly); + 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())); - if (!gamePath.isEmpty()) { - QDir gameDir(gamePath); - QFileInfo directoryInfo(gamePath); - if (directoryInfo.isSymLink()) { - reportError(QObject::tr("The configured path to the game directory (%1) appears to be a symbolic (or other) link. " - "This setup is incompatible with MO2's VFS and will not run correctly.").arg(gamePath)); - } - QList<IPluginGame *> possibleGames; - for (IPluginGame * const game : plugins.plugins<IPluginGame>()) { - //If a game is already configured, skip any plugins that are not for that game - if (gameConfigured && gameName->compare(game->gameName(), Qt::CaseInsensitive) != 0) - continue; + CreateInstanceDialog dlg(pc, nullptr); + dlg.setSinglePage<cid::GamePage>(instance.name()); + + dlg.show(); + dlg.activateWindow(); + dlg.raise(); - //Only try plugins that look valid for this directory - if (game->looksValid(gameDir)) { - possibleGames.append(game); - } + if (dlg.exec() != QDialog::Accepted) { + return SetupInstanceResults::Exit; } - if (possibleGames.count() > 1) { - SelectionDialog browseSelection(gameConfigured ? - QObject::tr("Please select the installation of %1 to manage").arg(*gameName) : - QObject::tr("Please select the game to manage"), - nullptr, QSize(32, 32)); + instance.setGame( + dlg.creationInfo().game->gameName(), + dlg.creationInfo().gameLocation); - for (IPluginGame *game : possibleGames) { - browseSelection.addChoice(game->gameIcon(), game->gameName(), gamePath, QVariant::fromValue(game)); - } + return SetupInstanceResults::TryAgain; + } - if (browseSelection.exec() == QDialog::Accepted) { - return selectGame(settings, gameDir, browseSelection.getChoiceData().value<IPluginGame *>()); - } else { - reportError(gameConfigured ? - QObject::tr("Canceled finding %1 in \"%2\".").arg(*gameName).arg(gamePath) : - QObject::tr("Canceled finding game in \"%1\".").arg(gamePath)); - } - } else if(possibleGames.count() == 1) { - return selectGame(settings, gameDir, possibleGames[0]); - } else { - if (gameConfigured) { - reportError( - QObject::tr("%1 not identified in \"%2\". The directory is required to contain the game binary.") - .arg(*gameName).arg(gamePath)); - } else { - QString supportedGames; + case Instance::SetupResults::MissingVariant: + { + CreateInstanceDialog dlg(pc, nullptr); - for (IPluginGame * const game : plugins.plugins<IPluginGame>()) { - supportedGames += "<li>" + game->gameName() + "</li>"; - } + dlg.getPage<cid::GamePage>()->select( + instance.gamePlugin(), instance.gameDirectory()); - QString text = QObject::tr( - "No game identified in \"%1\". The directory is required to " - "contain the game binary.<br><br>" - "<b>These are the games supported by Mod Organizer:</b>" - "<ul>%2</ul>") - .arg(gamePath) - .arg(supportedGames); + dlg.setSinglePage<cid::VariantsPage>(instance.name()); - reportError(text); - } + dlg.show(); + dlg.activateWindow(); + dlg.raise(); + + if (dlg.exec() != QDialog::Accepted) { + return SetupInstanceResults::Exit; } - } - } - return nullptr; -} + instance.setVariant(dlg.creationInfo().gameVariant); -static QString getVersionDisplayString() -{ - return createVersionInfo().displayString(3); -} + return SetupInstanceResults::TryAgain; + } + default: + { + return SetupInstanceResults::Exit; + } + } +} -int runApplication(MOApplication &application, SingleInstance &instance, - const QString &splashPath) +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: {}", - getVersionDisplayString(), GITID, QCoreApplication::applicationDirPath(), - MOShared::getUsvfsVersionString()); + createVersionInfo().displayString(3), GITID, + QCoreApplication::applicationDirPath(), MOShared::getUsvfsVersionString()); - if (!QSslSocket::supportsSsl()) { - log::warn("no ssl support"); - } - - const QString dataPath = application.property("dataPath").toString(); log::info("data path: {}", dataPath); if (InstanceManager::isPortablePath(dataPath)) { log::debug("this is a portable instance"); } - if (!bootstrap()) { - reportError("failed to set up data paths"); - InstanceManager::instance().clearCurrentInstance(); - return 1; + log::info("working directory: {}", QDir::currentPath()); + + if (!instance.secondary()) { + purgeOldFiles(); } QWindowsWindowFunctions::setWindowActivationBehavior( QWindowsWindowFunctions::AlwaysActivateWindow); - QStringList arguments = application.arguments(); - - try { - log::info("working directory: {}", QDir::currentPath()); + try + { + Settings settings( + dataPath + "/" + QString::fromStdWString(AppConfig::iniFileName()), + true); - Settings settings(dataPath + "/" + QString::fromStdWString(AppConfig::iniFileName())); 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()); @@ -524,172 +495,70 @@ int runApplication(MOApplication &application, SingleInstance &instance, checkIncompatibleModule(m); }); - log::debug("initializing core"); + // 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::instance().clearCurrentInstance(); + InstanceManager::singleton().clearCurrentInstance(); return 1; } - { - // log if there are any dmp files - const auto hasCrashDumps = - !QDir(QString::fromStdWString(organizer.crashDumpsPath())) - .entryList({"*.dmp"}, QDir::Files) - .empty(); - - if (hasCrashDumps) { - log::debug( - "there are crash dumps in '{}'", - QString::fromStdWString(organizer.crashDumpsPath())); - } - } - log::debug("initializing plugins"); pluginContainer = std::make_unique<PluginContainer>(&organizer); pluginContainer->loadPlugins(); - MOBase::IPluginGame *game = determineCurrentGame( - application.applicationDirPath(), settings, *pluginContainer); - - if (game == nullptr) { - InstanceManager &instance = InstanceManager::instance(); - QString instanceName = instance.currentInstance(); + for (;;) + { + const auto setupResult = setupInstance(currentInstance, *pluginContainer); - if (instanceName.compare("Portable", Qt::CaseInsensitive) != 0) { - instance.clearCurrentInstance(); + if (setupResult == SetupInstanceResults::Ok) { + break; + } else if (setupResult == SetupInstanceResults::TryAgain) { + continue; + } else if (setupResult == SetupInstanceResults::SelectAnother) { + InstanceManager::singleton().clearCurrentInstance(); return RestartExitCode; + } else { + return 1; } - - return 1; } - checkPathsForSanity(*game, settings); - - bool useSplash = settings.useSplash(); + checkPathsForSanity(*currentInstance.gamePlugin(), settings); - if (useSplash) { - if (splashPath.startsWith(':')) { - // currently using MO splash, see if the plugin contains one - QString pluginSplash - = QString(":/%1/splash").arg(game->gameShortName()); - QImage image(pluginSplash); - if (!image.isNull()) { - image.save(dataPath + "/splash.png"); - } - } - } - - organizer.setManagedGame(game); + organizer.setManagedGame(currentInstance.gamePlugin()); organizer.createDefaultProfile(); - QString edition; - - if (auto v=settings.game().edition()) { - edition = *v; - } else { - QStringList editions = game->gameVariants(); - if (editions.size() > 1) { - SelectionDialog selection( - QObject::tr("Please select the game edition you have (MO can't " - "start the game correctly if this is set " - "incorrectly!)"), - nullptr); - selection.setWindowFlag(Qt::WindowStaysOnTopHint, true); - int index = 0; - for (const QString &edition : editions) { - selection.addChoice(edition, "", index++); - } - if (selection.exec() == QDialog::Rejected) { - return 1; - } else { - edition = selection.getChoiceString(); - settings.game().setEdition(edition); - } - } - } - - game->setGameVariant(edition); - log::info( - "using game plugin '{}' ('{}', steam id '{}') at {}", - game->gameName(), game->gameShortName(), game->steamAPPId(), - game->gameDirectory().absolutePath()); + "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(); - QString selectedProfileName = determineProfile(arguments, settings); - organizer.setCurrentProfile(selectedProfileName); - - // if we have a command line parameter, it is either a nxm link or - // a binary to start - if (arguments.size() > 1) { - if (MOShortcut shortcut{ arguments.at(1) }) { - if (shortcut.hasExecutable()) { - try { - organizer.processRunner() - .setFromShortcut(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 (OrganizerCore::isNxmLink(arguments.at(1))) { - log::debug("starting download from command line: {}", arguments.at(1)); - organizer.externalMessage(arguments.at(1)); - } - else { - QString exeName = arguments.at(1); - log::debug("starting {} from command line", exeName); - - arguments.removeFirst(); // remove application name (ModOrganizer.exe) - arguments.removeFirst(); // remove binary name - - try - { - // pass the remaining parameters to the binary - organizer.processRunner() - .setFromFileOrExecutable(exeName, arguments) - .setWaitForCompletion() - .run(); - return 0; - } - catch (const std::exception &e) - { - reportError( - QObject::tr("failed to start application: %1").arg(e.what())); - return 1; - } - } - } + organizer.setCurrentProfile(currentInstance.profileName()); - QPixmap pixmap; - - QSplashScreen splash; - - if (useSplash) { - pixmap = QPixmap(splashPath); - splash.setPixmap(pixmap); - - settings.geometry().centerOnMainWindowMonitor(&splash); - splash.show(); - splash.activateWindow(); + if (auto r=handleCommandLine(cl, organizer)) { + return *r; } + auto splash = createSplash(settings, dataPath, currentInstance.gamePlugin()); + QString apiKey; - if (settings.nexus().apiKey(apiKey)) { - NexusInterface::instance(pluginContainer.get())->getAccessManager()->apiCheck(apiKey); + if (GlobalSettings::nexusApiKey(apiKey)) { + ni.getAccessManager()->apiCheck(apiKey); } log::debug("initializing tutorials"); @@ -707,8 +576,7 @@ int runApplication(MOApplication &application, SingleInstance &instance, // set up main window and its data structures MainWindow mainWindow(settings, organizer, *pluginContainer); - NexusInterface::instance(pluginContainer.get()) - ->getAccessManager()->setTopLevelWidget(&mainWindow); + ni.getAccessManager()->setTopLevelWidget(&mainWindow); QObject::connect(&mainWindow, SIGNAL(styleChanged(QString)), &application, SLOT(setStyleFile(QString))); @@ -719,10 +587,10 @@ int runApplication(MOApplication &application, SingleInstance &instance, mainWindow.show(); mainWindow.activateWindow(); - if (useSplash) { + if (splash) { // don't pass mainwindow as it just waits half a second for it // instead of proceding - splash.finish(nullptr); + splash->finish(nullptr); } tt.stop(); @@ -730,237 +598,148 @@ int runApplication(MOApplication &application, SingleInstance &instance, res = application.exec(); mainWindow.close(); - NexusInterface::instance(pluginContainer.get()) - ->getAccessManager()->setTopLevelWidget(nullptr); + ni.getAccessManager()->setTopLevelWidget(nullptr); } settings.geometry().resetIfNeeded(); return res; - } catch (const std::exception &e) { + } + catch (const std::exception &e) + { reportError(e.what()); } return 1; } -int doCoreDump(env::CoreDumpTypes type) +int forwardToPrimary(SingleInstance& instance, const cl::CommandLine& cl) { - env::Console c; - - // dump - const auto b = env::coredumpOther(type); - if (!b) { - std::wcerr << L"\n>>>> a minidump file was not written\n\n"; + if (cl.shortcut().isValid()) { + instance.sendMessage(cl.shortcut().toString()); + } else if (cl.nxmLink()) { + instance.sendMessage(*cl.nxmLink()); + } else { + QMessageBox::information( + nullptr, QObject::tr("Mod Organizer"), + QObject::tr("An instance of Mod Organizer is already running")); } - std::wcerr << L"Press enter to continue..."; - std::wcin.get(); - - return (b ? 0 : 1); + return 0; } -log::Levels convertQtLevel(QtMsgType t) +void resetForRestart(cl::CommandLine& cl) { - switch (t) - { - case QtDebugMsg: - return log::Debug; - - case QtWarningMsg: - return log::Warning; + LogModel::instance().clear(); + ResetExitFlag(); - case QtCriticalMsg: // fall-through - case QtFatalMsg: - return log::Error; + // make sure the log file isn't locked in case MO was restarted and + // the previous instance gets deleted + log::getDefault().setFile({}); - case QtInfoMsg: // fall-through - default: - return log::Info; - } + // don't reprocess command line + cl.clear(); } -void qtLogCallback( - QtMsgType type, const QMessageLogContext& context, const QString& message) +int doOneRun( + cl::CommandLine& cl, MOApplication& application, SingleInstance& instance) { - std::string_view file = ""; + TimeThis tt("doOneRun() to runApplication()"); - if (type != QtDebugMsg) { - if (context.file) { - file = context.file; + // resets things when MO is "restarted" + resetForRestart(cl); - const auto lastSep = file.find_last_of("/\\"); - if (lastSep != std::string_view::npos) { - file = {context.file + lastSep + 1}; - } - } - } + auto& m = InstanceManager::singleton(); + auto currentInstance = m.currentInstance(); - if (file.empty()) { - log::log( - convertQtLevel(type), "{}", - message.toStdString()); - } else { - log::log( - convertQtLevel(type), "[{}:{}] {}", - file, context.line, message.toStdString()); + if (!currentInstance) + { + currentInstance = selectInstance(); + if (!currentInstance) { + return 1; + } } -} - -void initLogging() -{ - LogModel::create(); + else + { + if (!currentInstance->directory().exists()) { + // the previously used instance doesn't exist anymore - log::LoggerConfiguration conf; - conf.maxLevel = MOBase::log::Debug; - conf.pattern = "%^[%Y-%m-%d %H:%M:%S.%e %L] %v%$"; - conf.utc = true; + 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())); + } - log::createDefault(conf); + currentInstance = selectInstance(); + if (!currentInstance) { + return 1; + } + } + } - log::getDefault().setCallback( - [](log::Entry e){ LogModel::instance().add(e); }); + const QString dataPath = currentInstance->directory().path(); + application.setProperty("dataPath", dataPath); - qInstallMessageHandler(qtLogCallback); -} + setExceptionHandlers(); -void preloadDll(const QString& filename) -{ - if (GetModuleHandleW(filename.toStdWString().c_str())) - return; + if (!setLogDirectory(dataPath)) { + reportError("Failed to create log folder"); + InstanceManager::singleton().clearCurrentInstance(); + return 1; + } - const auto appPath = QDir::toNativeSeparators(QDir::currentPath()); - const auto dllPath = appPath + "\\dlls\\" + filename; + log::debug("command line: '{}'", QString::fromWCharArray(GetCommandLineW())); - if (!QFile::exists(dllPath)) { - log::warn("{} not found", dllPath); - return; - } + tt.stop(); - if (!LoadLibraryW(dllPath.toStdWString().c_str())) { - const auto e = GetLastError(); - log::warn("failed to load {}: {}", dllPath, formatSystemMessage(e)); - } + return runApplication(application, cl, instance, dataPath, *currentInstance); } - int main(int argc, char *argv[]) { - TimeThis tt("main to runApplication()"); - - // handle --crashdump first - for (int i=1; i<argc; ++i) { - if (std::strcmp(argv[i], "--crashdump") == 0) { - return doCoreDump(env::CoreDumpTypes::Mini); - } else if (std::strcmp(argv[i], "--crashdump-data") == 0) { - return doCoreDump(env::CoreDumpTypes::Data); - } else if (std::strcmp(argv[i], "--crashdump-full") == 0) { - return doCoreDump(env::CoreDumpTypes::Full); - } - } - - preloadDll("libcrypto-1_1-x64.dll"); - preloadDll("libssl-1_1-x64.dll"); - - initLogging(); + cl::CommandLine cl; - //Make sure the configured temp folder exists - QDir tempDir = QDir::temp(); - if (!tempDir.exists()) - tempDir.root().mkpath(tempDir.canonicalPath()); - - //Should allow for better scaling of ui with higher resolution displays - QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); - - if (argc >= 4) { - std::vector<std::wstring> arg; - auto args = UntouchedCommandLineArguments(2, arg); - if (arg[0] == L"launch") - return SpawnWaitProcess(arg[1].c_str(), args); + if (auto r=cl.run(GetCommandLineW())) { + return *r; } - MOApplication application(argc, argv); - QStringList arguments = application.arguments(); + TimeThis tt("main() to doOneRun()"); env::prependToPath(QDir::toNativeSeparators( QCoreApplication::applicationDirPath() + "/dlls")); - SetThisThreadName("main"); + initLogging(); + auto application = MOApplication::create(argc, argv); + addDllsToPath(); - bool forcePrimary = false; - if (arguments.contains("update")) { - arguments.removeAll("update"); - forcePrimary = true; + SingleInstance instance(cl.multiple()); + if (instance.ephemeral()) { + return forwardToPrimary(instance, cl); } - MOShortcut moshortcut{ arguments.size() > 1 ? arguments.at(1) : "" }; + tt.stop(); - SingleInstance instance(forcePrimary); - if (!instance.primaryInstance()) { - if (moshortcut || - arguments.size() > 1 && OrganizerCore::isNxmLink(arguments.at(1))) - { - log::debug("not primary instance, sending shortcut/download message"); - instance.sendMessage(arguments.at(1)); - return 0; - } else if (arguments.size() == 1) { - QMessageBox::information( - nullptr, QObject::tr("Mod Organizer"), - QObject::tr("An instance of Mod Organizer is already running")); - return 0; - } - } // we continue for the primary instance OR if MO was called with parameters - - do { - 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({}); - - QString dataPath; - - try { - InstanceManager& instanceManager = InstanceManager::instance(); - if (moshortcut && moshortcut.hasInstance()) - instanceManager.overrideInstance(moshortcut.instance()); - dataPath = instanceManager.determineDataPath(); - } catch (const std::exception &e) { - if (strcmp(e.what(),"Canceled")) - QMessageBox::critical(nullptr, QObject::tr("Failed to set up instance"), e.what()); - return 1; - } - application.setProperty("dataPath", dataPath); + if (cl.instance()) + InstanceManager::singleton().overrideInstance(*cl.instance()); - // initialize dump collection only after "dataPath" since the crashes are stored under it - setUnhandledExceptionHandler(); - - const auto logFile = - qApp->property("dataPath").toString() + "/logs/mo_interface.log"; - - if (!createAndMakeWritable(AppConfig::logPath())) { - reportError("Failed to create log folder"); - InstanceManager::instance().clearCurrentInstance(); - return 1; - } - - log::getDefault().setFile(MOBase::log::File::single(logFile.toStdWString())); - - log::debug("command line: '{}'", QString::fromWCharArray(GetCommandLineW())); - - QString splash = dataPath + "/splash.png"; - if (!QFile::exists(dataPath + "/splash.png")) { - splash = ":/MO/gui/splash"; - } + if (cl.profile()) { + InstanceManager::singleton().overrideProfile(*cl.profile()); + } - tt.stop(); + // makes plugin data path available to plugins, see + // IOrganizer::getPluginDataPath() + MOBase::details::setPluginDataPath(OrganizerCore::pluginDataPath()); - const int result = runApplication(application, instance, splash); - if (result != RestartExitCode) { - return result; + for (;;) + { + const auto r = doOneRun(cl, application, instance); + if (r == RestartExitCode) { + continue; } - argc = 1; - moshortcut = MOShortcut(""); - } while (true); + return r; + } } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 522657fb..25a1314e 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -76,6 +76,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "statusbar.h" #include "filterlist.h" #include "datatab.h" +#include "instancemanagerdialog.h" #include <utility.h> #include <dataarchives.h> #include <bsainvalidation.h> @@ -168,7 +169,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #ifndef Q_MOC_RUN #include <boost/thread.hpp> #include <boost/algorithm/string.hpp> -#include <boost/bind.hpp> +#include <boost/bind/bind.hpp> #include <boost/assign.hpp> #include <boost/range/adaptor/reversed.hpp> #endif @@ -291,7 +292,7 @@ MainWindow::MainWindow(Settings &settings ui->statusBar->setup(ui, settings); { - auto* ni = NexusInterface::instance(&m_PluginContainer); + auto& ni = NexusInterface::instance(); // there are two ways to get here: // 1) the user just started MO, and @@ -311,8 +312,8 @@ MainWindow::MainWindow(Settings &settings // // in the rare case where the user restarts MO through the settings, this // will correctly pick up the previous values - updateWindowTitle(ni->getAPIUserAccount()); - ui->statusBar->setAPI(ni->getAPIStats(), ni->getAPIUserAccount()); + updateWindowTitle(ni.getAPIUserAccount()); + ui->statusBar->setAPI(ni.getAPIStats(), ni.getAPIUserAccount()); } m_Filters.reset(new FilterList(ui, &m_OrganizerCore, m_CategoryFactory)); @@ -439,24 +440,24 @@ MainWindow::MainWindow(Settings &settings connect(m_OrganizerCore.updater(), SIGNAL(updateAvailable()), this, SLOT(updateAvailable())); connect(m_OrganizerCore.updater(), SIGNAL(motdAvailable(QString)), this, SLOT(motdReceived(QString))); - connect(NexusInterface::instance(&pluginContainer), SIGNAL(requestNXMDownload(QString)), &m_OrganizerCore, SLOT(downloadRequestedNXM(QString))); - connect(NexusInterface::instance(&pluginContainer), SIGNAL(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)), this, SLOT(nxmDownloadURLs(QString,int,int,QVariant,QVariant,int))); - connect(NexusInterface::instance(&pluginContainer), SIGNAL(needLogin()), &m_OrganizerCore, SLOT(nexusApi())); + connect(&NexusInterface::instance(), SIGNAL(requestNXMDownload(QString)), &m_OrganizerCore, SLOT(downloadRequestedNXM(QString))); + connect(&NexusInterface::instance(), SIGNAL(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)), this, SLOT(nxmDownloadURLs(QString,int,int,QVariant,QVariant,int))); + connect(&NexusInterface::instance(), SIGNAL(needLogin()), &m_OrganizerCore, SLOT(nexusApi())); connect( - NexusInterface::instance(&pluginContainer)->getAccessManager(), + NexusInterface::instance().getAccessManager(), SIGNAL(credentialsReceived(const APIUserAccount&)), this, SLOT(updateWindowTitle(const APIUserAccount&))); connect( - NexusInterface::instance(&pluginContainer)->getAccessManager(), + NexusInterface::instance().getAccessManager(), SIGNAL(credentialsReceived(const APIUserAccount&)), - NexusInterface::instance(&m_PluginContainer), + &NexusInterface::instance(), SLOT(setUserAccount(const APIUserAccount&))); connect( - NexusInterface::instance(&pluginContainer), + &NexusInterface::instance(), SIGNAL(requestsChanged(const APIStats&, const APIUserAccount&)), this, SLOT(onRequestsChanged(const APIStats&, const APIUserAccount&))); @@ -836,7 +837,7 @@ void MainWindow::setupToolbar() log::warn("no separator found on the toolbar, icons won't be right-aligned"); } - if (!InstanceManager::instance().allowedToChangeInstance()) { + if (!InstanceManager::singleton().allowedToChangeInstance()) { ui->actionChange_Game->setVisible(false); } } @@ -1334,6 +1335,30 @@ void MainWindow::hookUpWindowTutorials() } } +bool MainWindow::shouldStartTutorial() const +{ + if (GlobalSettings::hideTutorialQuestion()) { + return false; + } + + QMessageBox dlg( + QMessageBox::Question, tr("Show tutorial?"), + tr("You are starting Mod Organizer for the first time. " + "Do you want to show a tutorial of its basic features? If you choose " + "no you can always start the tutorial from the \"Help\"-menu."), + QMessageBox::Yes | QMessageBox::No); + + dlg.setCheckBox(new QCheckBox(tr("Never ask to show tutorials"))); + + const auto r = dlg.exec(); + + if (dlg.checkBox()->isChecked()) { + GlobalSettings::setHideTutorialQuestion(true); + } + + return (r == QMessageBox::Yes); +} + void MainWindow::showEvent(QShowEvent *event) { QMainWindow::showEvent(event); @@ -1360,11 +1385,7 @@ void MainWindow::showEvent(QShowEvent *event) if (m_OrganizerCore.settings().firstStart()) { QString firstStepsTutorial = ToQString(AppConfig::firstStepsTutorial()); if (TutorialManager::instance().hasTutorial(firstStepsTutorial)) { - if (QMessageBox::question(this, tr("Show tutorial?"), - tr("You are starting Mod Organizer for the first time. " - "Do you want to show a tutorial of its basic features? If you choose " - "no you can always start the tutorial from the \"Help\"-menu."), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + if (shouldStartTutorial()) { TutorialManager::instance().activateTutorial("MainWindow", firstStepsTutorial); } } else { @@ -1891,9 +1912,9 @@ QDir MainWindow::currentSavesDir() const wchar_t path[MAX_PATH]; if (::GetPrivateProfileStringW( - L"General", L"SLocalSavePath", L"", - path, MAX_PATH, - iniPath.toStdWString().c_str() + L"General", L"SLocalSavePath", L"", + path, MAX_PATH, + iniPath.toStdWString().c_str() )) { savesDir.setPath(m_OrganizerCore.managedGame()->documentsDirectory().absoluteFilePath(QString::fromWCharArray(path))); } @@ -1941,8 +1962,8 @@ void MainWindow::refreshSaveList() it.next(); files.append(it.fileInfo()); } - std::sort(files.begin(), files.end(), [](auto const& lhs, auto const& rhs) { - return lhs.fileTime(QFileDevice::FileModificationTime) < rhs.fileTime(QFileDevice::FileModificationTime); + std::sort(files.begin(), files.end(), [](auto const& lhs, auto const& rhs) { + return lhs.fileTime(QFileDevice::FileModificationTime) < rhs.fileTime(QFileDevice::FileModificationTime); }); for (const QFileInfo &file : files) { @@ -3268,7 +3289,7 @@ void MainWindow::visitOnNexus_clicked() int modID = info->nexusId(); gameName = info->gameName(); if (modID > 0) { - linkClicked(NexusInterface::instance(&m_PluginContainer)->getModURL(modID, gameName)); + linkClicked(NexusInterface::instance().getModURL(modID, gameName)); } else { log::error("mod '{}' has no nexus id", info->name()); } @@ -3278,7 +3299,7 @@ void MainWindow::visitOnNexus_clicked() int modID = m_OrganizerCore.modList()->data(m_OrganizerCore.modList()->index(m_ContextRow, 0), Qt::UserRole).toInt(); QString gameName = m_OrganizerCore.modList()->data(m_OrganizerCore.modList()->index(m_ContextRow, 0), Qt::UserRole + 4).toString(); if (modID > 0) { - linkClicked(NexusInterface::instance(&m_PluginContainer)->getModURL(modID, gameName)); + linkClicked(NexusInterface::instance().getModURL(modID, gameName)); } else { MessageDialog::showMessage(tr("Nexus ID for this mod is unknown"), this); } @@ -3335,7 +3356,7 @@ void MainWindow::visitNexusOrWebPage(const QModelIndex& idx) const auto url = info->parseCustomURL(); if (modID > 0) { - linkClicked(NexusInterface::instance(&m_PluginContainer)->getModURL(modID, gameName)); + linkClicked(NexusInterface::instance().getModURL(modID, gameName)); } else if (url.isValid()) { linkClicked(url.toString()); } else { @@ -4200,15 +4221,15 @@ void MainWindow::saveArchiveList() void MainWindow::checkModsForUpdates() { bool checkingModsForUpdate = false; - if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { + if (NexusInterface::instance().getAccessManager()->validated()) { checkingModsForUpdate = ModInfo::checkAllForUpdate(&m_PluginContainer, this); - NexusInterface::instance(&m_PluginContainer)->requestEndorsementInfo(this, QVariant(), QString()); - NexusInterface::instance(&m_PluginContainer)->requestTrackingInfo(this, QVariant(), QString()); + NexusInterface::instance().requestEndorsementInfo(this, QVariant(), QString()); + NexusInterface::instance().requestTrackingInfo(this, QVariant(), QString()); } else { QString apiKey; - if (m_OrganizerCore.settings().nexus().apiKey(apiKey)) { + if (GlobalSettings::nexusApiKey(apiKey)) { m_OrganizerCore.doAfterLogin([this] () { this->checkModsForUpdates(); }); - NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); + NexusInterface::instance().getAccessManager()->apiCheck(apiKey); } else { log::warn("{}", tr("You are not currently authenticated with Nexus. Please do so under Settings -> Nexus.")); } @@ -4583,7 +4604,7 @@ void MainWindow::exportModListCSV() if (nexus_ID->isChecked()) builder.setRowField("#Nexus_ID", info->nexusId()); if (mod_Nexus_URL->isChecked()) - builder.setRowField("#Mod_Nexus_URL",(info->nexusId()>0)? NexusInterface::instance(&m_PluginContainer)->getModURL(info->nexusId(), info->gameName()) : ""); + builder.setRowField("#Mod_Nexus_URL",(info->nexusId()>0)? NexusInterface::instance().getModURL(info->nexusId(), info->gameName()) : ""); if (mod_Version->isChecked()) builder.setRowField("#Mod_Version", info->version().canonicalString()); if (install_Date->isChecked()) @@ -5137,7 +5158,7 @@ void MainWindow::on_actionSettings_triggered() } if (settings.paths().cache() != oldCacheDirectory) { - NexusInterface::instance(&m_PluginContainer)->setCacheDirectory( + NexusInterface::instance().setCacheDirectory( settings.paths().cache()); } @@ -5171,7 +5192,7 @@ void MainWindow::on_actionNexus_triggered() QString gameName = game->gameShortName(); if (game->gameNexusName().isEmpty() && game->primarySources().count()) gameName = game->primarySources()[0]; - QDesktopServices::openUrl(QUrl(NexusInterface::instance(&m_PluginContainer)->getGameURL(gameName))); + QDesktopServices::openUrl(QUrl(NexusInterface::instance().getGameURL(gameName))); } @@ -5330,9 +5351,9 @@ void MainWindow::actionEndorseMO() if (QMessageBox::question(this, tr("Endorse Mod Organizer"), tr("Do you want to endorse Mod Organizer on %1 now?").arg( - NexusInterface::instance(&m_PluginContainer)->getGameURL(game->gameShortName())), + NexusInterface::instance().getGameURL(game->gameShortName())), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - NexusInterface::instance(&m_PluginContainer)->requestToggleEndorsement( + NexusInterface::instance().requestToggleEndorsement( game->gameShortName(), game->nexusModOrganizerID(), m_OrganizerCore.getVersion().canonicalString(), true, this, QVariant(), QString()); } } @@ -5346,9 +5367,9 @@ void MainWindow::actionWontEndorseMO() if (QMessageBox::question(this, tr("Abstain from Endorsing Mod Organizer"), tr("Are you sure you want to abstain from endorsing Mod Organizer 2?\n" "You will have to visit the mod page on the %1 Nexus site to change your mind.").arg( - NexusInterface::instance(&m_PluginContainer)->getGameURL(game->gameShortName())), + NexusInterface::instance().getGameURL(game->gameShortName())), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - NexusInterface::instance(&m_PluginContainer)->requestToggleEndorsement( + NexusInterface::instance().requestToggleEndorsement( game->gameShortName(), game->nexusModOrganizerID(), m_OrganizerCore.getVersion().canonicalString(), false, this, QVariant(), QString()); } } @@ -5410,13 +5431,13 @@ void MainWindow::updateDownloadView() void MainWindow::modUpdateCheck(std::multimap<QString, int> IDs) { - if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { - ModInfo::manualUpdateCheck(&m_PluginContainer, this, IDs); + if (NexusInterface::instance().getAccessManager()->validated()) { + ModInfo::manualUpdateCheck(this, IDs); } else { QString apiKey; - if (m_OrganizerCore.settings().nexus().apiKey(apiKey)) { + if (GlobalSettings::nexusApiKey(apiKey)) { m_OrganizerCore.doAfterLogin([=]() { this->modUpdateCheck(IDs); }); - NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); + NexusInterface::instance().getAccessManager()->apiCheck(apiKey); } else log::warn("{}", tr("You are not currently authenticated with Nexus. Please do so under Settings -> Nexus.")); } @@ -5549,7 +5570,7 @@ void MainWindow::nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVa void MainWindow::finishUpdateInfo() { QFutureWatcher<std::pair<QString, std::set<QSharedPointer<ModInfo>>>> *watcher = static_cast<QFutureWatcher<std::pair<QString, std::set<QSharedPointer<ModInfo>>>> *>(sender()); - + QString game = watcher->result().first; auto finalMods = watcher->result().second; @@ -5568,7 +5589,7 @@ void MainWindow::finishUpdateInfo() log::warn("{}", tr("All of your mods have been checked recently. We restrict update checks to help preserve your available API requests.")); for (auto game : organizedGames) - NexusInterface::instance(&m_PluginContainer)->requestUpdates(game.second, this, QVariant(), game.first, QString()); + NexusInterface::instance().requestUpdates(game.second, this, QVariant(), game.first, QString()); disconnect(sender()); delete sender(); @@ -5655,7 +5676,7 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD } if (requiresInfo) - NexusInterface::instance(&m_PluginContainer)->requestModInfo(gameNameReal, modID, this, QVariant(), QString()); + NexusInterface::instance().requestModInfo(gameNameReal, modID, this, QVariant(), QString()); } void MainWindow::nxmModInfoAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) @@ -5883,6 +5904,8 @@ bool MainWindow::extractProgress(QProgressDialog &progress, int percentage, std: void MainWindow::extractBSATriggered() { + using namespace boost::placeholders; + QTreeWidgetItem *item = m_ContextItem; QString origin; @@ -5954,18 +5977,8 @@ void MainWindow::on_actionNotifications_triggered() void MainWindow::on_actionChange_Game_triggered() { - if (m_OrganizerCore.settings().interface().showChangeGameConfirmation()) { - const auto r = QMessageBox::question( - this, tr("Are you sure?"), tr("This will restart MO, continue?"), - QMessageBox::Yes | QMessageBox::Cancel); - - if (r != QMessageBox::Yes) { - return; - } - } - - InstanceManager::instance().clearCurrentInstance(); - ExitModOrganizer(Exit::Restart); + InstanceManagerDialog dlg(m_PluginContainer, this); + dlg.exec(); } void MainWindow::setCategoryListVisible(bool visible) diff --git a/src/mainwindow.h b/src/mainwindow.h index 814e0363..8b2188c8 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -506,6 +506,7 @@ private slots: void hideSaveGameInfo(); void hookUpWindowTutorials(); + bool shouldStartTutorial() const; void resumeDownload(int downloadIndex); void endorseMod(ModInfo::Ptr mod); diff --git a/src/moapplication.cpp b/src/moapplication.cpp index dd49bf53..290666af 100644 --- a/src/moapplication.cpp +++ b/src/moapplication.cpp @@ -24,16 +24,10 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "shared/appconfig.h"
#include <QFile>
#include <QStringList>
-#if QT_VERSION < QT_VERSION_CHECK(5,0,0)
-#include <QPlastiqueStyle>
-#include <QCleanlooksStyle>
-#endif
#include <QProxyStyle>
#include <QStyleFactory>
#include <QPainter>
#include <QStyleOption>
-
-
#include <QDebug>
@@ -77,7 +71,7 @@ public: };
-MOApplication::MOApplication(int &argc, char **argv)
+MOApplication::MOApplication(int& argc, char** argv)
: QApplication(argc, argv)
{
connect(&m_StyleWatcher, &QFileSystemWatcher::fileChanged, [&](auto&& file){
@@ -89,8 +83,13 @@ MOApplication::MOApplication(int &argc, char **argv) setStyle(new ProxyStyle(style()));
}
+MOApplication MOApplication::create(int& argc, char** argv)
+{
+ QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+ return MOApplication(argc, argv);
+}
-bool MOApplication::setStyleFile(const QString &styleName)
+bool MOApplication::setStyleFile(const QString& styleName)
{
// remove all files from watch
QStringList currentWatch = m_StyleWatcher.files();
@@ -114,7 +113,7 @@ bool MOApplication::setStyleFile(const QString &styleName) }
-bool MOApplication::notify(QObject *receiver, QEvent *event)
+bool MOApplication::notify(QObject* receiver, QEvent* event)
{
try {
return QApplication::notify(receiver, event);
@@ -134,7 +133,7 @@ bool MOApplication::notify(QObject *receiver, QEvent *event) }
-void MOApplication::updateStyle(const QString &fileName)
+void MOApplication::updateStyle(const QString& fileName)
{
if (QStyleFactory::keys().contains(fileName)) {
setStyle(QStyleFactory::create(fileName));
diff --git a/src/moapplication.h b/src/moapplication.h index 9db130af..c67c4622 100644 --- a/src/moapplication.h +++ b/src/moapplication.h @@ -24,27 +24,25 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <QFileSystemWatcher>
-class MOApplication : public QApplication {
-Q_OBJECT
-public:
-
- MOApplication(int &argc, char **argv);
+class MOApplication : public QApplication
+{
+ Q_OBJECT
- virtual bool notify (QObject *receiver, QEvent *event);
+public:
+ static MOApplication create(int& argc, char** argv);
+ virtual bool notify (QObject* receiver, QEvent* event);
public slots:
-
- bool setStyleFile(const QString &style);
+ bool setStyleFile(const QString& style);
private slots:
-
- void updateStyle(const QString &fileName);
+ void updateStyle(const QString& fileName);
private:
-
QFileSystemWatcher m_StyleWatcher;
QString m_DefaultStyle;
+ MOApplication(int& argc, char** argv);
};
diff --git a/src/modinfo.cpp b/src/modinfo.cpp index bbeefb12..a0382fe8 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -266,7 +266,7 @@ void ModInfo::updateFromDisc(const QString &modDirectory, createFromOverwrite(pluginContainer, game, directoryStructure); std::sort(s_Collection.begin(), s_Collection.end(), ModInfo::ByName); - + parallelMap(std::begin(s_Collection), std::end(s_Collection), &ModInfo::prefetch, refreshThreadCount); updateIndices(); @@ -348,19 +348,19 @@ bool ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *recei } for (auto game : organizedGames) - NexusInterface::instance(pluginContainer)->requestUpdates(game.second, receiver, QVariant(), game.first, QString()); + NexusInterface::instance().requestUpdates(game.second, receiver, QVariant(), game.first, QString()); } else if (earliest < QDateTime::currentDateTimeUtc().addMonths(-1)) { for (auto gameName : games) - NexusInterface::instance(pluginContainer)->requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::MONTH, receiver, QVariant(true), QString()); + NexusInterface::instance().requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::MONTH, receiver, QVariant(true), QString()); } else if (earliest < QDateTime::currentDateTimeUtc().addDays(-7)) { for (auto gameName : games) - NexusInterface::instance(pluginContainer)->requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::MONTH, receiver, QVariant(false), QString()); + NexusInterface::instance().requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::MONTH, receiver, QVariant(false), QString()); } else if (earliest < QDateTime::currentDateTimeUtc().addDays(-1)) { for (auto gameName : games) - NexusInterface::instance(pluginContainer)->requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::WEEK, receiver, QVariant(false), QString()); + NexusInterface::instance().requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::WEEK, receiver, QVariant(false), QString()); } else { for (auto gameName : games) - NexusInterface::instance(pluginContainer)->requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::DAY, receiver, QVariant(false), QString()); + NexusInterface::instance().requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::DAY, receiver, QVariant(false), QString()); } return updatesAvailable; @@ -400,7 +400,7 @@ std::set<QSharedPointer<ModInfo>> ModInfo::filteredMods(QString gameName, QVaria return finalMods; } -void ModInfo::manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiver, std::multimap<QString, int> IDs) +void ModInfo::manualUpdateCheck(QObject *receiver, std::multimap<QString, int> IDs) { std::vector<QSharedPointer<ModInfo>> mods; std::set<std::pair<QString, int>> organizedGames; @@ -438,7 +438,7 @@ void ModInfo::manualUpdateCheck(PluginContainer *pluginContainer, QObject *recei } for (auto game : organizedGames) { - NexusInterface::instance(pluginContainer)->requestUpdates(game.second, receiver, QVariant(), game.first, QString()); + NexusInterface::instance().requestUpdates(game.second, receiver, QVariant(), game.first, QString()); } } else { log::info("None of the selected mods can be updated."); diff --git a/src/modinfo.h b/src/modinfo.h index 7223cece..e5e65c82 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -190,8 +190,7 @@ public: // Static functions: * @brief Run a limited batch of mod update checks for "newest version" information. * */ - static void manualUpdateCheck( - PluginContainer *pluginContainer, QObject *receiver, std::multimap<QString, int> IDs); + static void manualUpdateCheck(QObject *receiver, std::multimap<QString, int> IDs); /** * @brief Query nexus information for every mod and update the "newest version" information. @@ -368,6 +367,17 @@ public: // IModInterface implementations / Re-declaration */ virtual MOBase::EndorsedState endorsedState() const override { return MOBase::EndorsedState::ENDORSED_NEVER; } + /** + * @brief Retrieve a file tree corresponding to the underlying disk content + * of this mod. + * + * The file tree should not be cached since it is already cached and updated when + * required. + * + * @return a file tree representing the content of this mod. + */ + virtual std::shared_ptr<const MOBase::IFileTree> fileTree() const = 0; + public: // Mutable operations: /** diff --git a/src/modinfodialog.cpp b/src/modinfodialog.cpp index c3239e0d..cc50d446 100644 --- a/src/modinfodialog.cpp +++ b/src/modinfodialog.cpp @@ -40,30 +40,6 @@ namespace fs = std::filesystem; const int max_scan_for_context_menu = 50; -int naturalCompare(const QString& a, const QString& b) -{ - static QCollator c = []{ - QCollator c; - c.setNumericMode(true); - c.setCaseSensitivity(Qt::CaseInsensitive); - return c; - }(); - - - // todo: remove this once the fix is released - // see https://bugreports.qt.io/projects/QTBUG/issues/QTBUG-81673 - // and https://codereview.qt-project.org/c/qt/qtbase/+/287966/5/src/corelib/text/qcollator_win.cpp - if (!a.size()) { - return b.size() ? -1 : 0; - } - if (!b.size()) { - return +1; - } - - - return c.compare(a, b); -} - bool canPreviewFile( const PluginContainer& pluginContainer, bool isArchive, const QString& filename) diff --git a/src/modinfodialogconflicts.cpp b/src/modinfodialogconflicts.cpp index 0103b58a..cf7f6340 100644 --- a/src/modinfodialogconflicts.cpp +++ b/src/modinfodialogconflicts.cpp @@ -1001,7 +1001,7 @@ std::optional<ConflictItem> AdvancedConflictsTab::createItem( if (currOrigin->getID() == fileOrigin) { // current origin is the active winner, all alternatives go in 'before' - + if (showAllAlts) { for (const auto& alt : alternatives) { @@ -1023,7 +1023,7 @@ std::optional<ConflictItem> AdvancedConflictsTab::createItem( } else { // current mod is one of the alternatives, find its position - + auto currOrgId = currOrigin->getID(); auto currModIter = std::find_if(alternatives.begin(), alternatives.end(), @@ -1037,14 +1037,14 @@ std::optional<ConflictItem> AdvancedConflictsTab::createItem( } isCurrOrigArchive = currModIter->isFromArchive(); - + if (showAllAlts) { // fills 'before' and 'after' with all the alternatives that come - // before and after the current mod, trusting the alternatives vector to be + // before and after the current mod, trusting the alternatives vector to be // already sorted correctly - + for (auto iter = alternatives.begin(); iter != alternatives.end(); iter++) { - + const auto& altOrigin = ds.getOriginByID(iter->originID()); if (iter < currModIter) { @@ -1094,7 +1094,7 @@ std::optional<ConflictItem> AdvancedConflictsTab::createItem( after += ds.getOriginByID(fileOrigin).getName(); } - + } } } diff --git a/src/modinfodialogconflictsmodels.cpp b/src/modinfodialogconflictsmodels.cpp index 17fdef90..464abbd3 100644 --- a/src/modinfodialogconflictsmodels.cpp +++ b/src/modinfodialogconflictsmodels.cpp @@ -1,5 +1,8 @@ #include "modinfodialogconflictsmodels.h" #include "modinfodialog.h" +#include <utility.h> + +using MOBase::naturalCompare; ConflictItem::ConflictItem( QString before, QString relativeName, QString after, diff --git a/src/modinfodialogfwd.h b/src/modinfodialogfwd.h index c758573c..805e8a06 100644 --- a/src/modinfodialogfwd.h +++ b/src/modinfodialogfwd.h @@ -35,9 +35,6 @@ FileRenamer::RenameResults unhideFile(FileRenamer& renamer, const QString& oldNa FileRenamer::RenameResults restoreHiddenFilesRecursive(FileRenamer& renamer, const QString &targetDir); -int naturalCompare(const QString& a, const QString& b); - - class ElideLeftDelegate : public QStyledItemDelegate { public: diff --git a/src/modinfodialognexus.cpp b/src/modinfodialognexus.cpp index dd95cdaa..b4099a8d 100644 --- a/src/modinfodialognexus.cpp +++ b/src/modinfodialognexus.cpp @@ -141,8 +141,8 @@ void NexusTab::updateWebpage() const int modID = mod().nexusId(); if (isValidModID(modID)) { - const QString nexusLink = NexusInterface::instance(&plugin()) - ->getModURL(modID, mod().gameName()); + const QString nexusLink = NexusInterface::instance() + .getModURL(modID, mod().gameName()); ui->visitNexus->setToolTip(nexusLink); refreshData(modID); @@ -360,8 +360,8 @@ void NexusTab::onVisitNexus() const int modID = mod().nexusId(); if (isValidModID(modID)) { - const QString nexusLink = NexusInterface::instance(&plugin()) - ->getModURL(modID, mod().gameName()); + const QString nexusLink = NexusInterface::instance() + .getModURL(modID, mod().gameName()); shell::Open(QUrl(nexusLink)); } diff --git a/src/modinfodialogtextfiles.cpp b/src/modinfodialogtextfiles.cpp index 4bdc8ee1..26ca3eb8 100644 --- a/src/modinfodialogtextfiles.cpp +++ b/src/modinfodialogtextfiles.cpp @@ -203,7 +203,7 @@ void GenericFilesTab::select(const QModelIndex& index) } m_editor->setEnabled(true); - m_editor->load(m_model->fullPath(m_filter.map(index))); + m_editor->load(m_model->fullPath(m_filter.mapToSource(index))); } diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index 78c04f2d..3f5fc7fc 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -682,7 +682,7 @@ std::set<int> ModInfoRegular::doGetContents() const ModDataContent* contentFeature = m_GamePlugin->feature<ModDataContent>(); if (contentFeature) { - auto result = contentFeature->getContentsFor(contentFileTree()); + auto result = contentFeature->getContentsFor(fileTree()); return std::set<int>(std::begin(result), std::end(result)); } diff --git a/src/modinfowithconflictinfo.cpp b/src/modinfowithconflictinfo.cpp index 24c154e6..6e3f751e 100644 --- a/src/modinfowithconflictinfo.cpp +++ b/src/modinfowithconflictinfo.cpp @@ -311,21 +311,21 @@ void ModInfoWithConflictInfo::diskContentModified() { void ModInfoWithConflictInfo::prefetch() { // Populating the tree to 1-depth (IFileTree is lazy, so size() forces the // tree to populate the first level): - contentFileTree()->size(); + fileTree()->size(); } bool ModInfoWithConflictInfo::doIsValid() const { auto mdc = m_GamePlugin->feature<ModDataChecker>(); if (mdc) { - auto qdirfiletree = contentFileTree(); + auto qdirfiletree = fileTree(); return mdc->dataLooksValid(qdirfiletree) == ModDataChecker::CheckReturn::VALID; } return true; } -std::shared_ptr<const IFileTree> ModInfoWithConflictInfo::contentFileTree() const { +std::shared_ptr<const IFileTree> ModInfoWithConflictInfo::fileTree() const { return m_FileTree.value(); } diff --git a/src/modinfowithconflictinfo.h b/src/modinfowithconflictinfo.h index 7109b83d..feded99b 100644 --- a/src/modinfowithconflictinfo.h +++ b/src/modinfowithconflictinfo.h @@ -34,7 +34,18 @@ public: * * @return true if the content is there, false otherwise. */ - virtual bool hasContent(int content) const override; + virtual bool hasContent(int content) const override; /** + * @brief Retrieve a file tree corresponding to the underlying disk content + * of this mod. + * + * The file tree should not be cached since it is already cached and updated when + * required. + * + * @return a file tree representing the content of this mod. + */ + std::shared_ptr<const MOBase::IFileTree> fileTree() const override; + +public: /** * @brief clear all caches held for this mod @@ -78,17 +89,6 @@ protected: **/ virtual std::set<int> doGetContents() const { return {}; } - /** - * @brief Retrieve a file tree corresponding to the underlying disk content - * of this mod. - * - * The file tree should not be cached since it is already cached and updated when - * required. - * - * @return a file tree representing the content of this mod. - */ - std::shared_ptr<const MOBase::IFileTree> contentFileTree() const; - ModInfoWithConflictInfo( PluginContainer* pluginContainer, const MOBase::IPluginGame* gamePlugin, diff --git a/src/modlist.cpp b/src/modlist.cpp index 1f845999..04abfb01 100644 --- a/src/modlist.cpp +++ b/src/modlist.cpp @@ -977,7 +977,7 @@ int ModList::setActive(const QStringList& names, bool active) { indices.append(modIndex); } else { - log::debug("Trying to {} mod {} which does not exist.", + log::debug("Trying to {} mod {} which does not exist.", active ? "enable" : "disable", name); } } @@ -1043,7 +1043,7 @@ void ModList::notifyModRemoved(QString const& modName) const m_ModRemoved(modName); } -void ModList::notifyModStateChanged(QList<unsigned int> modIndices) const +void ModList::notifyModStateChanged(QList<unsigned int> modIndices) const { std::map<QString, ModStates> mods; for (auto modIndex : modIndices) { diff --git a/src/moshortcut.cpp b/src/moshortcut.cpp index aad7380e..4efedbdb 100644 --- a/src/moshortcut.cpp +++ b/src/moshortcut.cpp @@ -40,3 +40,12 @@ MOShortcut::MOShortcut(const QString& link) m_hasExecutable=true; } } + +QString MOShortcut::toString() const +{ + if (m_hasInstance) { + return "moshortcut://" + m_instance + ":" + m_executable; + } else { + return "moshortcut://" + m_executable; + } +} diff --git a/src/moshortcut.h b/src/moshortcut.h index 2ce54910..33346bb9 100644 --- a/src/moshortcut.h +++ b/src/moshortcut.h @@ -1,45 +1,26 @@ -/* -Copyright (C) 2016 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/>. -*/ - - -#pragma once - +#ifndef MODORGANIZER_MOSHORTCUT_INCLUDED +#define MODORGANIZER_MOSHORTCUT_INCLUDED #include <QString> - -class MOShortcut { - +class MOShortcut +{ public: - MOShortcut(const QString& link); + MOShortcut(const QString& link={}); /// true iff intialized using a valid moshortcut link - operator bool() const { return m_valid; } + bool isValid() const { return m_valid; } bool hasInstance() const { return m_hasInstance; } - + bool hasExecutable() const { return m_hasExecutable; } const QString& instance() const { return m_instance; } const QString& executable() const { return m_executable; } + QString toString() const; + private: QString m_instance; QString m_executable; @@ -47,3 +28,5 @@ private: bool m_hasInstance; bool m_hasExecutable; }; + +#endif // MODORGANIZER_MOSHORTCUT_INCLUDED diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 9eb1540a..1364d7e1 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -23,9 +23,11 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "nxmaccessmanager.h" #include "selectiondialog.h" #include "bbcode.h" +#include "settings.h" #include <utility.h> #include "shared/util.h" #include <log.h> +#include <moassert.h> #include <QApplication> #include <QNetworkCookieJar> @@ -50,7 +52,7 @@ void throttledWarning(const APIUserAccount& user) NexusBridge::NexusBridge(PluginContainer *pluginContainer, const QString &subModule) - : m_Interface(NexusInterface::instance(pluginContainer)) + : m_Interface(&NexusInterface::instance()) , m_SubModule(subModule) { } @@ -234,31 +236,42 @@ APILimits NexusInterface::parseLimits( } -NexusInterface::NexusInterface(PluginContainer *pluginContainer) - : m_PluginContainer(pluginContainer) +static NexusInterface* g_instance = nullptr; + +NexusInterface::NexusInterface(Settings* s) + : m_PluginContainer(nullptr) { + MO_ASSERT(!g_instance); + g_instance = this; + m_User.limits(defaultAPILimits()); m_MOVersion = createVersionInfo(); - m_AccessManager = new NXMAccessManager(this, m_MOVersion.displayString(3)); + m_AccessManager = new NXMAccessManager( + this, s, m_MOVersion.displayString(3)); + m_DiskCache = new QNetworkDiskCache(this); + connect(m_AccessManager, SIGNAL(requestNXMDownload(QString)), this, SLOT(downloadRequestedNXM(QString))); } -NXMAccessManager *NexusInterface::getAccessManager() +NexusInterface::~NexusInterface() { - return m_AccessManager; + cleanup(); + + MO_ASSERT(g_instance == this); + g_instance = nullptr; } -NexusInterface::~NexusInterface() +NXMAccessManager *NexusInterface::getAccessManager() { - cleanup(); + return m_AccessManager; } -NexusInterface *NexusInterface::instance(PluginContainer *pluginContainer) +NexusInterface& NexusInterface::instance() { - static NexusInterface s_Instance(pluginContainer); - return &s_Instance; + MO_ASSERT(g_instance); + return *g_instance; } void NexusInterface::setCacheDirectory(const QString &directory) @@ -284,7 +297,7 @@ void NexusInterface::interpretNexusFileName(const QString &fileName, QString &mo static const QRegularExpression complex(R"(^([a-zA-Z0-9_'"\-.() ]*?)([-_ ][VvRr]+[0-9]+(?:(?:[\.][0-9]+){0,2}|(?:[_][0-9]+){0,2}|(?:[-.][0-9]+){0,2})?[ab]?)??-([1-9][0-9]+)?-.*?\.(zip|rar|7z))"); //complex regex explanation: //group 1: modname. - //group 2: optional version, + //group 2: optional version, // assumed to start with v (empty most of the time). //group 3: NexusId, // assumed wrapped in "-", will miss single digit IDs for better accuracy. diff --git a/src/nexusinterface.h b/src/nexusinterface.h index 35362bdf..cf365b2e 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -40,7 +40,7 @@ namespace MOBase { class IPluginGame; } class NexusInterface; class NXMAccessManager; - +class Settings; /** * @brief convenience class to make nxm requests easier @@ -153,9 +153,10 @@ public: static APILimits parseLimits(const QNetworkReply* reply); static APILimits parseLimits(const QList<QNetworkReply::RawHeaderPair>& headers); + NexusInterface(Settings* s); ~NexusInterface(); - static NexusInterface *instance(PluginContainer *pluginContainer); + static NexusInterface& instance(); /** * @return the access manager object used to connect to nexus @@ -534,8 +535,6 @@ private: static const int MAX_ACTIVE_DOWNLOADS = 6; private: - - NexusInterface(PluginContainer *pluginContainer); void nextRequest(); void requestFinished(std::list<NXMRequestInfo>::iterator iter); MOBase::IPluginGame *getGame(QString gameName) const; diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp index 2fe676ba..6c8b9054 100644 --- a/src/nxmaccessmanager.cpp +++ b/src/nxmaccessmanager.cpp @@ -48,8 +48,8 @@ const QString NexusSSO("wss://sso.nexusmods.com"); const QString NexusSSOPage("https://www.nexusmods.com/sso?id=%1&application=modorganizer2"); -ValidationProgressDialog::ValidationProgressDialog(NexusKeyValidator& v) - : m_validator(v), m_updateTimer(nullptr), m_first(true) +ValidationProgressDialog::ValidationProgressDialog(Settings* s, NexusKeyValidator& v) + : m_settings(s), m_validator(v), m_updateTimer(nullptr), m_first(true) { ui.reset(new Ui::ValidationProgressDialog); ui->setupUi(this); @@ -98,7 +98,10 @@ void ValidationProgressDialog::stop() void ValidationProgressDialog::showEvent(QShowEvent* e) { if (m_first) { - Settings::instance().geometry().centerOnMainWindowMonitor(this); + if (m_settings) { + m_settings->geometry().centerOnMainWindowMonitor(this); + } + m_first = false; } } @@ -592,8 +595,8 @@ void ValidationAttempt::cleanup() } -NexusKeyValidator::NexusKeyValidator(NXMAccessManager& am) - : m_manager(am) +NexusKeyValidator::NexusKeyValidator(Settings* s, NXMAccessManager& am) + : m_settings(s), m_manager(am) { } @@ -602,6 +605,15 @@ NexusKeyValidator::~NexusKeyValidator() cancel(); } +std::vector<std::chrono::seconds> NexusKeyValidator::getTimeouts() const +{ + if (m_settings) { + return m_settings->nexus().validationTimeouts(); + } else { + return {10s, 15s, 20s}; + } +} + void NexusKeyValidator::start(const QString& key, Behaviour b) { if (isActive()) { @@ -611,7 +623,7 @@ void NexusKeyValidator::start(const QString& key, Behaviour b) m_key = key; - const auto timeouts = Settings::instance().nexus().validationTimeouts(); + const auto timeouts = getTimeouts(); switch (b) { @@ -755,10 +767,11 @@ void NexusKeyValidator::setFinished( } -NXMAccessManager::NXMAccessManager(QObject *parent, const QString &moVersion) +NXMAccessManager::NXMAccessManager(QObject *parent, Settings* s, const QString &moVersion) : QNetworkAccessManager(parent) + , m_Settings(s) , m_MOVersion(moVersion) - , m_validator(*this) + , m_validator(s, *this) , m_validationState(NotChecked) { m_validator.finished = [&](auto&& r, auto&& m, auto&& u) { @@ -769,8 +782,10 @@ NXMAccessManager::NXMAccessManager(QObject *parent, const QString &moVersion) onValidatorAttemptFinished(a); }; - setCookieJar(new PersistentCookieJar(QDir::fromNativeSeparators( - Settings::instance().paths().cache() + "/nexus_cookies.dat"))); + if (m_Settings) { + setCookieJar(new PersistentCookieJar(QDir::fromNativeSeparators( + m_Settings->paths().cache() + "/nexus_cookies.dat"))); + } if (networkAccessible() == QNetworkAccessManager::UnknownAccessibility) { // why is this necessary all of a sudden? @@ -963,7 +978,7 @@ void NXMAccessManager::clearApiKey() void NXMAccessManager::startProgress() { if (!m_ProgressDialog) { - m_ProgressDialog.reset(new ValidationProgressDialog(m_validator)); + m_ProgressDialog.reset(new ValidationProgressDialog(m_Settings, m_validator)); } m_ProgressDialog->start(); diff --git a/src/nxmaccessmanager.h b/src/nxmaccessmanager.h index 6a45d880..60100467 100644 --- a/src/nxmaccessmanager.h +++ b/src/nxmaccessmanager.h @@ -33,6 +33,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. namespace MOBase { class IPluginGame; } namespace Ui { class ValidationProgressDialog; } class NXMAccessManager; +class Settings; class NexusSSOLogin { @@ -146,7 +147,7 @@ public: std::function<FinishedCallback> finished; std::function<void (const ValidationAttempt&)> attemptFinished; - NexusKeyValidator(NXMAccessManager& am); + NexusKeyValidator(Settings* s, NXMAccessManager& am); ~NexusKeyValidator(); void start(const QString& key, Behaviour b); @@ -157,11 +158,13 @@ public: const ValidationAttempt* currentAttempt() const; private: + Settings* m_settings; NXMAccessManager& m_manager; QString m_key; std::vector<std::unique_ptr<ValidationAttempt>> m_attempts; void createAttempts(const std::vector<std::chrono::seconds>& timeouts); + std::vector<std::chrono::seconds> getTimeouts() const; bool nextTry(); void onAttemptSuccess(const ValidationAttempt& a, const APIUserAccount& u); @@ -178,7 +181,7 @@ class ValidationProgressDialog : public QDialog Q_OBJECT; public: - ValidationProgressDialog(NexusKeyValidator& v); + ValidationProgressDialog(Settings* s, NexusKeyValidator& v); void setParentWidget(QWidget* w); @@ -191,6 +194,7 @@ protected: private: std::unique_ptr<Ui::ValidationProgressDialog> ui; + Settings* m_settings; NexusKeyValidator& m_validator; QTimer* m_updateTimer; bool m_first; @@ -209,7 +213,7 @@ class NXMAccessManager : public QNetworkAccessManager { Q_OBJECT public: - NXMAccessManager(QObject *parent, const QString &moVersion); + NXMAccessManager(QObject *parent, Settings* s, const QString &moVersion); void setTopLevelWidget(QWidget* w); @@ -264,6 +268,7 @@ private: }; QWidget* m_TopLevel; + Settings* m_Settings; mutable std::unique_ptr<ValidationProgressDialog> m_ProgressDialog; QString m_MOVersion; NexusKeyValidator m_validator; diff --git a/src/organizer_en.ts b/src/organizer_en.ts index 1ad4650f..c65c946c 100644 --- a/src/organizer_en.ts +++ b/src/organizer_en.ts @@ -110,22 +110,22 @@ <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="405"/> + <location filename="aboutdialog.ui" line="400"/> <source>...and all other contributors!</source> <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="419"/> + <location filename="aboutdialog.ui" line="414"/> <source>Other Supporters && Contributors</source> <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="429"/> + <location filename="aboutdialog.ui" line="424"/> <source>Tannin (Original Creator)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="aboutdialog.ui" line="561"/> + <location filename="aboutdialog.ui" line="556"/> <source>Close</source> <translation type="unfinished"></translation> </message> @@ -178,17 +178,17 @@ p, li { white-space: pre-wrap; } <context> <name>AdvancedConflictListModel</name> <message> - <location filename="modinfodialogconflictsmodels.cpp" line="328"/> + <location filename="modinfodialogconflictsmodels.cpp" line="280"/> <source>Overwrites</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialogconflictsmodels.cpp" line="329"/> + <location filename="modinfodialogconflictsmodels.cpp" line="281"/> <source>File</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialogconflictsmodels.cpp" line="330"/> + <location filename="modinfodialogconflictsmodels.cpp" line="282"/> <source>Overwritten By</source> <translation type="unfinished"></translation> </message> @@ -1462,54 +1462,54 @@ Right now the only case I know of where this needs to be overwritten is for the <context> <name>FileTreeModel</name> <message> - <location filename="filetreemodel.cpp" line="445"/> + <location filename="filetreemodel.cpp" line="427"/> <source>Name</source> <translation type="unfinished"></translation> </message> <message> - <location filename="filetreemodel.cpp" line="445"/> + <location filename="filetreemodel.cpp" line="427"/> <source>Mod</source> <translation type="unfinished"></translation> </message> <message> - <location filename="filetreemodel.cpp" line="445"/> + <location filename="filetreemodel.cpp" line="427"/> <source>Type</source> <translation type="unfinished"></translation> </message> <message> - <location filename="filetreemodel.cpp" line="445"/> + <location filename="filetreemodel.cpp" line="427"/> <source>Size</source> <translation type="unfinished"></translation> </message> <message> - <location filename="filetreemodel.cpp" line="445"/> + <location filename="filetreemodel.cpp" line="427"/> <source>Date modified</source> <translation type="unfinished"></translation> </message> <message> - <location filename="filetreemodel.cpp" line="1156"/> + <location filename="filetreemodel.cpp" line="1123"/> <source>Directory</source> <translation type="unfinished"></translation> </message> <message> - <location filename="filetreemodel.cpp" line="1157"/> - <location filename="filetreemodel.cpp" line="1174"/> + <location filename="filetreemodel.cpp" line="1124"/> + <location filename="filetreemodel.cpp" line="1141"/> <source>Virtual path</source> <translation type="unfinished"></translation> </message> <message> - <location filename="filetreemodel.cpp" line="1175"/> + <location filename="filetreemodel.cpp" line="1142"/> <source>Real path</source> <translation type="unfinished"></translation> </message> <message> - <location filename="filetreemodel.cpp" line="1176"/> + <location filename="filetreemodel.cpp" line="1143"/> <source>From</source> <translation type="unfinished"></translation> </message> <message> - <location filename="filetreemodel.cpp" line="1192"/> - <location filename="filetreemodel.cpp" line="1194"/> + <location filename="filetreemodel.cpp" line="1159"/> + <location filename="filetreemodel.cpp" line="1161"/> <source>Also in</source> <translation type="unfinished"></translation> </message> @@ -1869,6 +1869,85 @@ This is likely due to a corrupted or incompatible download or unrecognized archi </message> </context> <context> + <name>InstanceManagerDialog</name> + <message> + <location filename="instancemanagerdialog.ui" line="14"/> + <source>Instance manager</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="instancemanagerdialog.ui" line="35"/> + <source>Create new instance</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="instancemanagerdialog.ui" line="46"/> + <source>Create portable instance</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="instancemanagerdialog.ui" line="70"/> + <source><html><head/><body><p><a href="https://github.com/ModOrganizer2/modorganizer/wiki/Instance"><span style=" text-decoration: underline; color:#0000ff;">What is an instance?</span></a></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="instancemanagerdialog.ui" line="163"/> + <source>Game:</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="instancemanagerdialog.ui" line="170"/> + <source>Name:</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="instancemanagerdialog.ui" line="177"/> + <source>Location:</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="instancemanagerdialog.ui" line="184"/> + <source>Rename</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="instancemanagerdialog.ui" line="201"/> + <source>Base folder:</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="instancemanagerdialog.ui" line="218"/> + <location filename="instancemanagerdialog.ui" line="225"/> + <source>Explore</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="instancemanagerdialog.ui" line="273"/> + <source>Convert to portable</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="instancemanagerdialog.ui" line="280"/> + <source>Convert to global</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="instancemanagerdialog.ui" line="287"/> + <source>Delete instance</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="instancemanagerdialog.ui" line="351"/> + <source>Switch to this instance</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="instancemanagerdialog.ui" line="362"/> + <source>Cancel</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> <name>ListDialog</name> <message> <location filename="listdialog.ui" line="14"/> @@ -1999,12 +2078,12 @@ This is likely due to a corrupted or incompatible download or unrecognized archi <context> <name>MOApplication</name> <message> - <location filename="moapplication.cpp" line="125"/> + <location filename="moapplication.cpp" line="124"/> <source>an error occurred: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="moapplication.cpp" line="131"/> + <location filename="moapplication.cpp" line="130"/> <source>an error occurred</source> <translation type="unfinished"></translation> </message> @@ -2199,7 +2278,7 @@ p, li { white-space: pre-wrap; } </message> <message> <location filename="mainwindow.ui" line="338"/> - <location filename="mainwindow.cpp" line="4803"/> + <location filename="mainwindow.cpp" line="4806"/> <source>Create Backup</source> <translation type="unfinished"></translation> </message> @@ -2353,7 +2432,7 @@ p, li { white-space: pre-wrap; } <message> <location filename="mainwindow.ui" line="1093"/> <location filename="mainwindow.ui" line="1259"/> - <location filename="mainwindow.cpp" line="4646"/> + <location filename="mainwindow.cpp" line="4649"/> <source>Refresh</source> <translation type="unfinished"></translation> </message> @@ -2677,7 +2756,7 @@ p, li { white-space: pre-wrap; } <message> <location filename="mainwindow.ui" line="1759"/> <location filename="mainwindow.ui" line="1762"/> - <location filename="mainwindow.cpp" line="5319"/> + <location filename="mainwindow.cpp" line="5322"/> <source>Endorse Mod Organizer</source> <translation type="unfinished"></translation> </message> @@ -2755,829 +2834,833 @@ p, li { white-space: pre-wrap; } <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="385"/> + <location filename="mainwindow.cpp" line="386"/> <source>Toolbar and Menu</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="386"/> + <location filename="mainwindow.cpp" line="387"/> <source>Desktop</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="387"/> + <location filename="mainwindow.cpp" line="388"/> <source>Start Menu</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="412"/> + <location filename="mainwindow.cpp" line="413"/> <source>There is no supported sort mechanism for this game. You will probably have to use a third-party tool.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="711"/> + <location filename="mainwindow.cpp" line="712"/> <source>Crash on exit</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="712"/> + <location filename="mainwindow.cpp" line="713"/> <source>MO crashed while exiting. Some settings may not be saved. Error: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1042"/> + <location filename="mainwindow.cpp" line="1043"/> <source>There are notifications to read</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1061"/> + <location filename="mainwindow.cpp" line="1062"/> <source>There are no notifications</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1163"/> - <location filename="mainwindow.cpp" line="4826"/> - <location filename="mainwindow.cpp" line="4830"/> + <location filename="mainwindow.cpp" line="1164"/> + <location filename="mainwindow.cpp" line="4829"/> + <location filename="mainwindow.cpp" line="4833"/> <source>Endorse</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1167"/> + <location filename="mainwindow.cpp" line="1168"/> <source>Won't Endorse</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1184"/> + <location filename="mainwindow.cpp" line="1185"/> <source>Help on UI</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1188"/> + <location filename="mainwindow.cpp" line="1189"/> <source>Documentation</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1192"/> + <location filename="mainwindow.cpp" line="1193"/> <source>Chat on Discord</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1196"/> + <location filename="mainwindow.cpp" line="1197"/> <source>Report Issue</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1200"/> + <location filename="mainwindow.cpp" line="1201"/> <source>Tutorials</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1239"/> + <location filename="mainwindow.cpp" line="1240"/> <source>About</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1240"/> + <location filename="mainwindow.cpp" line="1241"/> <source>About Qt</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1299"/> + <location filename="mainwindow.cpp" line="1300"/> <source>Name</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1300"/> + <location filename="mainwindow.cpp" line="1301"/> <source>Please enter a name for the new profile</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1308"/> + <location filename="mainwindow.cpp" line="1309"/> <source>failed to create profile: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1363"/> + <location filename="mainwindow.cpp" line="1364"/> <source>Show tutorial?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1364"/> + <location filename="mainwindow.cpp" line="1365"/> <source>You are starting Mod Organizer for the first time. Do you want to show a tutorial of its basic features? If you choose no you can always start the tutorial from the "Help"-menu.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1467"/> + <location filename="mainwindow.cpp" line="1468"/> <source>Downloads in progress</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1468"/> + <location filename="mainwindow.cpp" line="1469"/> <source>There are still downloads in progress, do you really want to quit?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1588"/> + <location filename="mainwindow.cpp" line="1589"/> <source>Plugin "%1" failed: %2</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1590"/> + <location filename="mainwindow.cpp" line="1591"/> <source>Plugin "%1" failed</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1679"/> + <location filename="mainwindow.cpp" line="1680"/> <source>Browse Mod Page</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1853"/> + <location filename="mainwindow.cpp" line="1854"/> <source><Edit...></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1864"/> + <location filename="mainwindow.cpp" line="1865"/> <source>(no executables)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2092"/> + <location filename="mainwindow.cpp" line="2093"/> <source>This bsa is enabled in the ini file so it may be required!</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2145"/> + <location filename="mainwindow.cpp" line="2146"/> <source>Activating Network Proxy</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2237"/> + <location filename="mainwindow.cpp" line="2238"/> <source>Notice: Your current MO version (%1) is lower than the previously used one (%2). The GUI may not downgrade gracefully, so you may experience oddities. However, there should be no serious issues.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2311"/> + <location filename="mainwindow.cpp" line="2312"/> <source>Choose Mod</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2312"/> + <location filename="mainwindow.cpp" line="2313"/> <source>Mod Archive</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2406"/> + <location filename="mainwindow.cpp" line="2407"/> <source>Start Tutorial?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2407"/> + <location filename="mainwindow.cpp" line="2408"/> <source>You're about to start a tutorial. For technical reasons it's not possible to end the tutorial early. Continue?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2615"/> + <location filename="mainwindow.cpp" line="2616"/> <source>failed to change origin name: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2639"/> + <location filename="mainwindow.cpp" line="2640"/> <source>failed to move "%1" from mod "%2" to "%3": %4</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2652"/> + <location filename="mainwindow.cpp" line="2653"/> <source>failed to rename mod: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2665"/> + <location filename="mainwindow.cpp" line="2666"/> <source>Overwrite?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2666"/> + <location filename="mainwindow.cpp" line="2667"/> <source>This will replace the existing mod "%1". Continue?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2669"/> + <location filename="mainwindow.cpp" line="2670"/> <source>failed to remove mod "%1"</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2673"/> + <location filename="mainwindow.cpp" line="2674"/> <source>failed to rename "%1" to "%2"</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2761"/> - <location filename="mainwindow.cpp" line="3188"/> - <location filename="mainwindow.cpp" line="4349"/> - <location filename="mainwindow.cpp" line="4357"/> - <location filename="mainwindow.cpp" line="4928"/> + <location filename="mainwindow.cpp" line="2762"/> + <location filename="mainwindow.cpp" line="3189"/> + <location filename="mainwindow.cpp" line="4352"/> + <location filename="mainwindow.cpp" line="4360"/> + <location filename="mainwindow.cpp" line="4931"/> <source>Confirm</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2762"/> + <location filename="mainwindow.cpp" line="2763"/> <source>Remove the following mods?<br><ul>%1</ul></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2777"/> + <location filename="mainwindow.cpp" line="2778"/> <source>failed to remove mod: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2809"/> - <location filename="mainwindow.cpp" line="2812"/> - <location filename="mainwindow.cpp" line="2822"/> + <location filename="mainwindow.cpp" line="2810"/> + <location filename="mainwindow.cpp" line="2813"/> + <location filename="mainwindow.cpp" line="2823"/> <source>Failed</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2809"/> + <location filename="mainwindow.cpp" line="2810"/> <source>Installation file no longer exists</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2813"/> + <location filename="mainwindow.cpp" line="2814"/> <source>Mods installed with old versions of MO can't be reinstalled in this way.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2823"/> + <location filename="mainwindow.cpp" line="2824"/> <source>Failed to create backup.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2851"/> + <location filename="mainwindow.cpp" line="2852"/> <source>Endorsing multiple mods will take a while. Please wait...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2887"/> + <location filename="mainwindow.cpp" line="2888"/> <source>Unendorsing multiple mods will take a while. Please wait...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="2964"/> + <location filename="mainwindow.cpp" line="2965"/> <source>Failed to display overwrite dialog: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3189"/> + <location filename="mainwindow.cpp" line="3190"/> <source>Restore all hidden files in the following mods?<br><ul>%1</ul></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3218"/> - <location filename="mainwindow.cpp" line="3818"/> - <location filename="mainwindow.cpp" line="5945"/> + <location filename="mainwindow.cpp" line="3219"/> + <location filename="mainwindow.cpp" line="3825"/> <source>Are you sure?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3219"/> + <location filename="mainwindow.cpp" line="3220"/> <source>About to restore all hidden files in: </source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3244"/> + <location filename="mainwindow.cpp" line="3245"/> <source>Opening Nexus Links</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3245"/> + <location filename="mainwindow.cpp" line="3246"/> <source>You are trying to open %1 links to Nexus Mods. Are you sure you want to do this?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3272"/> + <location filename="mainwindow.cpp" line="3273"/> <source>Nexus ID for this mod is unknown</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3283"/> - <location filename="mainwindow.cpp" line="3340"/> + <location filename="mainwindow.cpp" line="3284"/> + <location filename="mainwindow.cpp" line="3318"/> <source>Opening Web Pages</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3284"/> - <location filename="mainwindow.cpp" line="3341"/> + <location filename="mainwindow.cpp" line="3285"/> + <location filename="mainwindow.cpp" line="3319"/> <source>You are trying to open %1 Web Pages. Are you sure you want to do this?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3499"/> + <location filename="mainwindow.cpp" line="3358"/> + <source>No valid Web Page for this mod</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="mainwindow.cpp" line="3506"/> <source><table cellspacing="5"><tr><th>Type</th><th>All</th><th>Visible</th><tr><td>Enabled mods:&emsp;</td><td align=right>%1 / %2</td><td align=right>%3 / %4</td></tr><tr><td>Unmanaged/DLCs:&emsp;</td><td align=right>%5</td><td align=right>%6</td></tr><tr><td>Mod backups:&emsp;</td><td align=right>%7</td><td align=right>%8</td></tr><tr><td>Separators:&emsp;</td><td align=right>%9</td><td align=right>%10</td></tr></table></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3554"/> + <location filename="mainwindow.cpp" line="3561"/> <source><table cellspacing="6"><tr><th>Type</th><th>Active </th><th>Total</th></tr><tr><td>All plugins:</td><td align=right>%1 </td><td align=right>%2</td></tr><tr><td>ESMs:</td><td align=right>%3 </td><td align=right>%4</td></tr><tr><td>ESPs:</td><td align=right>%7 </td><td align=right>%8</td></tr><tr><td>ESMs+ESPs:</td><td align=right>%9 </td><td align=right>%10</td></tr><tr><td>ESLs:</td><td align=right>%5 </td><td align=right>%6</td></tr></table></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3586"/> - <location filename="mainwindow.cpp" line="3722"/> - <location filename="mainwindow.cpp" line="4711"/> + <location filename="mainwindow.cpp" line="3593"/> + <location filename="mainwindow.cpp" line="3729"/> + <location filename="mainwindow.cpp" line="4714"/> <source>Create Mod...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3587"/> + <location filename="mainwindow.cpp" line="3594"/> <source>This will create an empty mod. Please enter a name:</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3596"/> - <location filename="mainwindow.cpp" line="3732"/> + <location filename="mainwindow.cpp" line="3603"/> + <location filename="mainwindow.cpp" line="3739"/> <source>A mod with this name already exists</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3624"/> + <location filename="mainwindow.cpp" line="3631"/> <source>Create Separator...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3625"/> + <location filename="mainwindow.cpp" line="3632"/> <source>This will create a new separator. Please enter a name:</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3632"/> + <location filename="mainwindow.cpp" line="3639"/> <source>A separator with this name already exists</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3723"/> + <location filename="mainwindow.cpp" line="3730"/> <source>This will move all files from overwrite into a new, regular mod. Please enter a name:</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3796"/> + <location filename="mainwindow.cpp" line="3803"/> <source>Move successful.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3819"/> + <location filename="mainwindow.cpp" line="3826"/> <source>About to recursively delete: </source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4231"/> + <location filename="mainwindow.cpp" line="4234"/> <source>Continue?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4232"/> + <location filename="mainwindow.cpp" line="4235"/> <source>The versioning scheme decides which version is considered newer than another. This function will guess the versioning scheme under the assumption that the installed version is outdated.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4252"/> + <location filename="mainwindow.cpp" line="4255"/> <source>Sorry</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4253"/> + <location filename="mainwindow.cpp" line="4256"/> <source>I don't know a versioning scheme where %1 is newer than %2.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4349"/> + <location filename="mainwindow.cpp" line="4352"/> <source>Really enable all visible mods?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4357"/> + <location filename="mainwindow.cpp" line="4360"/> <source>Really disable all visible mods?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4439"/> + <location filename="mainwindow.cpp" line="4442"/> <source>Export to csv</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4442"/> + <location filename="mainwindow.cpp" line="4445"/> <source>CSV (Comma Separated Values) is a format that can be imported in programs like Excel to create a spreadsheet. You can also use online editors and converters instead.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4445"/> + <location filename="mainwindow.cpp" line="4448"/> <source>Select what mods you want export:</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4446"/> + <location filename="mainwindow.cpp" line="4449"/> <source>All installed mods</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4447"/> + <location filename="mainwindow.cpp" line="4450"/> <source>Only active (checked) mods from your current profile</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4448"/> + <location filename="mainwindow.cpp" line="4451"/> <source>All currently visible mods in the mod list</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4469"/> + <location filename="mainwindow.cpp" line="4472"/> <source>Choose what Columns to export:</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4472"/> + <location filename="mainwindow.cpp" line="4475"/> <source>Mod_Priority</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4474"/> + <location filename="mainwindow.cpp" line="4477"/> <source>Mod_Name</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4476"/> + <location filename="mainwindow.cpp" line="4479"/> <source>Notes_column</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4477"/> + <location filename="mainwindow.cpp" line="4480"/> <source>Mod_Status</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4479"/> + <location filename="mainwindow.cpp" line="4482"/> <source>Primary_Category</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4480"/> + <location filename="mainwindow.cpp" line="4483"/> <source>Nexus_ID</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4481"/> + <location filename="mainwindow.cpp" line="4484"/> <source>Mod_Nexus_URL</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4482"/> + <location filename="mainwindow.cpp" line="4485"/> <source>Mod_Version</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4483"/> + <location filename="mainwindow.cpp" line="4486"/> <source>Install_Date</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4484"/> + <location filename="mainwindow.cpp" line="4487"/> <source>Download_File_Name</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4592"/> + <location filename="mainwindow.cpp" line="4595"/> <source>export failed: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4611"/> + <location filename="mainwindow.cpp" line="4614"/> <source>Open Game folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4612"/> + <location filename="mainwindow.cpp" line="4615"/> <source>Open MyGames folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4613"/> + <location filename="mainwindow.cpp" line="4616"/> <source>Open INIs folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4618"/> + <location filename="mainwindow.cpp" line="4621"/> <source>Open Instance folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4619"/> + <location filename="mainwindow.cpp" line="4622"/> <source>Open Mods folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4620"/> + <location filename="mainwindow.cpp" line="4623"/> <source>Open Profile folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4621"/> + <location filename="mainwindow.cpp" line="4624"/> <source>Open Downloads folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4627"/> + <location filename="mainwindow.cpp" line="4630"/> <source>Open MO2 Install folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4628"/> + <location filename="mainwindow.cpp" line="4631"/> <source>Open MO2 Plugins folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4629"/> + <location filename="mainwindow.cpp" line="4632"/> <source>Open MO2 Stylesheets folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4630"/> + <location filename="mainwindow.cpp" line="4633"/> <source>Open MO2 Logs folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4637"/> + <location filename="mainwindow.cpp" line="4640"/> <source>Install Mod...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4638"/> + <location filename="mainwindow.cpp" line="4641"/> <source>Create empty mod</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4639"/> + <location filename="mainwindow.cpp" line="4642"/> <source>Create Separator</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4643"/> + <location filename="mainwindow.cpp" line="4646"/> <source>Enable all visible</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4644"/> + <location filename="mainwindow.cpp" line="4647"/> <source>Disable all visible</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4645"/> + <location filename="mainwindow.cpp" line="4648"/> <source>Check for updates</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4647"/> + <location filename="mainwindow.cpp" line="4650"/> <source>Export to csv...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4656"/> - <location filename="mainwindow.cpp" line="4672"/> + <location filename="mainwindow.cpp" line="4659"/> + <location filename="mainwindow.cpp" line="4675"/> <source>Send to</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4657"/> - <location filename="mainwindow.cpp" line="4673"/> + <location filename="mainwindow.cpp" line="4660"/> + <location filename="mainwindow.cpp" line="4676"/> <source>Top</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4658"/> - <location filename="mainwindow.cpp" line="4674"/> + <location filename="mainwindow.cpp" line="4661"/> + <location filename="mainwindow.cpp" line="4677"/> <source>Bottom</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4659"/> - <location filename="mainwindow.cpp" line="4675"/> + <location filename="mainwindow.cpp" line="4662"/> + <location filename="mainwindow.cpp" line="4678"/> <source>Priority...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4660"/> + <location filename="mainwindow.cpp" line="4663"/> <source>Separator...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4701"/> + <location filename="mainwindow.cpp" line="4704"/> <source>All Mods</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4710"/> + <location filename="mainwindow.cpp" line="4713"/> <source>Sync to Mods...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4712"/> + <location filename="mainwindow.cpp" line="4715"/> <source>Move content to Mod...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4713"/> + <location filename="mainwindow.cpp" line="4716"/> <source>Clear Overwrite...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4715"/> - <location filename="mainwindow.cpp" line="4740"/> - <location filename="mainwindow.cpp" line="4880"/> + <location filename="mainwindow.cpp" line="4718"/> + <location filename="mainwindow.cpp" line="4743"/> + <location filename="mainwindow.cpp" line="4883"/> <source>Open in Explorer</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4719"/> + <location filename="mainwindow.cpp" line="4722"/> <source>Restore Backup</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4720"/> + <location filename="mainwindow.cpp" line="4723"/> <source>Remove Backup...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4723"/> - <location filename="mainwindow.cpp" line="4860"/> + <location filename="mainwindow.cpp" line="4726"/> + <location filename="mainwindow.cpp" line="4863"/> <source>Ignore missing data</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4726"/> - <location filename="mainwindow.cpp" line="4864"/> + <location filename="mainwindow.cpp" line="4729"/> + <location filename="mainwindow.cpp" line="4867"/> <source>Mark as converted/working</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4730"/> - <location filename="mainwindow.cpp" line="4870"/> + <location filename="mainwindow.cpp" line="4733"/> + <location filename="mainwindow.cpp" line="4873"/> <source>Visit on Nexus</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4736"/> - <location filename="mainwindow.cpp" line="4876"/> + <location filename="mainwindow.cpp" line="4739"/> + <location filename="mainwindow.cpp" line="4879"/> <source>Visit on %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4744"/> - <location filename="mainwindow.cpp" line="4767"/> + <location filename="mainwindow.cpp" line="4747"/> + <location filename="mainwindow.cpp" line="4770"/> <source>Change Categories</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4748"/> - <location filename="mainwindow.cpp" line="4772"/> + <location filename="mainwindow.cpp" line="4751"/> + <location filename="mainwindow.cpp" line="4775"/> <source>Primary Category</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4752"/> + <location filename="mainwindow.cpp" line="4755"/> <source>Rename Separator...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4753"/> + <location filename="mainwindow.cpp" line="4756"/> <source>Remove Separator...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4756"/> - <location filename="mainwindow.cpp" line="4812"/> + <location filename="mainwindow.cpp" line="4759"/> + <location filename="mainwindow.cpp" line="4815"/> <source>Select Color...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4759"/> - <location filename="mainwindow.cpp" line="4815"/> + <location filename="mainwindow.cpp" line="4762"/> + <location filename="mainwindow.cpp" line="4818"/> <source>Reset Color</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4779"/> + <location filename="mainwindow.cpp" line="4782"/> <source>Change versioning scheme</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4783"/> + <location filename="mainwindow.cpp" line="4786"/> <source>Force-check updates</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4785"/> + <location filename="mainwindow.cpp" line="4788"/> <source>Un-ignore update</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4788"/> + <location filename="mainwindow.cpp" line="4791"/> <source>Ignore update</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4793"/> - <location filename="mainwindow.cpp" line="6105"/> + <location filename="mainwindow.cpp" line="4796"/> + <location filename="mainwindow.cpp" line="6111"/> <source>Enable selected</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4794"/> - <location filename="mainwindow.cpp" line="6106"/> + <location filename="mainwindow.cpp" line="4797"/> + <location filename="mainwindow.cpp" line="6112"/> <source>Disable selected</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4800"/> + <location filename="mainwindow.cpp" line="4803"/> <source>Rename Mod...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4801"/> + <location filename="mainwindow.cpp" line="4804"/> <source>Reinstall Mod</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4802"/> + <location filename="mainwindow.cpp" line="4805"/> <source>Remove Mod...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4806"/> + <location filename="mainwindow.cpp" line="4809"/> <source>Restore hidden files</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4823"/> + <location filename="mainwindow.cpp" line="4826"/> <source>Un-Endorse</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4827"/> + <location filename="mainwindow.cpp" line="4830"/> <source>Won't endorse</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4833"/> + <location filename="mainwindow.cpp" line="4836"/> <source>Endorsement state unknown</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4843"/> + <location filename="mainwindow.cpp" line="4846"/> <source>Start tracking</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4846"/> + <location filename="mainwindow.cpp" line="4849"/> <source>Stop tracking</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4849"/> + <location filename="mainwindow.cpp" line="4852"/> <source>Tracked state unknown</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4884"/> + <location filename="mainwindow.cpp" line="4887"/> <source>Information...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4891"/> - <location filename="mainwindow.cpp" line="6158"/> + <location filename="mainwindow.cpp" line="4894"/> + <location filename="mainwindow.cpp" line="6164"/> <source>Exception: </source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4893"/> - <location filename="mainwindow.cpp" line="6160"/> + <location filename="mainwindow.cpp" line="4896"/> + <location filename="mainwindow.cpp" line="6166"/> <source>Unknown exception</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="4925"/> + <location filename="mainwindow.cpp" line="4928"/> <source>%1 more</source> <translation type="unfinished"></translation> </message> <message numerus="yes"> - <location filename="mainwindow.cpp" line="4929"/> + <location filename="mainwindow.cpp" line="4932"/> <source>Are you sure you want to remove the following %n save(s)?<br><ul>%1</ul><br>Removed saves will be sent to the Recycle Bin.</source> <translation type="unfinished"> <numerusform></numerusform> @@ -3585,12 +3668,12 @@ You can also use online editors and converters instead.</source> </translation> </message> <message> - <location filename="mainwindow.cpp" line="4974"/> + <location filename="mainwindow.cpp" line="4977"/> <source>Enable Mods...</source> <translation type="unfinished"></translation> </message> <message numerus="yes"> - <location filename="mainwindow.cpp" line="4989"/> + <location filename="mainwindow.cpp" line="4992"/> <source>Delete %n save(s)</source> <translation type="unfinished"> <numerusform></numerusform> @@ -3598,242 +3681,227 @@ You can also use online editors and converters instead.</source> </translation> </message> <message> - <location filename="mainwindow.cpp" line="5069"/> + <location filename="mainwindow.cpp" line="5072"/> <source>Restart Mod Organizer</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5071"/> + <location filename="mainwindow.cpp" line="5074"/> <source>Mod Organizer must restart to finish configuration changes</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5073"/> + <location filename="mainwindow.cpp" line="5076"/> <source>Restart</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5074"/> + <location filename="mainwindow.cpp" line="5077"/> <source>Continue</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5074"/> + <location filename="mainwindow.cpp" line="5077"/> <source>Some things might be weird.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5095"/> + <location filename="mainwindow.cpp" line="5098"/> <source>Can't change download directory while downloads are in progress!</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5256"/> - <location filename="mainwindow.cpp" line="6507"/> + <location filename="mainwindow.cpp" line="5259"/> + <location filename="mainwindow.cpp" line="6503"/> <source>Set Priority</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5256"/> + <location filename="mainwindow.cpp" line="5259"/> <source>Set the priority of the selected plugins</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5284"/> + <location filename="mainwindow.cpp" line="5287"/> <source>Update available</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5320"/> + <location filename="mainwindow.cpp" line="5323"/> <source>Do you want to endorse Mod Organizer on %1 now?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5334"/> + <location filename="mainwindow.cpp" line="5337"/> <source>Abstain from Endorsing Mod Organizer</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5335"/> + <location filename="mainwindow.cpp" line="5338"/> <source>Are you sure you want to abstain from endorsing Mod Organizer 2? You will have to visit the mod page on the %1 Nexus site to change your mind.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5430"/> + <location filename="mainwindow.cpp" line="5433"/> <source>Thank you for endorsing MO2! :)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5436"/> + <location filename="mainwindow.cpp" line="5439"/> <source>Please reconsider endorsing MO2 on Nexus!</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5707"/> + <location filename="mainwindow.cpp" line="5710"/> <source>Thank you!</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5707"/> + <location filename="mainwindow.cpp" line="5710"/> <source>Thank you for your endorsement!</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5799"/> + <location filename="mainwindow.cpp" line="5802"/> <source>Mod ID %1 no longer seems to be available on Nexus.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5815"/> + <location filename="mainwindow.cpp" line="5818"/> <source>Request to Nexus failed: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5831"/> - <location filename="mainwindow.cpp" line="5893"/> + <location filename="mainwindow.cpp" line="5834"/> + <location filename="mainwindow.cpp" line="5896"/> <source>failed to read %1: %2</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5843"/> + <location filename="mainwindow.cpp" line="5846"/> <source>Error</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5843"/> + <location filename="mainwindow.cpp" line="5846"/> <source>failed to extract %1 (errorcode %2)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5875"/> + <location filename="mainwindow.cpp" line="5878"/> <source>Extract BSA</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5904"/> + <location filename="mainwindow.cpp" line="5907"/> <source>This archive contains invalid hashes. Some files may be broken.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5918"/> + <location filename="mainwindow.cpp" line="5921"/> <source>Extract...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="5945"/> - <source>This will restart MO, continue?</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="mainwindow.cpp" line="6025"/> + <location filename="mainwindow.cpp" line="6031"/> <source><Multiple></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6089"/> + <location filename="mainwindow.cpp" line="6095"/> <source>Remove '%1' from the toolbar</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6110"/> + <location filename="mainwindow.cpp" line="6116"/> <source>Enable all</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6111"/> + <location filename="mainwindow.cpp" line="6117"/> <source>Disable all</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6132"/> + <location filename="mainwindow.cpp" line="6138"/> <source>Unlock load order</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6135"/> + <location filename="mainwindow.cpp" line="6141"/> <source>Lock load order</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6145"/> + <location filename="mainwindow.cpp" line="6151"/> <source>Open Origin in Explorer</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6150"/> + <location filename="mainwindow.cpp" line="6156"/> <source>Open Origin Info...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6222"/> - <source>Sorting plugins</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="mainwindow.cpp" line="6223"/> - <source>Are you sure you want to sort your plugins list?</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="mainwindow.cpp" line="6268"/> + <location filename="mainwindow.cpp" line="6264"/> <source>Backup of load order created</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6278"/> + <location filename="mainwindow.cpp" line="6274"/> <source>Choose backup to restore</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6291"/> + <location filename="mainwindow.cpp" line="6287"/> <source>No Backups</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6291"/> + <location filename="mainwindow.cpp" line="6287"/> <source>There are no backups to restore</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6316"/> - <location filename="mainwindow.cpp" line="6341"/> + <location filename="mainwindow.cpp" line="6312"/> + <location filename="mainwindow.cpp" line="6337"/> <source>Restore failed</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6317"/> - <location filename="mainwindow.cpp" line="6342"/> + <location filename="mainwindow.cpp" line="6313"/> + <location filename="mainwindow.cpp" line="6338"/> <source>Failed to restore the backup. Errorcode: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6329"/> + <location filename="mainwindow.cpp" line="6325"/> <source>Backup of mod list created</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6412"/> + <location filename="mainwindow.cpp" line="6408"/> <source>A file with the same name has already been downloaded. What would you like to do?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6414"/> + <location filename="mainwindow.cpp" line="6410"/> <source>Overwrite</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6415"/> + <location filename="mainwindow.cpp" line="6411"/> <source>Rename new file</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6416"/> + <location filename="mainwindow.cpp" line="6412"/> <source>Ignore file</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="6507"/> + <location filename="mainwindow.cpp" line="6503"/> <source>Set the priority of the selected mods</source> <translation type="unfinished"></translation> </message> @@ -4701,7 +4769,7 @@ p, li { white-space: pre-wrap; } <context> <name>NoConflictListModel</name> <message> - <location filename="modinfodialogconflictsmodels.cpp" line="320"/> + <location filename="modinfodialogconflictsmodels.cpp" line="272"/> <source>File</source> <translation type="unfinished"></translation> </message> @@ -4709,211 +4777,211 @@ p, li { white-space: pre-wrap; } <context> <name>OrganizerCore</name> <message> - <location filename="organizercore.cpp" line="197"/> + <location filename="organizercore.cpp" line="198"/> <source>File is write protected</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="199"/> + <location filename="organizercore.cpp" line="200"/> <source>Invalid file format (probably a bug)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="201"/> + <location filename="organizercore.cpp" line="202"/> <source>Unknown error %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="205"/> + <location filename="organizercore.cpp" line="206"/> <source>Failed to write settings</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="206"/> + <location filename="organizercore.cpp" line="207"/> <source>An error occurred trying to write back MO settings to %1: %2</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="390"/> - <location filename="organizercore.cpp" line="401"/> + <location filename="organizercore.cpp" line="393"/> + <location filename="organizercore.cpp" line="404"/> <source>Download started</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="404"/> + <location filename="organizercore.cpp" line="407"/> <source>Download failed</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="722"/> - <location filename="organizercore.cpp" line="773"/> + <location filename="organizercore.cpp" line="774"/> + <location filename="organizercore.cpp" line="825"/> <source>Installation cancelled</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="723"/> - <location filename="organizercore.cpp" line="774"/> + <location filename="organizercore.cpp" line="775"/> + <location filename="organizercore.cpp" line="826"/> <source>Another installation is currently in progress.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="735"/> - <location filename="organizercore.cpp" line="803"/> + <location filename="organizercore.cpp" line="787"/> + <location filename="organizercore.cpp" line="855"/> <source>Installation successful</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="743"/> - <location filename="organizercore.cpp" line="813"/> + <location filename="organizercore.cpp" line="795"/> + <location filename="organizercore.cpp" line="865"/> <source>Configure Mod</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="744"/> - <location filename="organizercore.cpp" line="814"/> + <location filename="organizercore.cpp" line="796"/> + <location filename="organizercore.cpp" line="866"/> <source>This mod contains ini tweaks. Do you want to configure them now?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="756"/> - <location filename="organizercore.cpp" line="824"/> + <location filename="organizercore.cpp" line="808"/> + <location filename="organizercore.cpp" line="876"/> <source>mod not found: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="759"/> - <location filename="organizercore.cpp" line="830"/> + <location filename="organizercore.cpp" line="811"/> + <location filename="organizercore.cpp" line="882"/> <source>Extraction cancelled</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="760"/> - <location filename="organizercore.cpp" line="831"/> + <location filename="organizercore.cpp" line="812"/> + <location filename="organizercore.cpp" line="883"/> <source>The installation was cancelled while extracting files. If this was prior to a FOMOD setup, this warning may be ignored. However, if this was during installation, the mod will likely be missing files.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1002"/> + <location filename="organizercore.cpp" line="1054"/> <source>file not found: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1016"/> + <location filename="organizercore.cpp" line="1068"/> <source>failed to generate preview for %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1068"/> + <location filename="organizercore.cpp" line="1120"/> <source>Sorry</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1069"/> + <location filename="organizercore.cpp" line="1121"/> <source>Sorry, can't preview anything. This function currently does not support extracting from bsas.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1079"/> + <location filename="organizercore.cpp" line="1131"/> <source>File '%1' not found.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1087"/> + <location filename="organizercore.cpp" line="1139"/> <source>Failed to generate preview for %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1169"/> + <location filename="organizercore.cpp" line="1221"/> <source>Failed to refresh list of esps: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1280"/> + <location filename="organizercore.cpp" line="1332"/> <source>Multiple esps/esls activated, please check that they don't conflict.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1348"/> + <location filename="organizercore.cpp" line="1400"/> <source>You need to be logged in with Nexus</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1387"/> + <location filename="organizercore.cpp" line="1439"/> <source>Download?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1388"/> + <location filename="organizercore.cpp" line="1440"/> <source>A download has been started but no installed page plugin recognizes it. If you download anyway no information (i.e. version) will be associated with the download. Continue?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1548"/> - <location filename="organizercore.cpp" line="1597"/> + <location filename="organizercore.cpp" line="1600"/> + <location filename="organizercore.cpp" line="1649"/> <source>failed to update mod list: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1604"/> - <location filename="organizercore.cpp" line="1621"/> + <location filename="organizercore.cpp" line="1656"/> + <location filename="organizercore.cpp" line="1673"/> <source>login successful</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1631"/> + <location filename="organizercore.cpp" line="1683"/> <source>Login failed</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1632"/> + <location filename="organizercore.cpp" line="1684"/> <source>Login failed, try again?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1641"/> + <location filename="organizercore.cpp" line="1693"/> <source>login failed: %1. Download will not be associated with an account</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1649"/> + <location filename="organizercore.cpp" line="1701"/> <source>login failed: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1659"/> + <location filename="organizercore.cpp" line="1711"/> <source>login failed: %1. You need to log-in with Nexus to update MO.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1712"/> + <location filename="organizercore.cpp" line="1764"/> <source>MO1 "Script Extender" load mechanism has left hook.dll in your game folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1715"/> - <location filename="organizercore.cpp" line="1731"/> + <location filename="organizercore.cpp" line="1767"/> + <location filename="organizercore.cpp" line="1783"/> <source>Description missing</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1724"/> + <location filename="organizercore.cpp" line="1776"/> <source><a href="%1">hook.dll</a> has been found in your game folder (right click to copy the full path). This is most likely a leftover of setting the ModOrganizer 1 load mechanism to "Script Extender", in which case you must remove this file either by changing the load mechanism in ModOrganizer 1 or manually removing the file, otherwise the game is likely to crash and burn.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1758"/> + <location filename="organizercore.cpp" line="1810"/> <source>failed to save load order: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1839"/> + <location filename="organizercore.cpp" line="1891"/> <source>Error</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="1915"/> + <location filename="organizercore.cpp" line="1967"/> <source>The designated write target "%1" is not enabled.</source> <translation type="unfinished"></translation> </message> @@ -4921,12 +4989,12 @@ Continue?</source> <context> <name>OverwriteConflictListModel</name> <message> - <location filename="modinfodialogconflictsmodels.cpp" line="302"/> + <location filename="modinfodialogconflictsmodels.cpp" line="254"/> <source>File</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialogconflictsmodels.cpp" line="303"/> + <location filename="modinfodialogconflictsmodels.cpp" line="255"/> <source>Overwritten Mods</source> <translation type="unfinished"></translation> </message> @@ -5026,12 +5094,12 @@ Continue?</source> <context> <name>OverwrittenConflictListModel</name> <message> - <location filename="modinfodialogconflictsmodels.cpp" line="311"/> + <location filename="modinfodialogconflictsmodels.cpp" line="263"/> <source>File</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinfodialogconflictsmodels.cpp" line="312"/> + <location filename="modinfodialogconflictsmodels.cpp" line="264"/> <source>Providing Mod</source> <translation type="unfinished"></translation> </message> @@ -5653,9 +5721,10 @@ p, li { white-space: pre-wrap; } <message> <location filename="../../uibase/src/report.cpp" line="39"/> <location filename="../../uibase/src/report.cpp" line="42"/> + <location filename="instancemanager.cpp" line="162"/> + <location filename="loglist.cpp" line="336"/> <location filename="loot.cpp" line="413"/> - <location filename="main.cpp" line="105"/> - <location filename="organizercore.cpp" line="428"/> + <location filename="organizercore.cpp" line="431"/> <location filename="settingsdialogdiagnostics.cpp" line="37"/> <location filename="settingsdialogdiagnostics.cpp" line="61"/> <location filename="settingsdialogpaths.cpp" line="71"/> @@ -5972,137 +6041,134 @@ Destination:<byte value="xd"/> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="83"/> - <source>Deleting folder</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="instancemanager.cpp" line="84"/> - <source>I'm about to delete the following folder: "%1". Proceed?</source> + <location filename="instancemanager.cpp" line="116"/> + <source>Deleting instance folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="108"/> - <source>Choose Instance to Delete</source> + <location filename="instancemanager.cpp" line="117"/> + <source>This will delete the instance folder.</source> + <oldsource>The folder will be deleted: "%1"</oldsource> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="109"/> - <source>Be Careful! Deleting an Instance will remove all your files for that Instance (mods, downloads, profiles, configuration, ...). Custom paths outside of the instance folder for downloads, mods, etc. will be left untoched.</source> + <location filename="instancemanager.cpp" line="120"/> + <source>Move the folder to the recycle bin</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="122"/> - <source>Are you sure?</source> + <location filename="instancemanager.cpp" line="121"/> + <source>Delete the folder permanently</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="123"/> - <source>Are you really sure you want to delete the Instance "%1" with all its files?</source> + <location filename="instancemanager.cpp" line="162"/> + <source>Could not delete instance folder "%1". + +%2</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="127"/> - <source>Failed to delete Instance</source> + <location filename="instancemanager.cpp" line="173"/> + <source>Select an instance to delete</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="128"/> - <source>Could not delete Instance "%1". -If the folder was still in use, restart MO and try again.</source> + <location filename="instancemanager.cpp" line="174"/> + <source>Deleting an instance will delete all the mods, downloads, profiles (including profile-specific saves) and anything in the overwrite folder.<br><br>Custom paths outside of the instance folder will not be deleted.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="143"/> + <location filename="instancemanager.cpp" line="202"/> <source>Enter a Name for the new Instance</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="144"/> + <location filename="instancemanager.cpp" line="203"/> <source>Enter a new name or select one from the suggested list: (This is just a name for the Instance and can be whatever you wish, the actual game selection will happen on the next screen regardless of chosen name)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="155"/> - <location filename="instancemanager.cpp" line="232"/> + <location filename="instancemanager.cpp" line="214"/> + <location filename="instancemanager.cpp" line="291"/> <source>Canceled</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="161"/> + <location filename="instancemanager.cpp" line="220"/> <source>Invalid instance name</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="162"/> + <location filename="instancemanager.cpp" line="221"/> <source>The instance name "%1" is invalid. Use the name "%2" instead?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="177"/> + <location filename="instancemanager.cpp" line="236"/> <source>The instance "%1" already exists.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="178"/> + <location filename="instancemanager.cpp" line="237"/> <source>Please choose a different instance name, like: "%1 1" .</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="201"/> + <location filename="instancemanager.cpp" line="260"/> <source>Choose Instance</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="202"/> + <location filename="instancemanager.cpp" line="261"/> <source>Each Instance is a full set of MO data files (mods, downloads, profiles, configuration, ...). You can use multiple instances for different games. Instances are stored in Appdata and can be accessed by all MO installations. If your MO folder is writable, you can also store a single instance locally (called a Portable install, and all the MO data files will be inside the installation folder).</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="214"/> + <location filename="instancemanager.cpp" line="273"/> <source>New</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="215"/> + <location filename="instancemanager.cpp" line="274"/> <source>Create a new instance.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="219"/> + <location filename="instancemanager.cpp" line="278"/> <source>Portable</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="220"/> + <location filename="instancemanager.cpp" line="279"/> <source>Use MO folder for data.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="224"/> + <location filename="instancemanager.cpp" line="283"/> <source>Manage Instances</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="225"/> + <location filename="instancemanager.cpp" line="284"/> <source>Delete an Instance.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="311"/> + <location filename="instancemanager.cpp" line="384"/> <location filename="profile.cpp" line="69"/> <source>failed to create %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="314"/> + <location filename="instancemanager.cpp" line="387"/> <source>Data directory created</source> <translation type="unfinished"></translation> </message> <message> - <location filename="instancemanager.cpp" line="315"/> + <location filename="instancemanager.cpp" line="388"/> <source>New data directory created at %1. If you don't want to store a lot of data there, reconfigure the storage directories via settings.</source> <translation type="unfinished"></translation> </message> @@ -6194,112 +6260,112 @@ If the folder was still in use, restart MO and try again.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="106"/> - <location filename="organizercore.cpp" line="429"/> + <location filename="loglist.cpp" line="337"/> + <location filename="organizercore.cpp" line="432"/> <source>Failed to create "%1". Your user account probably lacks permission.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="302"/> + <location filename="instancemanager.cpp" line="517"/> <source>Plugin to handle %1 no longer installed. An antivirus might have deleted files.</source> <oldsource>Plugin to handle %1 no longer installed</oldsource> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="317"/> - <location filename="main.cpp" line="359"/> - <location filename="main.cpp" line="375"/> + <location filename="instancemanager.cpp" line="532"/> + <location filename="instancemanager.cpp" line="574"/> + <location filename="instancemanager.cpp" line="590"/> <source>The configured path to the game directory (%1) appears to be a symbolic (or other) link. This setup is incompatible with MO2's VFS and will not run correctly.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="332"/> + <location filename="instancemanager.cpp" line="547"/> <source>Could not use configuration settings for game "%1", path "%2".</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="337"/> - <location filename="main.cpp" line="367"/> - <location filename="main.cpp" line="392"/> + <location filename="instancemanager.cpp" line="552"/> + <location filename="instancemanager.cpp" line="582"/> + <location filename="instancemanager.cpp" line="607"/> <source>Please select the installation of %1 to manage</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="338"/> - <location filename="main.cpp" line="368"/> - <location filename="main.cpp" line="393"/> + <location filename="instancemanager.cpp" line="553"/> + <location filename="instancemanager.cpp" line="583"/> + <location filename="instancemanager.cpp" line="608"/> <source>Please select the game to manage</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="404"/> + <location filename="instancemanager.cpp" line="619"/> <source>Canceled finding %1 in "%2".</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="405"/> + <location filename="instancemanager.cpp" line="620"/> <source>Canceled finding game in "%1".</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="412"/> + <location filename="instancemanager.cpp" line="627"/> <source>%1 not identified in "%2". The directory is required to contain the game binary.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="421"/> + <location filename="instancemanager.cpp" line="636"/> <source>No game identified in "%1". The directory is required to contain the game binary.<br><br><b>These are the games supported by Mod Organizer:</b><ul>%2</ul></source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="631"/> + <location filename="instancemanager.cpp" line="465"/> <source>Please select the game edition you have (MO can't start the game correctly if this is set incorrectly!)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="678"/> + <location filename="main.cpp" line="218"/> <source>failed to start shortcut: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="706"/> + <location filename="main.cpp" line="242"/> <source>failed to start application: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="921"/> + <location filename="main.cpp" line="442"/> <location filename="settingsdialogworkarounds.cpp" line="17"/> <source>Mod Organizer</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="922"/> + <location filename="main.cpp" line="443"/> <source>An instance of Mod Organizer is already running</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="944"/> + <location filename="main.cpp" line="476"/> <source>Failed to set up instance</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="204"/> + <location filename="mainwindow.cpp" line="205"/> <source><Unmanaged></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1376"/> + <location filename="mainwindow.cpp" line="1377"/> <source>Please use "Help" from the toolbar to get usage instructions to all elements</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1798"/> - <location filename="mainwindow.cpp" line="5205"/> + <location filename="mainwindow.cpp" line="1799"/> + <location filename="mainwindow.cpp" line="5208"/> <source><Manage...></source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="1810"/> + <location filename="mainwindow.cpp" line="1811"/> <source>failed to parse profile %1: %2</source> <translation type="unfinished"></translation> </message> @@ -6427,7 +6493,7 @@ If the folder was still in use, restart MO and try again.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="organizercore.cpp" line="445"/> + <location filename="organizercore.cpp" line="448"/> <source>One of the configured MO2 directories (profiles, mods, or overwrite) is on a path containing a symbolic (or other) link. This is likely to be incompatible with MO2's virtual filesystem.</source> <translation type="unfinished"></translation> </message> @@ -6590,6 +6656,7 @@ This program is known to cause issues with Mod Organizer, such as freezing or bl <translation type="unfinished"></translation> </message> <message> + <location filename="instancemanager.cpp" line="122"/> <location filename="settingsdialognexus.cpp" line="342"/> <location filename="settingsdialognexus.cpp" line="353"/> <location filename="settingsdialogworkarounds.cpp" line="128"/> @@ -7903,22 +7970,22 @@ programs you are intentionally running.</source> <context> <name>SingleInstance</name> <message> - <location filename="singleinstance.cpp" line="52"/> + <location filename="singleinstance.cpp" line="46"/> <source>SHM error: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="singleinstance.cpp" line="86"/> + <location filename="singleinstance.cpp" line="81"/> <source>failed to connect to running instance: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="singleinstance.cpp" line="92"/> + <location filename="singleinstance.cpp" line="87"/> <source>failed to communicate with running instance: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="singleinstance.cpp" line="119"/> + <location filename="singleinstance.cpp" line="114"/> <source>failed to receive data from secondary instance: %1</source> <translation type="unfinished"></translation> </message> diff --git a/src/organizercore.cpp b/src/organizercore.cpp index 1c35720b..d01aab96 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -38,6 +38,7 @@ #include "shared/directoryentry.h" #include "shared/filesorigin.h" #include "shared/fileentry.h" +#include "shared/util.h" #include <QApplication> #include <QCoreApplication> @@ -94,12 +95,12 @@ OrganizerCore::OrganizerCore(Settings &settings) , m_PluginContainer(nullptr) , m_CurrentProfile(nullptr) , m_Settings(settings) - , m_Updater(NexusInterface::instance(m_PluginContainer)) + , m_Updater(&NexusInterface::instance()) , m_ModList(m_PluginContainer, this) , m_PluginList(this) , m_DirectoryRefresher(new DirectoryRefresher(settings.refreshThreadCount())) , m_DirectoryStructure(new DirectoryEntry(L"data", nullptr, 0)) - , m_DownloadManager(NexusInterface::instance(m_PluginContainer), this) + , m_DownloadManager(&NexusInterface::instance(), this) , m_DirectoryUpdate(false) , m_ArchivesInit(false) , m_PluginListsWriter(std::bind(&OrganizerCore::savePluginList, this)) @@ -107,8 +108,7 @@ OrganizerCore::OrganizerCore(Settings &settings) env::setHandleCloserThreadCount(settings.refreshThreadCount()); m_DownloadManager.setOutputDirectory(m_Settings.paths().downloads(), false); - NexusInterface::instance(m_PluginContainer)->setCacheDirectory( - m_Settings.paths().cache()); + NexusInterface::instance().setCacheDirectory(m_Settings.paths().cache()); m_InstallationManager.setModsDirectory(m_Settings.paths().mods()); m_InstallationManager.setDownloadDirectory(m_Settings.paths().downloads()); @@ -121,9 +121,9 @@ OrganizerCore::OrganizerCore(Settings &settings) connect(&m_ModList, SIGNAL(removeOrigin(QString)), this, SLOT(removeOrigin(QString))); - connect(NexusInterface::instance(m_PluginContainer)->getAccessManager(), + connect(NexusInterface::instance().getAccessManager(), SIGNAL(validateSuccessful(bool)), this, SLOT(loginSuccessful(bool))); - connect(NexusInterface::instance(m_PluginContainer)->getAccessManager(), + connect(NexusInterface::instance().getAccessManager(), SIGNAL(validateFailed(QString)), this, SLOT(loginFailed(QString))); // This seems awfully imperative @@ -331,8 +331,7 @@ Settings &OrganizerCore::settings() bool OrganizerCore::nexusApi(bool retry) { - NXMAccessManager *accessManager - = NexusInterface::instance(m_PluginContainer)->getAccessManager(); + auto* accessManager = NexusInterface::instance().getAccessManager(); if ((accessManager->validateAttempted() || accessManager->validated()) && !retry) { @@ -340,7 +339,7 @@ bool OrganizerCore::nexusApi(bool retry) return false; } else { QString apiKey; - if (m_Settings.nexus().apiKey(apiKey)) { + if (GlobalSettings::nexusApiKey(apiKey)) { // credentials stored or user entered them manually log::debug("attempt to verify nexus api key"); accessManager->apiCheck(apiKey); @@ -395,7 +394,9 @@ void OrganizerCore::profileRemoved(QString const& profileName) void OrganizerCore::externalMessage(const QString &message) { - if (MOShortcut moshortcut{ message } ) { + MOShortcut moshortcut(message); + + if (moshortcut.isValid()) { if(moshortcut.hasExecutable()) { processRunner() .setFromShortcut(moshortcut) @@ -470,21 +471,53 @@ bool OrganizerCore::checkPathSymlinks() { return true; } -bool OrganizerCore::bootstrap() { - return createDirectory(m_Settings.paths().profiles()) && - createDirectory(m_Settings.paths().mods()) && - createDirectory(m_Settings.paths().downloads()) && - createDirectory(m_Settings.paths().overwrite()) && - createDirectory(QString::fromStdWString(crashDumpsPath())) && - checkPathSymlinks() && cycleDiagnostics(); +bool OrganizerCore::bootstrap() +{ + const auto dirs = { + m_Settings.paths().profiles(), + m_Settings.paths().mods(), + m_Settings.paths().downloads(), + m_Settings.paths().overwrite(), + QString::fromStdWString(crashDumpsPath()) + }; + + for (auto&& dir : dirs) { + if (!createDirectory(dir)) { + return false; + } + } + + if (!checkPathSymlinks()) { + return false; + } + + if (!cycleDiagnostics()) { + return false; + } + + // log if there are any dmp files + const auto hasCrashDumps = + !QDir(QString::fromStdWString(crashDumpsPath())) + .entryList({"*.dmp"}, QDir::Files) + .empty(); + + if (hasCrashDumps) { + log::debug( + "there are crash dumps in '{}'", + QString::fromStdWString(crashDumpsPath())); + } + + return true; } void OrganizerCore::createDefaultProfile() { QString profilesPath = settings().paths().profiles(); - if (QDir(profilesPath).entryList(QDir::AllDirs | QDir::NoDotAndDotDot).size() - == 0) { - Profile newProf("Default", managedGame(), false); + if (QDir(profilesPath).entryList(QDir::AllDirs | QDir::NoDotAndDotDot).size() == 0) { + Profile newProf( + QString::fromStdWString(AppConfig::defaultProfileName()), + managedGame(), false); + m_ProfileCreated(&newProf); } } @@ -552,15 +585,34 @@ void OrganizerCore::setCurrentProfile(const QString &profileName) log::debug("selecting profile '{}'", profileName); QDir profileBaseDir(settings().paths().profiles()); - QString profileDir = profileBaseDir.absoluteFilePath(profileName); - if (!QDir(profileDir).exists()) { + const auto subdirs = profileBaseDir.entryList( + QDir::AllDirs | QDir::NoDotAndDotDot); + + QString profileDir; + + // the profile name may not have the correct case, which breaks other parts + // of the ui like the profile combobox, which walks directories on its own + // + // find the real name with the correct case by walking the directories + for (auto&& dirName : subdirs) { + if (QString::compare(dirName, profileName, Qt::CaseInsensitive) == 0) { + profileDir = profileBaseDir.absoluteFilePath(dirName); + break; + } + } + + if (profileDir.isEmpty()) { + log::error("profile '{}' does not exist", profileName); + // selected profile doesn't exist. Ensure there is at least one profile, // then pick any one createDefaultProfile(); profileDir = profileBaseDir.absoluteFilePath( profileBaseDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot).at(0)); + + log::error("picked profile '{}' instead", QDir(profileDir).dirName()); } // Keep the old profile to emit signal-changed: @@ -706,7 +758,7 @@ void OrganizerCore::setPersistent(const QString &pluginName, const QString &key, m_Settings.plugins().setPersistent(pluginName, key, value, sync); } -QString OrganizerCore::pluginDataPath() const +QString OrganizerCore::pluginDataPath() { return qApp->applicationDirPath() + "/" + ToQString(AppConfig::pluginPath()) + "/data"; @@ -1372,13 +1424,13 @@ void OrganizerCore::updateModsInDirectoryStructure(QMap<unsigned int, ModInfo::P void OrganizerCore::loggedInAction(QWidget* parent, std::function<void ()> f) { - if (NexusInterface::instance(m_PluginContainer)->getAccessManager()->validated()) { + if (NexusInterface::instance().getAccessManager()->validated()) { f(); } else { QString apiKey; - if (settings().nexus().apiKey(apiKey)) { + if (GlobalSettings::nexusApiKey(apiKey)) { doAfterLogin([f]{ f(); }); - NexusInterface::instance(m_PluginContainer)->getAccessManager()->apiCheck(apiKey); + NexusInterface::instance().getAccessManager()->apiCheck(apiKey); } else { MessageDialog::showMessage(tr("You need to be logged in with Nexus"), parent); } @@ -1652,7 +1704,7 @@ void OrganizerCore::loginSuccessful(bool necessary) } m_PostLoginTasks.clear(); - NexusInterface::instance(m_PluginContainer)->loginCompleted(); + NexusInterface::instance().loginCompleted(); } void OrganizerCore::loginSuccessfulUpdate(bool necessary) @@ -1690,7 +1742,7 @@ void OrganizerCore::loginFailed(const QString &message) qApp->activeWindow()); m_PostLoginTasks.clear(); } - NexusInterface::instance(m_PluginContainer)->loginCompleted(); + NexusInterface::instance().loginCompleted(); } void OrganizerCore::loginFailedUpdate(const QString &message) diff --git a/src/organizercore.h b/src/organizercore.h index 70ce94f5..80b89a43 100644 --- a/src/organizercore.h +++ b/src/organizercore.h @@ -191,9 +191,6 @@ public: };
public:
-
- static bool isNxmLink(const QString &link) { return link.startsWith("nxm://", Qt::CaseInsensitive); }
-
OrganizerCore(Settings &settings);
~OrganizerCore();
@@ -317,7 +314,7 @@ public: void setPluginSetting(const QString &pluginName, const QString &key, const QVariant &value);
QVariant persistent(const QString &pluginName, const QString &key, const QVariant &def) const;
void setPersistent(const QString &pluginName, const QString &key, const QVariant &value, bool sync);
- QString pluginDataPath() const;
+ static QString pluginDataPath();
virtual MOBase::IModInterface *installMod(const QString &fileName, bool reinstallation, ModInfo::Ptr currentMod, const QString &initModName);
QString resolvePath(const QString &fileName) const;
QStringList listDirectories(const QString &directoryName) const;
diff --git a/src/organizerproxy.cpp b/src/organizerproxy.cpp index 45efc00c..a988ba9f 100644 --- a/src/organizerproxy.cpp +++ b/src/organizerproxy.cpp @@ -9,6 +9,7 @@ #include "modlistproxy.h"
#include "pluginlistproxy.h"
#include "proxyutils.h"
+#include "shared/util.h"
#include <QObject>
#include <QApplication>
@@ -104,7 +105,7 @@ void OrganizerProxy::setPersistent(const QString &pluginName, const QString &key QString OrganizerProxy::pluginDataPath() const
{
- return m_Proxied->pluginDataPath();
+ return OrganizerCore::pluginDataPath();
}
HANDLE OrganizerProxy::startApplication(
@@ -48,7 +48,7 @@ #include <boost/algorithm/string.hpp> #include <boost/algorithm/string/predicate.hpp> #include <boost/assign.hpp> -#include <boost/bind.hpp> +#include <boost/bind/bind.hpp> #include <boost/function.hpp> #include <boost/fusion/algorithm/iteration/for_each.hpp> #include <boost/fusion/container.hpp> @@ -64,6 +64,7 @@ #include <boost/thread.hpp> #include <boost/uuid/uuid_generators.hpp> #include <boost/uuid/uuid_io.hpp> +#include <boost/program_options.hpp> // openssl #include <tlhelp32.h> diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp index 4771359d..a571a0aa 100644 --- a/src/plugincontainer.cpp +++ b/src/plugincontainer.cpp @@ -169,14 +169,49 @@ QObject* PluginContainer::as_qobject(MOBase::IPlugin* plugin) const return *it;
}
-bool PluginContainer::verifyPlugin(IPlugin *plugin)
+bool PluginContainer::initPlugin(IPlugin *plugin)
{
+ // when MO has no instance loaded, init() is not called on plugins, except
+ // for proxy plugins, where init() is called with a null IOrganizer
+ //
+ // after proxies are initialized, instantiate() is called for all the plugins
+ // they've discovered, but as for regular plugins, init() won't be
+ // called on them if m_OrganizerCore is null
+
+ if (plugin == nullptr) {
+ return false;
+ }
+
+ if (m_Organizer) {
+ auto* proxy = new OrganizerProxy(m_Organizer, this, plugin);
+
+ if (!plugin->init(proxy)) {
+ log::warn("plugin failed to initialize");
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool PluginContainer::initProxyPlugin(IPlugin *plugin)
+{
+ // see initPlugin() above for info
+
if (plugin == nullptr) {
return false;
- } else if (!plugin->init(new OrganizerProxy(m_Organizer, this, plugin))) {
- log::warn("plugin failed to initialize");
+ }
+
+ IOrganizer* proxy = nullptr;
+ if (m_Organizer) {
+ proxy = new OrganizerProxy(m_Organizer, this, plugin);
+ }
+
+ if (!plugin->init(proxy)) {
+ log::warn("proxy plugin failed to initialize");
return false;
}
+
return true;
}
@@ -186,7 +221,6 @@ void PluginContainer::registerGame(IPluginGame *game) m_SupportedGames.insert({ game->gameName(), game });
}
-
bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName)
{
// Storing the original QObject* is a bit of a hack as I couldn't figure out any
@@ -199,8 +233,14 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName) log::debug("not an IPlugin");
return false;
}
+
plugin->setProperty("filename", fileName);
- m_Organizer->settings().plugins().registerPlugin(pluginObj);
+
+ if (m_Organizer) {
+ m_Organizer->settings().plugins().registerPlugin(pluginObj);
+ }
+
+ pluginObj->registered();
}
{ // diagnosis plugin
@@ -220,14 +260,14 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName) }
{ // mod page plugin
IPluginModPage *modPage = qobject_cast<IPluginModPage*>(plugin);
- if (verifyPlugin(modPage)) {
+ if (initPlugin(modPage)) {
bf::at_key<IPluginModPage>(m_Plugins).push_back(modPage);
return true;
}
}
{ // game plugin
IPluginGame *game = qobject_cast<IPluginGame*>(plugin);
- if (verifyPlugin(game)) {
+ if (initPlugin(game)) {
bf::at_key<IPluginGame>(m_Plugins).push_back(game);
registerGame(game);
return true;
@@ -235,22 +275,24 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName) }
{ // tool plugins
IPluginTool *tool = qobject_cast<IPluginTool*>(plugin);
- if (verifyPlugin(tool)) {
+ if (initPlugin(tool)) {
bf::at_key<IPluginTool>(m_Plugins).push_back(tool);
return true;
}
}
{ // installer plugins
IPluginInstaller *installer = qobject_cast<IPluginInstaller*>(plugin);
- if (verifyPlugin(installer)) {
+ if (initPlugin(installer)) {
bf::at_key<IPluginInstaller>(m_Plugins).push_back(installer);
- m_Organizer->installationManager()->registerInstaller(installer);
+ if (m_Organizer) {
+ m_Organizer->installationManager()->registerInstaller(installer);
+ }
return true;
}
}
{ // preview plugins
IPluginPreview *preview = qobject_cast<IPluginPreview*>(plugin);
- if (verifyPlugin(preview)) {
+ if (initPlugin(preview)) {
bf::at_key<IPluginPreview>(m_Plugins).push_back(preview);
m_PreviewGenerator.registerPlugin(preview);
return true;
@@ -258,7 +300,7 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName) }
{ // proxy plugins
IPluginProxy *proxy = qobject_cast<IPluginProxy*>(plugin);
- if (verifyPlugin(proxy)) {
+ if (initProxyPlugin(proxy)) {
bf::at_key<IPluginProxy>(m_Plugins).push_back(proxy);
QStringList pluginNames = proxy->pluginList(
QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::pluginPath()));
@@ -290,7 +332,7 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName) { // dummy plugins
// only initialize these, no processing otherwise
IPlugin *dummy = qobject_cast<IPlugin*>(plugin);
- if (verifyPlugin(dummy)) {
+ if (initPlugin(dummy)) {
bf::at_key<IPlugin>(m_Plugins).push_back(dummy);
return true;
}
@@ -317,7 +359,7 @@ void PluginContainer::unloadPlugins() }
// disconnect all slots before unloading plugins so plugins don't have to take care of that
- if (m_Organizer != nullptr) {
+ if (m_Organizer) {
m_Organizer->disconnectPlugins();
}
@@ -363,24 +405,28 @@ void PluginContainer::loadPlugins() registerPlugin(plugin, "");
}
- QFile loadCheck(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);
+ QFile loadCheck;
+
+ 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);
+ }
+ loadCheck.close();
}
- loadCheck.close();
- }
- loadCheck.open(QIODevice::WriteOnly);
+ loadCheck.open(QIODevice::WriteOnly);
+ }
QString pluginPath = qApp->applicationDirPath() + "/" + ToQString(AppConfig::pluginPath());
log::debug("looking for plugins in {}", QDir::toNativeSeparators(pluginPath));
@@ -388,13 +434,20 @@ void PluginContainer::loadPlugins() while (iter.hasNext()) {
iter.next();
- if (m_Organizer->settings().plugins().blacklisted(iter.fileName())) {
- log::debug("plugin \"{}\" blacklisted", iter.fileName());
- continue;
+
+ if (m_Organizer) {
+ if (m_Organizer->settings().plugins().blacklisted(iter.fileName())) {
+ log::debug("plugin \"{}\" blacklisted", iter.fileName());
+ continue;
+ }
}
- loadCheck.write(iter.fileName().toUtf8());
- loadCheck.write("\n");
- loadCheck.flush();
+
+ if (loadCheck.isOpen()) {
+ loadCheck.write(iter.fileName().toUtf8());
+ loadCheck.write("\n");
+ loadCheck.flush();
+ }
+
QString pluginName = iter.filePath();
if (QLibrary::isLibrary(pluginName)) {
std::unique_ptr<QPluginLoader> pluginLoader(new QPluginLoader(pluginName, this));
@@ -416,12 +469,16 @@ void PluginContainer::loadPlugins() }
// remove the load check file on success
- loadCheck.remove();
+ if (loadCheck.isOpen()) {
+ loadCheck.remove();
+ }
- bf::at_key<IPluginDiagnose>(m_Plugins).push_back(m_Organizer);
bf::at_key<IPluginDiagnose>(m_Plugins).push_back(this);
- m_Organizer->connectPlugins(this);
+ if (m_Organizer) {
+ bf::at_key<IPluginDiagnose>(m_Plugins).push_back(m_Organizer);
+ m_Organizer->connectPlugins(this);
+ }
}
diff --git a/src/plugincontainer.h b/src/plugincontainer.h index 07363ee7..2b39726b 100644 --- a/src/plugincontainer.h +++ b/src/plugincontainer.h @@ -168,10 +168,10 @@ private: QObject* as_qobject(MOBase::IPlugin* plugin) const;
- bool verifyPlugin(MOBase::IPlugin *plugin);
+ bool initPlugin(MOBase::IPlugin *plugin);
+ bool initProxyPlugin(MOBase::IPlugin *plugin);
void registerGame(MOBase::IPluginGame *game);
bool registerPlugin(QObject *pluginObj, const QString &fileName);
- bool unregisterPlugin(QObject *pluginObj, const QString &fileName);
OrganizerCore *m_Organizer;
diff --git a/src/pluginlist.cpp b/src/pluginlist.cpp index a4c1ec6d..8d04b592 100644 --- a/src/pluginlist.cpp +++ b/src/pluginlist.cpp @@ -801,7 +801,7 @@ bool PluginList::setPriority(const QString& name, int newPriority) { int rowIndex = findPluginByPriority(oldPriority);
- // We need to increment newPriority if its above the old one, otherwise the
+ // We need to increment newPriority if its above the old one, otherwise the
// plugin is place right below the new priority.
if (oldPriority < newPriority) {
newPriority += 1;
diff --git a/src/pluginlistview.cpp b/src/pluginlistview.cpp index 4217971d..a265d5d4 100644 --- a/src/pluginlistview.cpp +++ b/src/pluginlistview.cpp @@ -56,5 +56,3 @@ void PluginListView::setModel(QAbstractItemModel *model) QTreeView::setModel(model); setVerticalScrollBar(new ViewMarkingScrollBar(model, this)); } - -#pragma once diff --git a/src/processrunner.cpp b/src/processrunner.cpp index a0e74f47..bc4e6227 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -586,13 +586,16 @@ ProcessRunner& ProcessRunner::setFromExecutable(const Executable& exe) ProcessRunner& ProcessRunner::setFromShortcut(const MOShortcut& shortcut) { - const auto currentInstance = InstanceManager::instance().currentInstance(); + const auto currentInstance = InstanceManager::singleton().currentInstance(); - if (shortcut.hasInstance() && shortcut.instance() != currentInstance) { - throw std::runtime_error( - QString("Refusing to run executable from different instance %1:%2") - .arg(shortcut.instance(),shortcut.executable()) - .toLocal8Bit().constData()); + if (currentInstance) + { + if (shortcut.hasInstance() && shortcut.instance() != currentInstance->name()) { + throw std::runtime_error( + QString("Refusing to run executable from different instance %1:%2") + .arg(shortcut.instance(),shortcut.executable()) + .toLocal8Bit().constData()); + } } const Executable& exe = m_core.executablesList()->get(shortcut.executable()); diff --git a/src/selfupdater.cpp b/src/selfupdater.cpp index 904dbd83..c0f3b005 100644 --- a/src/selfupdater.cpp +++ b/src/selfupdater.cpp @@ -56,7 +56,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <QtDebug>
#include <QtAlgorithms>
-#include <boost/bind.hpp>
+#include <boost/bind/bind.hpp>
#include <Windows.h> //for VS_FIXEDFILEINFO, GetLastError
diff --git a/src/settings.cpp b/src/settings.cpp index 534e67c8..99b41e1f 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -61,24 +61,29 @@ QString toString(EndorsementState s) Settings *Settings::s_Instance = nullptr; -Settings::Settings(const QString& path) : +Settings::Settings(const QString& path, bool globalInstance) : m_Settings(path, QSettings::IniFormat), - m_Game(m_Settings), m_Geometry(m_Settings), m_Widgets(m_Settings), - m_Colors(m_Settings), m_Plugins(m_Settings), m_Paths(m_Settings), - m_Network(m_Settings), m_Nexus(*this, m_Settings), m_Steam(*this, m_Settings), + m_Game(m_Settings), m_Geometry(m_Settings), + m_Widgets(m_Settings, globalInstance), m_Colors(m_Settings), + m_Plugins(m_Settings), m_Paths(m_Settings), m_Network(m_Settings), + m_Nexus(*this, m_Settings), m_Steam(*this, m_Settings), m_Interface(m_Settings), m_Diagnostics(m_Settings) { - if (s_Instance != nullptr) { - throw std::runtime_error("second instance of \"Settings\" created"); - } else { - s_Instance = this; + if (globalInstance) { + if (s_Instance != nullptr) { + throw std::runtime_error("second instance of \"Settings\" created"); + } else { + s_Instance = this; + } } } Settings::~Settings() { - MOBase::QuestionBoxMemory::setCallbacks({}, {}, {}); - s_Instance = nullptr; + if (s_Instance == this) { + MOBase::QuestionBoxMemory::setCallbacks({}, {}, {}); + s_Instance = nullptr; + } } Settings &Settings::instance() @@ -460,6 +465,11 @@ QSettings::Status Settings::sync() const return m_Settings.status(); } +QSettings::Status Settings::iniStatus() const +{ + return m_Settings.status(); +} + void Settings::dump() const { static const QStringList ignore({ @@ -876,7 +886,7 @@ void GeometrySettings::setCenterDialogs(bool b) set(m_Settings, "Settings", "center_dialogs", b); } -void GeometrySettings::centerOnMainWindowMonitor(QWidget* w) +void GeometrySettings::centerOnMainWindowMonitor(QWidget* w) const { const auto monitor = getOptional<int>( m_Settings, "Geometry", "MainWindow_monitor").value_or(-1); @@ -1004,13 +1014,15 @@ void GeometrySettings::restoreDocks(QMainWindow* mw) const } -WidgetSettings::WidgetSettings(QSettings& s) +WidgetSettings::WidgetSettings(QSettings& s, bool globalInstance) : m_Settings(s) { - MOBase::QuestionBoxMemory::setCallbacks( - [this](auto&& w, auto&& f){ return questionButton(w, f); }, - [this](auto&& w, auto&& b){ setQuestionWindowButton(w, b); }, - [this](auto&& w, auto&& f, auto&& b){ setQuestionFileButton(w, f, b); }); + if (globalInstance) { + MOBase::QuestionBoxMemory::setCallbacks( + [this](auto&& w, auto&& f){ return questionButton(w, f); }, + [this](auto&& w, auto&& b){ setQuestionWindowButton(w, b); }, + [this](auto&& w, auto&& f, auto&& b){ setQuestionFileButton(w, f, b); }); + } } std::optional<int> WidgetSettings::index(const QComboBox* cb) const @@ -1457,6 +1469,8 @@ QSet<QString> PluginSettings::readBlacklist() const } +const QString PathSettings::BaseDirVariable = "%BASE_DIR%"; + PathSettings::PathSettings(QSettings& settings) : m_Settings(settings) { @@ -1504,10 +1518,10 @@ QString PathSettings::getConfigurablePath(const QString &key, bool resolve) const { QString result = QDir::fromNativeSeparators( - get<QString>(m_Settings, "Settings", key, QString("%BASE_DIR%/") + def)); + get<QString>(m_Settings, "Settings", key, makeDefaultPath(def))); if (resolve) { - result.replace("%BASE_DIR%", base()); + result = PathSettings::resolve(result, base()); } return result; @@ -1522,10 +1536,24 @@ void PathSettings::setConfigurablePath(const QString &key, const QString& path) } } +QString PathSettings::resolve(const QString& path, const QString& baseDir) +{ + QString s = path; + s.replace(BaseDirVariable, baseDir); + return s; +} + +QString PathSettings::makeDefaultPath(const QString dirName) +{ + return BaseDirVariable + "/" + dirName; +} + QString PathSettings::base() const { + const QString dataPath = QFileInfo(m_Settings.fileName()).dir().path(); + return QDir::fromNativeSeparators(get<QString>(m_Settings, - "Settings", "base_directory", qApp->property("dataPath").toString())); + "Settings", "base_directory", dataPath)); } QString PathSettings::downloads(bool resolve) const @@ -1789,37 +1817,6 @@ NexusSettings::NexusSettings(Settings& parent, QSettings& settings) { } -bool NexusSettings::apiKey(QString& apiKey) const -{ - QString tempKey = getWindowsCredential("APIKEY"); - if (tempKey.isEmpty()) - return false; - - apiKey = tempKey; - return true; -} - -bool NexusSettings::setApiKey(const QString& apiKey) -{ - if (!setWindowsCredential("APIKEY", apiKey)) { - const auto e = GetLastError(); - log::error("Storing API key failed: {}", formatSystemMessage(e)); - return false; - } - - return true; -} - -bool NexusSettings::clearApiKey() -{ - return setApiKey(""); -} - -bool NexusSettings::hasApiKey() const -{ - return !getWindowsCredential("APIKEY").isEmpty(); -} - bool NexusSettings::endorsementIntegration() const { return get<bool>(m_Settings, "Settings", "endorsement_integration", true); @@ -2144,3 +2141,96 @@ void DiagnosticsSettings::setSpawnDelay(std::chrono::seconds t) { set(m_Settings, "Settings", "spawn_delay", t.count()); } + + +void GlobalSettings::updateRegistryKey() +{ + const QString OldOrganization = "Tannin"; + const QString OldApplication = "Mod Organizer"; + const QString OldInstanceValue = "CurrentInstance"; + + const QString OldRootKey = "Software\\" + OldOrganization; + + if (env::registryValueExists(OldRootKey + "\\" + OldApplication, OldInstanceValue)) { + QSettings old(OldOrganization, OldApplication); + setCurrentInstance(old.value(OldInstanceValue).toString()); + old.remove(OldInstanceValue); + } + + env::deleteRegistryKeyIfEmpty(OldRootKey); +} + +QString GlobalSettings::currentInstance() +{ + return settings().value("CurrentInstance", "").toString(); +} + +void GlobalSettings::setCurrentInstance(const QString& s) +{ + settings().setValue("CurrentInstance", s); +} + +QSettings GlobalSettings::settings() +{ + const QString Organization = "Mod Organizer Team"; + const QString Application = "Mod Organizer"; + + return QSettings(Organization, Application); +} + +bool GlobalSettings::hideCreateInstanceIntro() +{ + return settings().value("HideCreateInstanceIntro", false).toBool(); +} + +void GlobalSettings::setHideCreateInstanceIntro(bool b) +{ + settings().setValue("HideCreateInstanceIntro", b); +} + +bool GlobalSettings::hideTutorialQuestion() +{ + return settings().value("HideTutorialQuestion", false).toBool(); +} + +void GlobalSettings::setHideTutorialQuestion(bool b) +{ + settings().setValue("HideTutorialQuestion", b); +} + +bool GlobalSettings::nexusApiKey(QString& apiKey) +{ + QString tempKey = getWindowsCredential("APIKEY"); + if (tempKey.isEmpty()) + return false; + + apiKey = tempKey; + return true; +} + +bool GlobalSettings::setNexusApiKey(const QString& apiKey) +{ + if (!setWindowsCredential("APIKEY", apiKey)) { + const auto e = GetLastError(); + log::error("Storing API key failed: {}", formatSystemMessage(e)); + return false; + } + + return true; +} + +bool GlobalSettings::clearNexusApiKey() +{ + return setNexusApiKey(""); +} + +bool GlobalSettings::hasNexusApiKey() +{ + return !getWindowsCredential("APIKEY").isEmpty(); +} + +void GlobalSettings::resetDialogs() +{ + setHideCreateInstanceIntro(false); + setHideTutorialQuestion(false); +} diff --git a/src/settings.h b/src/settings.h index 636719bb..c8325ba2 100644 --- a/src/settings.h +++ b/src/settings.h @@ -169,7 +169,7 @@ public: // assumes the given widget is a top-level // - void centerOnMainWindowMonitor(QWidget* w); + void centerOnMainWindowMonitor(QWidget* w) const; // saves the monitor number of the given window // @@ -195,7 +195,7 @@ private: class WidgetSettings { public: - WidgetSettings(QSettings& s); + WidgetSettings(QSettings& s, bool globalInstance); // selected index for a combobox // @@ -391,6 +391,9 @@ private: class PathSettings { public: + // %BASE_DIR% + static const QString BaseDirVariable; + PathSettings(QSettings& settings); QString base() const; @@ -418,6 +421,15 @@ public: std::map<QString, QString> recent() const; void setRecent(const std::map<QString, QString>& map); + + // resolves %BASE_DIR% + // + static QString resolve(const QString& path, const QString& baseDir); + + // returns %BASE_DIR%/dirName + // + static QString makeDefaultPath(const QString dirName); + private: QSettings& m_Settings; @@ -486,24 +498,6 @@ class NexusSettings public: NexusSettings(Settings& parent, QSettings& settings); - // if the key exists from the credentials store, puts it in `apiKey` and - // returns true; otherwise, returns false and leaves `apiKey` untouched - // - bool apiKey(QString& apiKey) const; - - // sets the api key in the credentials store, removes it if empty; returns - // false on errors - // - bool setApiKey(const QString& apiKey); - - // removes the api key from the credentials store; returns false on errors - // - bool clearApiKey(); - - // returns whether an API key is currently stored - // - bool hasApiKey() const; - // returns whether endorsement integration is enabled // bool endorsementIntegration() const; @@ -677,7 +671,7 @@ class Settings : public QObject Q_OBJECT; public: - Settings(const QString& path); + Settings(const QString& path, bool globalInstance=false); ~Settings(); static Settings &instance(); @@ -784,6 +778,10 @@ public: // QSettings::Status sync() const; + // last status of the ini file + // + QSettings::Status iniStatus() const; + void dump() const; public slots: @@ -815,6 +813,49 @@ private: }; +// manages global settings in the registry +// +class GlobalSettings +{ +public: + // migrates the old settings from the Tannin key to the new one + static void updateRegistryKey(); + + static QString currentInstance(); + static void setCurrentInstance(const QString& s); + + static bool hideCreateInstanceIntro(); + static void setHideCreateInstanceIntro(bool b); + + static bool hideTutorialQuestion(); + static void setHideTutorialQuestion(bool b); + + // if the key exists from the credentials store, puts it in `apiKey` and + // returns true; otherwise, returns false and leaves `apiKey` untouched + // + static bool nexusApiKey(QString& apiKey); + + // sets the api key in the credentials store, removes it if empty; returns + // false on errors + // + static bool setNexusApiKey(const QString& apiKey); + + // removes the api key from the credentials store; returns false on errors + // + static bool clearNexusApiKey(); + + // returns whether an API key is currently stored + // + static bool hasNexusApiKey(); + + // resets anything that the user can disable + static void resetDialogs(); + +private: + static QSettings settings(); +}; + + // helper class that calls restoreGeometry() in the constructor and // saveGeometry() in the destructor // diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp index 87c7201d..c4a53fcd 100644 --- a/src/settingsdialog.cpp +++ b/src/settingsdialog.cpp @@ -114,7 +114,7 @@ QString SettingsDialog::getColoredButtonStyleSheet() const void SettingsDialog::accept() { QString newModPath = ui->modDirEdit->text(); - newModPath.replace("%BASE_DIR%", ui->baseDirEdit->text()); + newModPath = PathSettings::resolve(newModPath, ui->baseDirEdit->text()); if ((QDir::fromNativeSeparators(newModPath) != QDir::fromNativeSeparators( diff --git a/src/settingsdialoggeneral.cpp b/src/settingsdialoggeneral.cpp index 072764c9..87489ab4 100644 --- a/src/settingsdialoggeneral.cpp +++ b/src/settingsdialoggeneral.cpp @@ -177,6 +177,7 @@ void GeneralSettingsTab::selectStyle() void GeneralSettingsTab::resetDialogs() { settings().widgets().resetQuestionButtons(); + GlobalSettings::resetDialogs(); } void GeneralSettingsTab::onExploreStyles() diff --git a/src/settingsdialognexus.cpp b/src/settingsdialognexus.cpp index d49e0a33..1ed4c58f 100644 --- a/src/settingsdialognexus.cpp +++ b/src/settingsdialognexus.cpp @@ -71,112 +71,42 @@ private: }; -NexusSettingsTab::NexusSettingsTab(Settings& s, SettingsDialog& d) - : SettingsTab(s, d) +NexusConnectionUI::NexusConnectionUI( + QWidget* parent, + Settings* s, + QAbstractButton* connectButton, + QAbstractButton* disconnectButton, + QAbstractButton* manualButton, + QListWidget* logList) : + m_parent(parent), + m_settings(s), + m_connect(connectButton), + m_disconnect(disconnectButton), + m_manual(manualButton), + m_log(logList) { - ui->offlineBox->setChecked(settings().network().offlineMode()); - ui->proxyBox->setChecked(settings().network().useProxy()); - ui->endorsementBox->setChecked(settings().nexus().endorsementIntegration()); - ui->hideAPICounterBox->setChecked(settings().interface().hideAPICounter()); - - // display server preferences - for (const auto& server : s.network().servers()) { - QString descriptor = server.name(); - - if (!descriptor.compare("CDN", Qt::CaseInsensitive)) { - descriptor += QStringLiteral(" (automatic)"); - } - - const auto averageSpeed = server.averageSpeed(); - if (averageSpeed > 0) { - descriptor += QString(" (%1)").arg(MOBase::localizedByteSpeed(averageSpeed)); - } - - QListWidgetItem *newItem = new ServerItem<int>(descriptor, Qt::UserRole + 1); - - newItem->setData(Qt::UserRole, server.name()); - newItem->setData(Qt::UserRole + 1, server.preferred()); - - if (server.preferred() > 0) { - ui->preferredServersList->addItem(newItem); - } else { - ui->knownServersList->addItem(newItem); - } - - ui->preferredServersList->sortItems(Qt::DescendingOrder); + if (m_connect) { + QObject::connect(m_connect, &QPushButton::clicked, [&]{ connect(); }); } - QObject::connect(ui->nexusConnect, &QPushButton::clicked, [&]{ on_nexusConnect_clicked(); }); - QObject::connect(ui->nexusManualKey, &QPushButton::clicked, [&]{ on_nexusManualKey_clicked(); }); - QObject::connect(ui->nexusDisconnect, &QPushButton::clicked, [&]{ on_nexusDisconnect_clicked(); }); - QObject::connect(ui->clearCacheButton, &QPushButton::clicked, [&]{ on_clearCacheButton_clicked(); }); - QObject::connect(ui->associateButton, &QPushButton::clicked, [&]{ on_associateButton_clicked(); }); - - if (settings().nexus().hasApiKey()) { - addNexusLog(QObject::tr("Connected.")); - } else { - addNexusLog(QObject::tr("Not connected.")); + if (m_disconnect) { + QObject::connect(m_disconnect, &QPushButton::clicked, [&]{ disconnect(); }); } - updateNexusState(); -} - -void NexusSettingsTab::update() -{ - settings().network().setOfflineMode(ui->offlineBox->isChecked()); - settings().network().setUseProxy(ui->proxyBox->isChecked()); - settings().nexus().setEndorsementIntegration(ui->endorsementBox->isChecked()); - settings().interface().setHideAPICounter(ui->hideAPICounterBox->isChecked()); - - auto servers = settings().network().servers(); - - // store server preference - for (int i = 0; i < ui->knownServersList->count(); ++i) { - const QString key = ui->knownServersList->item(i)->data(Qt::UserRole).toString(); - - bool found = false; - - for (auto& server : servers) { - if (server.name() == key) { - server.setPreferred(0); - found = true; - break; - } - } - - if (!found) { - log::error("while setting preferred to 0, server '{}' not found", key); - } + if (m_manual) { + QObject::connect(manualButton, &QPushButton::clicked, [&]{ manual(); }); } - const int count = ui->preferredServersList->count(); - - for (int i = 0; i < count; ++i) { - const QString key = ui->preferredServersList->item(i)->data(Qt::UserRole).toString(); - const int newPreferred = count - i; - - bool found = false; - - for (auto& server : servers) { - - if (server.name() == key) { - server.setPreferred(newPreferred); - found = true; - break; - } - } - - if (!found) { - log::error( - "while setting preference to {}, server '{}' not found", - newPreferred, key); - } + if (GlobalSettings::hasNexusApiKey()) { + addLog(tr("Connected.")); + } else { + addLog(tr("Not connected.")); } - settings().network().updateServers(servers); + updateState(); } -void NexusSettingsTab::on_nexusConnect_clicked() +void NexusConnectionUI::connect() { if (m_nexusLogin && m_nexusLogin->isActive()) { m_nexusLogin->cancel(); @@ -195,19 +125,19 @@ void NexusSettingsTab::on_nexusConnect_clicked() }; } - ui->nexusLog->clear(); + m_log->clear(); m_nexusLogin->start(); - updateNexusState(); + updateState(); } -void NexusSettingsTab::on_nexusManualKey_clicked() +void NexusConnectionUI::manual() { if (m_nexusValidator && m_nexusValidator->isActive()) { m_nexusValidator->cancel(); return; } - NexusManualKeyDialog d(&dialog()); + NexusManualKeyDialog d(m_parent); if (d.exec() != QDialog::Accepted) { return; } @@ -218,162 +148,275 @@ void NexusSettingsTab::on_nexusManualKey_clicked() return; } - ui->nexusLog->clear(); + m_log->clear(); validateKey(key); } -void NexusSettingsTab::on_nexusDisconnect_clicked() +void NexusConnectionUI::disconnect() { clearKey(); - ui->nexusLog->clear(); - addNexusLog(QObject::tr("Disconnected.")); + m_log->clear(); + addLog(tr("Disconnected.")); } -void NexusSettingsTab::on_clearCacheButton_clicked() -{ - QDir(Settings::instance().paths().cache()).removeRecursively(); - NexusInterface::instance(dialog().pluginContainer())->clearCache(); -} - -void NexusSettingsTab::on_associateButton_clicked() -{ - Settings::instance().nexus().registerAsNXMHandler(true); -} - -void NexusSettingsTab::validateKey(const QString& key) +void NexusConnectionUI::validateKey(const QString& key) { if (!m_nexusValidator) { m_nexusValidator.reset(new NexusKeyValidator( - *NexusInterface::instance(dialog().pluginContainer())->getAccessManager())); + m_settings, *NexusInterface::instance().getAccessManager())); m_nexusValidator->finished = [&](auto&& r, auto&& m, auto&& u) { onValidatorFinished(r, m, u); }; } - addNexusLog(QObject::tr("Checking API key...")); + addLog(tr("Checking API key...")); m_nexusValidator->start(key, NexusKeyValidator::OneShot); } -void NexusSettingsTab::onSSOKeyChanged(const QString& key) +void NexusConnectionUI::onSSOKeyChanged(const QString& key) { if (key.isEmpty()) { clearKey(); } else { - addNexusLog(QObject::tr("Received API key.")); + addLog(tr("Received API key.")); validateKey(key); } } -void NexusSettingsTab::onSSOStateChanged(NexusSSOLogin::States s, const QString& e) +void NexusConnectionUI::onSSOStateChanged( + NexusSSOLogin::States s, const QString& e) { if (s != NexusSSOLogin::Finished) { // finished state is handled in onSSOKeyChanged() const auto log = NexusSSOLogin::stateToString(s, e); for (auto&& line : log.split("\n")) { - addNexusLog(line); + addLog(line); } } - updateNexusState(); + updateState(); } -void NexusSettingsTab::onValidatorFinished( +void NexusConnectionUI::onValidatorFinished( ValidationAttempt::Result r, const QString& message, std::optional<APIUserAccount> user) { if (user) { - NexusInterface::instance(dialog().pluginContainer())->setUserAccount(*user); - addNexusLog(QObject::tr("Received user acount information")); + NexusInterface::instance().setUserAccount(*user); + addLog(tr("Received user account information")); if (setKey(user->apiKey())) { - addNexusLog(QObject::tr("Linked with Nexus successfully.")); + addLog(tr("Linked with Nexus successfully.")); } else { - addNexusLog(QObject::tr("Failed to set API key")); + addLog(tr("Failed to set API key")); } } else { if (message.isEmpty()) { // shouldn't happen - addNexusLog("Unknown error"); + addLog("Unknown error"); } else { - addNexusLog(message); + addLog(message); } } - updateNexusState(); + updateState(); } -void NexusSettingsTab::addNexusLog(const QString& s) +void NexusConnectionUI::addLog(const QString& s) { - ui->nexusLog->addItem(s); - ui->nexusLog->scrollToBottom(); + m_log->addItem(s); + m_log->scrollToBottom(); } -bool NexusSettingsTab::setKey(const QString& key) +bool NexusConnectionUI::setKey(const QString& key) { - dialog().setExitNeeded(Exit::Restart); - const bool ret = settings().nexus().setApiKey(key); - updateNexusState(); + const bool ret = GlobalSettings::setNexusApiKey(key); + updateState(); + + emit keyChanged(); + return ret; } -bool NexusSettingsTab::clearKey() +bool NexusConnectionUI::clearKey() { - dialog().setExitNeeded(Exit::Restart); - const auto ret = settings().nexus().clearApiKey(); + const auto ret = GlobalSettings::clearNexusApiKey(); + + NexusInterface::instance().getAccessManager()->clearApiKey(); + updateState(); - NexusInterface::instance(dialog().pluginContainer())->getAccessManager()->clearApiKey(); - updateNexusState(); + emit keyChanged(); return ret; } -void NexusSettingsTab::updateNexusState() +void NexusConnectionUI::updateState() { - updateNexusButtons(); - updateNexusData(); -} + auto setButton = [&](QAbstractButton* b, bool enabled, QString caption={}) { + if (b) { + b->setEnabled(enabled); + if (!caption.isEmpty()) { + b->setText(caption); + } + } + }; -void NexusSettingsTab::updateNexusButtons() -{ if (m_nexusLogin && m_nexusLogin->isActive()) { // api key is in the process of being retrieved - ui->nexusConnect->setText(QObject::tr("Cancel")); - ui->nexusConnect->setEnabled(true); - ui->nexusDisconnect->setEnabled(false); - ui->nexusManualKey->setText(QObject::tr("Enter API Key Manually")); - ui->nexusManualKey->setEnabled(false); + setButton(m_connect, true, QObject::tr("Cancel")); + setButton(m_disconnect, false); + setButton(m_manual, false,QObject::tr("Enter API Key Manually")); } else if (m_nexusValidator && m_nexusValidator->isActive()) { // api key is in the process of being tested - ui->nexusConnect->setText(QObject::tr("Connect to Nexus")); - ui->nexusConnect->setEnabled(false); - ui->nexusDisconnect->setEnabled(false); - ui->nexusManualKey->setText(QObject::tr("Cancel")); - ui->nexusManualKey->setEnabled(true); + setButton(m_connect, false, QObject::tr("Connect to Nexus")); + setButton(m_disconnect, false); + setButton(m_manual, true, QObject::tr("Cancel")); } - else if (settings().nexus().hasApiKey()) { + else if (GlobalSettings::hasNexusApiKey()) { // api key is present - ui->nexusConnect->setText(QObject::tr("Connect to Nexus")); - ui->nexusConnect->setEnabled(false); - ui->nexusDisconnect->setEnabled(true); - ui->nexusManualKey->setText(QObject::tr("Enter API Key Manually")); - ui->nexusManualKey->setEnabled(false); + setButton(m_connect, false, QObject::tr("Connect to Nexus")); + setButton(m_disconnect, true); + setButton(m_manual, false, QObject::tr("Enter API Key Manually")); } else { // api key not present - ui->nexusConnect->setText(QObject::tr("Connect to Nexus")); - ui->nexusConnect->setEnabled(true); - ui->nexusDisconnect->setEnabled(false); - ui->nexusManualKey->setText(QObject::tr("Enter API Key Manually")); - ui->nexusManualKey->setEnabled(true); + setButton(m_connect, true, QObject::tr("Connect to Nexus")); + setButton(m_disconnect, false); + setButton(m_manual, true, QObject::tr("Enter API Key Manually")); + } + + emit stateChanged(); +} + + + +NexusSettingsTab::NexusSettingsTab(Settings& s, SettingsDialog& d) + : SettingsTab(s, d) +{ + ui->offlineBox->setChecked(settings().network().offlineMode()); + ui->proxyBox->setChecked(settings().network().useProxy()); + ui->endorsementBox->setChecked(settings().nexus().endorsementIntegration()); + ui->hideAPICounterBox->setChecked(settings().interface().hideAPICounter()); + + // display server preferences + for (const auto& server : s.network().servers()) { + QString descriptor = server.name(); + + if (!descriptor.compare("CDN", Qt::CaseInsensitive)) { + descriptor += QStringLiteral(" (automatic)"); + } + + const auto averageSpeed = server.averageSpeed(); + if (averageSpeed > 0) { + descriptor += QString(" (%1)").arg(MOBase::localizedByteSpeed(averageSpeed)); + } + + QListWidgetItem *newItem = new ServerItem<int>(descriptor, Qt::UserRole + 1); + + newItem->setData(Qt::UserRole, server.name()); + newItem->setData(Qt::UserRole + 1, server.preferred()); + + if (server.preferred() > 0) { + ui->preferredServersList->addItem(newItem); + } else { + ui->knownServersList->addItem(newItem); + } + + ui->preferredServersList->sortItems(Qt::DescendingOrder); + } + + m_connectionUI.reset(new NexusConnectionUI( + &dialog(), + &settings(), + ui->nexusConnect, + ui->nexusDisconnect, + ui->nexusManualKey, + ui->nexusLog)); + + QObject::connect( + m_connectionUI.get(), &NexusConnectionUI::stateChanged, &d, + [&]{ updateNexusData(); }, Qt::QueuedConnection); + + QObject::connect( + m_connectionUI.get(), &NexusConnectionUI::keyChanged, &d, + [&]{ dialog().setExitNeeded(Exit::Restart); }); + + + QObject::connect(ui->clearCacheButton, &QPushButton::clicked, [&]{ on_clearCacheButton_clicked(); }); + QObject::connect(ui->associateButton, &QPushButton::clicked, [&]{ on_associateButton_clicked(); }); +} + +void NexusSettingsTab::update() +{ + settings().network().setOfflineMode(ui->offlineBox->isChecked()); + settings().network().setUseProxy(ui->proxyBox->isChecked()); + settings().nexus().setEndorsementIntegration(ui->endorsementBox->isChecked()); + settings().interface().setHideAPICounter(ui->hideAPICounterBox->isChecked()); + + auto servers = settings().network().servers(); + + // store server preference + for (int i = 0; i < ui->knownServersList->count(); ++i) { + const QString key = ui->knownServersList->item(i)->data(Qt::UserRole).toString(); + + bool found = false; + + for (auto& server : servers) { + if (server.name() == key) { + server.setPreferred(0); + found = true; + break; + } + } + + if (!found) { + log::error("while setting preferred to 0, server '{}' not found", key); + } + } + + const int count = ui->preferredServersList->count(); + + for (int i = 0; i < count; ++i) { + const QString key = ui->preferredServersList->item(i)->data(Qt::UserRole).toString(); + const int newPreferred = count - i; + + bool found = false; + + for (auto& server : servers) { + + if (server.name() == key) { + server.setPreferred(newPreferred); + found = true; + break; + } + } + + if (!found) { + log::error( + "while setting preference to {}, server '{}' not found", + newPreferred, key); + } } + + settings().network().updateServers(servers); +} + +void NexusSettingsTab::on_clearCacheButton_clicked() +{ + QDir(Settings::instance().paths().cache()).removeRecursively(); + NexusInterface::instance().clearCache(); +} + +void NexusSettingsTab::on_associateButton_clicked() +{ + Settings::instance().nexus().registerAsNXMHandler(true); } void NexusSettingsTab::updateNexusData() { - const auto user = NexusInterface::instance(dialog().pluginContainer()) - ->getAPIUserAccount(); + const auto user = NexusInterface::instance().getAPIUserAccount(); if (user.isValid()) { ui->nexusUserID->setText(user.id()); diff --git a/src/settingsdialognexus.h b/src/settingsdialognexus.h index 2cb1cc1e..c915accf 100644 --- a/src/settingsdialognexus.h +++ b/src/settingsdialognexus.h @@ -5,38 +5,69 @@ #include "settingsdialog.h" #include "nxmaccessmanager.h" -class NexusSettingsTab : public SettingsTab +// used by the settings dialog and the create instance dialog +// +class NexusConnectionUI : public QObject { + Q_OBJECT; + public: - NexusSettingsTab(Settings& settings, SettingsDialog& dialog); - void update(); + NexusConnectionUI( + QWidget* parent, + Settings* s, + QAbstractButton* connectButton, + QAbstractButton* disconnectButton, + QAbstractButton* manualButton, + QListWidget* logList); + + void connect(); + void manual(); + void disconnect(); + +signals: + void stateChanged(); + void keyChanged(); private: + QWidget* m_parent; + Settings* m_settings; + QAbstractButton* m_connect; + QAbstractButton* m_disconnect; + QAbstractButton* m_manual; + QListWidget* m_log; + std::unique_ptr<NexusSSOLogin> m_nexusLogin; std::unique_ptr<NexusKeyValidator> m_nexusValidator; - void on_nexusConnect_clicked(); - void on_nexusManualKey_clicked(); - void on_nexusDisconnect_clicked(); - void on_clearCacheButton_clicked(); - void on_associateButton_clicked(); + void addLog(const QString& s); + + void updateState(); void validateKey(const QString& key); bool setKey(const QString& key); bool clearKey(); - void updateNexusState(); - void updateNexusButtons(); - void updateNexusData(); - void onSSOKeyChanged(const QString& key); void onSSOStateChanged(NexusSSOLogin::States s, const QString& e); void onValidatorFinished( ValidationAttempt::Result r, const QString& message, std::optional<APIUserAccount> useR); +}; - void addNexusLog(const QString& s); + +class NexusSettingsTab : public SettingsTab +{ +public: + NexusSettingsTab(Settings& settings, SettingsDialog& dialog); + void update(); + +private: + std::unique_ptr<NexusConnectionUI> m_connectionUI; + + void on_clearCacheButton_clicked(); + void on_associateButton_clicked(); + void updateNexusData(); }; #endif // SETTINGSDIALOGNEXUS_H diff --git a/src/settingsdialogpaths.cpp b/src/settingsdialogpaths.cpp index 74ba4f25..eb334541 100644 --- a/src/settingsdialogpaths.cpp +++ b/src/settingsdialogpaths.cpp @@ -64,7 +64,7 @@ void PathsSettingsTab::update() std::tie(path, setter, defaultName) = dir; QString realPath = path; - realPath.replace("%BASE_DIR%", ui->baseDirEdit->text()); + realPath = PathSettings::resolve(realPath, ui->baseDirEdit->text()); if (!QDir(realPath).exists()) { if (!QDir().mkpath(realPath)) { @@ -113,7 +113,7 @@ void PathsSettingsTab::on_browseBaseDirBtn_clicked() void PathsSettingsTab::on_browseDownloadDirBtn_clicked() { QString searchPath = ui->downloadDirEdit->text(); - searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text()); + searchPath = PathSettings::resolve(searchPath, ui->baseDirEdit->text()); QString temp = QFileDialog::getExistingDirectory(&dialog(), QObject::tr("Select download directory"), searchPath); if (!temp.isEmpty()) { @@ -124,7 +124,7 @@ void PathsSettingsTab::on_browseDownloadDirBtn_clicked() void PathsSettingsTab::on_browseModDirBtn_clicked() { QString searchPath = ui->modDirEdit->text(); - searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text()); + searchPath = PathSettings::resolve(searchPath, ui->baseDirEdit->text()); QString temp = QFileDialog::getExistingDirectory(&dialog(), QObject::tr("Select mod directory"), searchPath); if (!temp.isEmpty()) { @@ -135,7 +135,7 @@ void PathsSettingsTab::on_browseModDirBtn_clicked() void PathsSettingsTab::on_browseCacheDirBtn_clicked() { QString searchPath = ui->cacheDirEdit->text(); - searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text()); + searchPath = PathSettings::resolve(searchPath, ui->baseDirEdit->text()); QString temp = QFileDialog::getExistingDirectory(&dialog(), QObject::tr("Select cache directory"), searchPath); if (!temp.isEmpty()) { @@ -146,7 +146,7 @@ void PathsSettingsTab::on_browseCacheDirBtn_clicked() void PathsSettingsTab::on_browseProfilesDirBtn_clicked() { QString searchPath = ui->profilesDirEdit->text(); - searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text()); + searchPath = PathSettings::resolve(searchPath, ui->baseDirEdit->text()); QString temp = QFileDialog::getExistingDirectory(&dialog(), QObject::tr("Select profiles directory"), searchPath); if (!temp.isEmpty()) { @@ -157,7 +157,7 @@ void PathsSettingsTab::on_browseProfilesDirBtn_clicked() void PathsSettingsTab::on_browseOverwriteDirBtn_clicked() { QString searchPath = ui->overwriteDirEdit->text(); - searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text()); + searchPath = PathSettings::resolve(searchPath, ui->baseDirEdit->text()); QString temp = QFileDialog::getExistingDirectory(&dialog(), QObject::tr("Select overwrite directory"), searchPath); if (!temp.isEmpty()) { diff --git a/src/shared/appconfig.inc b/src/shared/appconfig.inc index 709c845d..807f1d69 100644 --- a/src/shared/appconfig.inc +++ b/src/shared/appconfig.inc @@ -9,6 +9,7 @@ APPPARAM(std::wstring, cachePath, L"webcache") APPPARAM(std::wstring, tutorialsPath, L"tutorials")
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, iniFileName, L"ModOrganizer.ini")
diff --git a/src/shared/error_report.h b/src/shared/error_report.h index 17b25645..da07c728 100644 --- a/src/shared/error_report.h +++ b/src/shared/error_report.h @@ -17,16 +17,20 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
*/
-#pragma once
+#ifndef MODORGANIZER_SHARED_ERROR_REPORT_INCLUDED
+#define MODORGANIZER_SHARED_ERROR_REPORT_INCLUDED
#include <tchar.h>
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <string>
-namespace MOShared {
+namespace MOShared
+{
void reportError(LPCSTR format, ...);
void reportError(LPCWSTR format, ...);
} // namespace MOShared
+
+#endif // MODORGANIZER_SHARED_ERROR_REPORT_INCLUDED
diff --git a/src/shared/util.cpp b/src/shared/util.cpp index ba500da9..f316549e 100644 --- a/src/shared/util.cpp +++ b/src/shared/util.cpp @@ -435,3 +435,9 @@ void ResetExitFlag() {
g_exiting = false;
}
+
+
+bool isNxmLink(const QString& link)
+{
+ return link.startsWith("nxm://", Qt::CaseInsensitive);
+}
diff --git a/src/shared/util.h b/src/shared/util.h index 2761b64f..1688a931 100644 --- a/src/shared/util.h +++ b/src/shared/util.h @@ -83,4 +83,6 @@ bool ModOrganizerExiting(); bool ModOrganizerCanCloseNow();
void ResetExitFlag();
+bool isNxmLink(const QString& link);
+
#endif // UTIL_H
diff --git a/src/singleinstance.cpp b/src/singleinstance.cpp index aa62d40a..bd7ccc43 100644 --- a/src/singleinstance.cpp +++ b/src/singleinstance.cpp @@ -28,33 +28,28 @@ static const int s_Timeout = 5000; using MOBase::reportError;
-SingleInstance::SingleInstance(bool forcePrimary, QObject *parent) :
- QObject(parent), m_PrimaryInstance(false)
+SingleInstance::SingleInstance(bool allowMultiple, QObject *parent) :
+ QObject(parent), m_Ephemeral(false), m_OwnsSM(false)
{
m_SharedMem.setKey(s_Key);
+
if (!m_SharedMem.create(1)) {
- if (forcePrimary) {
- while (m_SharedMem.error() == QSharedMemory::AlreadyExists) {
- Sleep(500);
- if (m_SharedMem.create(1)) {
- m_PrimaryInstance = true;
- break;
- }
+ if (m_SharedMem.error() == QSharedMemory::AlreadyExists) {
+ if (!allowMultiple) {
+ m_SharedMem.attach();
+ m_Ephemeral = true;
}
}
- if (m_SharedMem.error() == QSharedMemory::AlreadyExists) {
- m_SharedMem.attach();
- m_PrimaryInstance = false;
- }
if ((m_SharedMem.error() != QSharedMemory::NoError) &&
(m_SharedMem.error() != QSharedMemory::AlreadyExists)) {
throw MOBase::MyException(tr("SHM error: %1").arg(m_SharedMem.errorString()));
}
} else {
- m_PrimaryInstance = true;
+ m_OwnsSM = true;
}
- if (m_PrimaryInstance) {
+
+ if (m_OwnsSM) {
connect(&m_Server, SIGNAL(newConnection()), this, SLOT(receiveMessage()), Qt::QueuedConnection);
// has to be called before listen
m_Server.setSocketOptions(QLocalServer::WorldAccessOption);
@@ -65,7 +60,7 @@ SingleInstance::SingleInstance(bool forcePrimary, QObject *parent) : void SingleInstance::sendMessage(const QString &message)
{
- if (m_PrimaryInstance) {
+ if (m_OwnsSM) {
// nobody there to receive the message
return;
}
diff --git a/src/singleinstance.h b/src/singleinstance.h index 14d49d6d..5f6c3633 100644 --- a/src/singleinstance.h +++ b/src/singleinstance.h @@ -27,8 +27,8 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. /**
* used to ensure only a single instance of Mod Organizer is started and to
- * allow secondary instances to send messages to the primary (visible) one. This way,
- * secondary instances can start a download in the primary one
+ * 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
{
@@ -36,24 +36,26 @@ 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);
/**
- * @brief constructor
- *
- * @param forcePrimary if true, this will be treated as the primary instance even
- * if another instance is running. This is used after an update since
- * the other instance is assumed to be in the process of quitting
- * @param parent parent object
- * @todo the forcePrimary parameter makes no sense. The second instance after an update
- * needs to delete the files from before the update so the first instance needs to quit
- * first anyway
+ * @return true if this instance's job is to forward data to the primary
+ * instance through shared memory
**/
- explicit SingleInstance(bool forcePrimary, QObject *parent = 0);
+ bool ephemeral() const
+ {
+ return m_Ephemeral;
+ }
- /**
- * @return true if this is the primary instance (the one that gets to display a UI)
- **/
- bool primaryInstance() const { return m_PrimaryInstance; }
+ // 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
@@ -65,7 +67,7 @@ public: signals:
/**
- * @brief emitted when a secondary instance has sent a message (to us)
+ * @brief emitted when an ephemeral instance has sent a message (to us)
*
* @param message the message we received
**/
@@ -78,8 +80,8 @@ private slots: void receiveMessage();
private:
-
- bool m_PrimaryInstance;
+ bool m_Ephemeral;
+ bool m_OwnsSM;
QSharedMemory m_SharedMem;
QLocalServer m_Server;
diff --git a/src/spawn.cpp b/src/spawn.cpp index 2016bb23..a9ecb61e 100644 --- a/src/spawn.cpp +++ b/src/spawn.cpp @@ -488,7 +488,7 @@ DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle) }
const QString moPath = QCoreApplication::applicationDirPath();
- const auto oldPath = env::addPath(QDir::toNativeSeparators(moPath));
+ const auto oldPath = env::appendToPath(QDir::toNativeSeparators(moPath));
PROCESS_INFORMATION pi = {};
BOOL success = FALSE;
diff --git a/src/statusbar.cpp b/src/statusbar.cpp index 5897b6bb..8fa43d5b 100644 --- a/src/statusbar.cpp +++ b/src/statusbar.cpp @@ -153,10 +153,9 @@ void StatusBar::updateNormalMessage(OrganizerCore& core) game = tr("Unknown game"); } - QString instance = InstanceManager::instance().currentInstance(); - if (instance.isEmpty()) { - instance = tr("Portable"); - } + QString instance = "?"; + if (auto i=InstanceManager::singleton().currentInstance()) + instance = i->name(); QString profile = core.profileName(); diff --git a/src/thread_utils.h b/src/thread_utils.h index 607d73a9..f64dd601 100644 --- a/src/thread_utils.h +++ b/src/thread_utils.h @@ -7,8 +7,7 @@ #include <thread> // in main.cpp -void setUnhandledExceptionHandler(); -LONG WINAPI MyUnhandledExceptionFilter(struct _EXCEPTION_POINTERS *exceptionPtrs); +void setExceptionHandlers(); namespace MOShared { @@ -20,7 +19,7 @@ template <class F> std::thread startSafeThread(F&& f) { return std::thread([f=std::forward<F>(f)] { - setUnhandledExceptionHandler(); + setExceptionHandlers(); f(); }); } diff --git a/src/uilocker.h b/src/uilocker.h index cc467184..44d9d8a2 100644 --- a/src/uilocker.h +++ b/src/uilocker.h @@ -1,4 +1,5 @@ -#pragma once +#ifndef MODORGANIZER_UILOCKER_INCLUDED +#define MODORGANIZER_UILOCKER_INCLUDED #include <QMainWindow> #include <mutex> @@ -94,3 +95,5 @@ private: void enableAll(); void disable(QWidget* w); }; + +#endif // MODORGANIZER_UILOCKER_INCLUDED |
