#include "createinstancedialog.h" #include "createinstancedialogpages.h" #include "instancemanager.h" #include "settings.h" #include "shared/appconfig.h" #include "shared/util.h" #include "ui_createinstancedialog.h" #include #include using namespace MOBase; class Failed {}; // create() will create all the directories in `target`; if any path component // fails to create, it will throw Failed // // unless commit() is called, all the created directories will be deleted in // the destructor // class DirectoryCreator { public: DirectoryCreator(const DirectoryCreator&) = delete; DirectoryCreator& operator=(const DirectoryCreator&) = delete; static std::unique_ptr create(const QDir& target, std::function log) { return std::unique_ptr(new DirectoryCreator(target, log)); } ~DirectoryCreator() { rollback(); } void commit() { m_created.clear(); } void rollback() noexcept { try { // delete each directory starting from the end for (auto itor = m_created.rbegin(); itor != m_created.rend(); ++itor) { const auto r = shell::DeleteDirectoryRecursive(*itor); if (!r) { m_logger(r.toString()); } } m_created.clear(); } catch (...) { // eat it } } private: std::function m_logger; DirectoryCreator(const QDir& target, std::function log) : m_logger(log) { try { // split on separators const QString s = QDir::toNativeSeparators(target.absolutePath()); const QStringList cs = s.split("\\"); if (cs.empty()) { return; } // root directory QDir d(cs[0]); // for each directory after the root for (int i = 1; i < cs.size(); ++i) { d = d.filePath(cs[i]); if (!d.exists()) { m_logger(QObject::tr("Creating %1").arg(d.path())); const auto r = shell::CreateDirectories(d); if (!r) { m_logger(r.toString()); throw Failed(); } m_created.push_back(d); } } } catch (...) { rollback(); throw; } } private: std::vector m_created; }; 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(*this)); m_pages.push_back(std::make_unique(*this)); m_pages.push_back(std::make_unique(*this)); m_pages.push_back(std::make_unique(*this)); m_pages.push_back(std::make_unique(*this)); m_pages.push_back(std::make_unique(*this)); m_pages.push_back(std::make_unique(*this)); m_pages.push_back(std::make_unique(*this)); m_pages.push_back(std::make_unique(*this)); ui->pages->setCurrentIndex(0); ui->launch->setChecked(true); if (!InstanceManager::singleton().hasAnyInstances()) { // first run of MO, there are no instances yet, force launch ui->launch->setEnabled(false); } if (m_pages[0]->skip()) { next(); } ui->next->setFocus(); updateNavigation(); addShortcutAction(QKeySequence::Find, Actions::Find); addShortcut(Qt::ALT + Qt::Key_Left, [&] { back(); }); addShortcut(Qt::ALT + Qt::Key_Right, [&] { next(false); }); addShortcut(Qt::CTRL + Qt::Key_Return, [&] { next(); }); connect(ui->next, &QPushButton::clicked, [&] { next(); }); connect(ui->back, &QPushButton::clicked, [&] { back(); }); connect(ui->cancel, &QPushButton::clicked, [&] { reject(); }); } 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(bool allowFinish) { if (!canNext()) { return; } const auto i = ui->pages->currentIndex(); const auto last = isOnLastPage(); if (last) { if (allowFinish) { if (m_singlePage) { // just close the dialog accept(); } else { finish(); } } } else { changePage(+1); } } void CreateInstanceDialog::back() { if (!canBack()) { return; } changePage(-1); } void CreateInstanceDialog::addShortcut(QKeySequence seq, std::function f) { auto* sc = new QShortcut(seq, this); sc->setAutoRepeat(false); sc->setContext(Qt::WidgetWithChildrenShortcut); QObject::connect(sc, &QShortcut::activated, f); } void CreateInstanceDialog::addShortcutAction(QKeySequence seq, Actions a) { addShortcut(seq, [this, a] { doAction(a); }); } void CreateInstanceDialog::doAction(Actions a) { std::size_t i = static_cast(ui->pages->currentIndex()); if (i >= m_pages.size()) { return; } m_pages[i]->action(a); } void CreateInstanceDialog::setSinglePageImpl(const QString& instanceName) { m_singlePage = true; 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(ui->pages->currentIndex()); // goes back or forwards until an unskippable page is reached, or the // first/last page if (d > 0) { // forwards for (;;) { ++i; if (i >= m_pages.size()) { break; } if (!m_pages[i]->skip()) { break; } } } else { // backwards for (;;) { if (i == 0) { break; } --i; if (!m_pages[i]->skip()) { break; } } } if (i < m_pages.size()) { selectPage(i); } } 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); }; // don't restart if this is the first instance, it'll be selected and opened const bool mustRestart = InstanceManager::singleton().hasAnyInstances(); try { std::vector> dirs; // creating all these directories; if any of them fail, this throws and // any newly created directory will be deleted in DirectoryCreator's dtor dirs.push_back(createDir(ci.dataPath)); dirs.push_back(createDir(ci.paths.base)); dirs.push_back(createDir(PathSettings::resolve(ci.paths.downloads, ci.paths.base))); 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))); // creating ini 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.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); } s.setProfileLocalInis(ci.profileSettings.localInis); s.setProfileLocalSaves(ci.profileSettings.localSaves); s.setProfileArchiveInvalidation(ci.profileSettings.archiveInvalidation); logCreation(tr("Writing %1...").arg(ci.iniPath)); // writing ini 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(r))); break; } throw Failed(); } // committing all the directories so they don't get deleted for (auto& d : dirs) { d->commit(); } logCreation(tr("Done.")); // launch the new instance if (ui->launch->isChecked()) { InstanceManager::singleton().setCurrentInstance(ci.instanceName); if (mustRestart) { ExitModOrganizer(Exit::Restart); m_switching = true; } } // close the dialog accept(); } catch (Failed&) { // if Failed was thrown, all the directories have been deleted } } 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(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]->skip() || 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; } bool CreateInstanceDialog::switching() const { return m_switching; } CreateInstanceDialog::CreationInfo CreateInstanceDialog::rawCreationInfo() 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.game); ci.instanceName = getSelected(&cid::Page::selectedInstanceName); ci.profileSettings = getSelected(&cid::Page::profileSettings); ci.paths = getSelected(&cid::Page::selectedPaths); if (ci.type == Portable) { ci.dataPath = QDir(InstanceManager::singleton().portablePath()).absolutePath(); } else { ci.dataPath = InstanceManager::singleton().instancePath(ci.instanceName); } ci.dataPath = QDir::toNativeSeparators(ci.dataPath); ci.iniPath = ci.dataPath + "/" + iniFilename; return ci; } CreateInstanceDialog::CreationInfo CreateInstanceDialog::creationInfo() const { auto fixVarDir = [](QString& path, const std::wstring& defaultDir) { // if the path is empty, it wasn't filled by the user, probably because // the "Advanced" checkbox wasn't checked, so use the base dir variable // with the default dir if (path.isEmpty()) { path = cid::makeDefaultPath(defaultDir); } else if (!path.contains(PathSettings::BaseDirVariable)) { path = QDir(path).absolutePath(); } path = QDir::toNativeSeparators(path); }; auto fixDirPath = [](QString& path) { path = QDir::toNativeSeparators(QDir(path).absolutePath()); }; auto fixFilePath = [](QString& path) { path = QDir::toNativeSeparators(QFileInfo(path).absolutePath()); }; auto ci = rawCreationInfo(); fixDirPath(ci.paths.base); 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; }