diff options
| -rw-r--r-- | src/createinstancedialog.cpp | 10 | ||||
| -rw-r--r-- | src/createinstancedialogpages.cpp | 173 | ||||
| -rw-r--r-- | src/createinstancedialogpages.h | 368 |
3 files changed, 448 insertions, 103 deletions
diff --git a/src/createinstancedialog.cpp b/src/createinstancedialog.cpp index f52d3ebd..1c1b62d0 100644 --- a/src/createinstancedialog.cpp +++ b/src/createinstancedialog.cpp @@ -13,6 +13,12 @@ using namespace MOBase; class Failed {}; +// create() will create all the directories in `target`; if any path component +// fails to create, it will throw Failed +// +// unless commit() is called, all the created directories will be deleted in +// the destructor +// class DirectoryCreator { public: @@ -39,6 +45,7 @@ public: { try { + // delete each directory starting from the end for (auto itor=m_created.rbegin(); itor!=m_created.rend(); ++itor) { const auto r = shell::DeleteDirectoryRecursive(*itor); if (!r) { @@ -62,6 +69,7 @@ private: { try { + // split on separators const QString s = QDir::toNativeSeparators(target.absolutePath()); const QStringList cs = s.split("\\"); @@ -69,8 +77,10 @@ private: return; } + // root directory QDir d(cs[0]); + // for each directory after the root for (int i=1; i<cs.size(); ++i) { d = d.filePath(cs[i]); diff --git a/src/createinstancedialogpages.cpp b/src/createinstancedialogpages.cpp index a08bf5a1..8431abd1 100644 --- a/src/createinstancedialogpages.cpp +++ b/src/createinstancedialogpages.cpp @@ -21,6 +21,21 @@ QString makeDefaultPath(const std::wstring& dir) QString::fromStdWString(dir))); } +QString toLocalizedString(CreateInstanceDialog::Types t) +{ + switch (t) + { + case CreateInstanceDialog::Global: + return QObject::tr("Global"); + + case CreateInstanceDialog::Portable: + return QObject::tr("Portable"); + + default: + return QObject::tr("Instance type: %1").arg(QObject::tr("?")); + } +} + PlaceholderLabel::PlaceholderLabel(QLabel* label) @@ -333,6 +348,15 @@ std::vector<IPluginGame*> GamePage::sortedGamePlugins() const return v; } +void GamePage::createGames() +{ + m_games.clear(); + + for (auto* game : sortedGamePlugins()) { + m_games.push_back(std::make_unique<Game>(game)); + } +} + GamePage::Game* GamePage::findGame(IPluginGame* game) { for (auto& g : m_games) { @@ -344,13 +368,24 @@ GamePage::Game* GamePage::findGame(IPluginGame* game) return nullptr; } -void GamePage::createGames() +void GamePage::createGameButton(Game* g) { - m_games.clear(); + g->button = new QCommandLinkButton; + g->button->setCheckable(true); - for (auto* game : sortedGamePlugins()) { - m_games.push_back(std::make_unique<Game>(game)); - } + updateButton(g); + + QObject::connect(g->button, &QAbstractButton::clicked, [g, this] { + select(g->game); + }); +} + +void GamePage::addButton(QAbstractButton* b) +{ + auto* ly = static_cast<QVBoxLayout*>(ui->games->layout()); + + // insert before the stretch + ly->insertWidget(ly->count() - 1, b); } void GamePage::updateButton(Game* g) @@ -407,6 +442,26 @@ void GamePage::selectButton(Game* g) } } +void GamePage::clearButtons() +{ + auto* ly = static_cast<QVBoxLayout*>(ui->games->layout()); + + ui->games->setUpdatesEnabled(false); + + // delete all children + qDeleteAll(ui->games->findChildren<QWidget*>("", Qt::FindDirectChildrenOnly)); + + // stretch widgets added with addStretch() are not in the parent widget, + // they have to be deleted from the layout itself + while (auto* child=ly->takeAt(0)) + delete child; + + // add a stretch, buttons will be added before + ly->addStretch(); + + ui->games->setUpdatesEnabled(true); +} + QCommandLinkButton* GamePage::createCustomButton() { auto* b = new QCommandLinkButton; @@ -422,18 +477,6 @@ QCommandLinkButton* GamePage::createCustomButton() return b; } -void GamePage::createGameButton(Game* g) -{ - g->button = new QCommandLinkButton; - g->button->setCheckable(true); - - updateButton(g); - - QObject::connect(g->button, &QAbstractButton::clicked, [g, this] { - select(g->game); - }); -} - void GamePage::fillList() { const bool showAll = ui->showAllGames->isChecked(); @@ -459,34 +502,6 @@ void GamePage::fillList() addButton(createCustomButton()); } -void GamePage::clearButtons() -{ - auto* ly = static_cast<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)) { @@ -495,7 +510,15 @@ GamePage::Game* GamePage::checkInstallation(const QString& path, Game* g) } // the selected game can't use that folder, find another one - auto* otherGame = findAnotherGame(path); + IPluginGame* otherGame = nullptr; + + for (auto* gg : m_pc.plugins<IPluginGame>()) { + if (gg->looksValid(path)) { + otherGame = gg; + break; + } + } + if (otherGame == g->game) { // shouldn't happen, but okay return g; @@ -531,17 +554,6 @@ GamePage::Game* GamePage::checkInstallation(const QString& path, Game* g) return g; } -IPluginGame* GamePage::findAnotherGame(const QString& path) -{ - for (auto* otherGame : m_pc.plugins<IPluginGame>()) { - if (otherGame->looksValid(path)) { - return otherGame; - } - } - - return nullptr; -} - bool GamePage::confirmUnknown(const QString& path, IPluginGame* game) { const auto r = TaskDialog(&m_dlg) @@ -733,7 +745,7 @@ void NamePage::activated() m_modified = false; } - updateWarnings(); + verify(); } QString NamePage::selectedInstanceName() const @@ -749,13 +761,12 @@ QString NamePage::selectedInstanceName() const void NamePage::onChanged() { m_modified = true; - updateWarnings(); + verify(); } -void NamePage::updateWarnings() +void NamePage::verify() { const auto root = InstanceManager::singleton().globalInstancesRootPath(); - m_okay = checkName(root, ui->instanceName->text()); updateNavigation(); } @@ -803,7 +814,8 @@ PathsPage::PathsPage(CreateInstanceDialog& dlg) : m_label(ui->pathsLabel), m_simpleExists(ui->locationExists), m_simpleInvalid(ui->locationInvalid), m_advancedExists(ui->advancedDirExists), - m_advancedInvalid(ui->advancedDirInvalid) + m_advancedInvalid(ui->advancedDirInvalid), + m_okay(false) { QObject::connect(ui->location, &QLineEdit::textEdited, [&]{ onChanged(); }); QObject::connect(ui->base, &QLineEdit::textEdited, [&]{ onChanged(); }); @@ -821,7 +833,7 @@ PathsPage::PathsPage(CreateInstanceDialog& dlg) : bool PathsPage::ready() const { - return checkPaths(); + return m_okay; } void PathsPage::activated() @@ -863,21 +875,27 @@ void PathsPage::onChanged() updateNavigation(); } -bool PathsPage::checkPaths() const +void PathsPage::checkPaths() { if (ui->advancedPathOptions->isChecked()) { - return + m_okay = checkAdvancedPath(ui->base->text()) && checkAdvancedPath(resolve(ui->downloads->text())) && checkAdvancedPath(resolve(ui->mods->text())) && checkAdvancedPath(resolve(ui->profiles->text())) && checkAdvancedPath(resolve(ui->overwrite->text())); } else { - return checkPath(ui->location->text(), m_simpleExists, m_simpleInvalid); + m_okay = + checkSimplePath(ui->location->text()); } } -bool PathsPage::checkAdvancedPath(const QString& path) const +bool PathsPage::checkSimplePath(const QString& path) +{ + return checkPath(path, m_simpleExists, m_simpleInvalid); +} + +bool PathsPage::checkAdvancedPath(const QString& path) { return checkPath(path, m_advancedExists, m_advancedInvalid); } @@ -931,7 +949,7 @@ void PathsPage::setIfEmpty(QLineEdit* e, const QString& path, bool force) bool PathsPage::checkPath( QString path, - PlaceholderLabel& existsLabel, PlaceholderLabel& invalidLabel) const + PlaceholderLabel& existsLabel, PlaceholderLabel& invalidLabel) { bool exists = false; bool invalid = false; @@ -1013,10 +1031,6 @@ bool NexusPage::doSkip() const return m_skip; } -void NexusPage::activated() -{ -} - ConfirmationPage::ConfirmationPage(CreateInstanceDialog& dlg) : Page(dlg) @@ -1029,21 +1043,6 @@ void ConfirmationPage::activated() ui->creationLog->clear(); } -QString ConfirmationPage::toLocalizedString(CreateInstanceDialog::Types t) const -{ - switch (t) - { - case CreateInstanceDialog::Global: - return QObject::tr("Global"); - - case CreateInstanceDialog::Portable: - return QObject::tr("Portable"); - - default: - return QObject::tr("Instance type: %1").arg(QObject::tr("?")); - } -} - QString ConfirmationPage::makeReview() const { QStringList lines; diff --git a/src/createinstancedialogpages.h b/src/createinstancedialogpages.h index 099071a2..e2eaf0fb 100644 --- a/src/createinstancedialogpages.h +++ b/src/createinstancedialogpages.h @@ -15,14 +15,26 @@ class NexusConnectionUI; namespace cid { +// returns "%base_dir%/dir" +// QString makeDefaultPath(const std::wstring& dir); +// remembers the original text of the given label and, if it contains a %1, +// sets it in setText() +// class PlaceholderLabel { public: PlaceholderLabel(QLabel* label); + + // if the original label text contained a %1, replaces it by the arg and + // sets that as the new label text + // void setText(const QString& arg); + + // whether the label is visible + // void setVisible(bool b); private: @@ -31,25 +43,67 @@ private: }; +// one page in the wizard +// +// each page can implement one or more selected*() below; those are called +// by CreateInstanceDialog to gather data from all pages +// class Page { public: Page(CreateInstanceDialog& dlg); + // whether this page has been filled and is valid; used by the dialog to + // determine if it can move to the next page + // virtual bool ready() const; + + // called every time a page is shown in the screen + // virtual void activated(); + // overrides whether this page should be skipped; this is used by + // CreateInstanceDialog::setSinglePage() to disable all other pages + // void setSkip(bool b); + + // whether this page should be skipped + // bool skip() const; + // asks the dialog to update its navigation buttons, typically used when a + // page changes its ready state without moving to a different page + // void updateNavigation(); + + // asks the dialog to move to the next page; some pages will automatically + // advance once the user has made the proper selection + // void next(); + + // returns the instance type + // virtual CreateInstanceDialog::Types selectedInstanceType() const; + + // returns the game plugin + // virtual MOBase::IPluginGame* selectedGame() const; + + // returns the game directory + // virtual QString selectedGameLocation() const; + + // returns the game variant + // virtual QString selectedGameVariant(MOBase::IPluginGame* game) const; + + // returns the instance name + // virtual QString selectedInstanceName() const; + + // returns the various paths + // virtual CreateInstanceDialog::Paths selectedPaths() const; protected: @@ -58,10 +112,15 @@ protected: const PluginContainer& m_pc; bool m_skip; + + // implemented by derived classes, overridden by setSkip(true) + // virtual bool doSkip() const; }; +// introduction page, can be disabled by a global setting +// class IntroPage : public Page { public: @@ -72,15 +131,27 @@ protected: }; +// instance type page +// class TypePage : public Page { public: TypePage(CreateInstanceDialog& dlg); + // whether a type has been been selected + // bool ready() const override; + + // returns the selected type + // CreateInstanceDialog::Types selectedInstanceType() const override; + // selects a global instance + // void global(); + + // selects a portable instance + // void portable(); private: @@ -88,160 +159,425 @@ private: }; +// game plugin page, displays a list of command buttons for each game, along +// with a "browse" button for custom directories and filtering stuff +// +// the game list initially only shows plugins that report isInstalled(), and the +// user has two ways of specifying paths for games that were not found: +// +// 1) by clicking the "Browse..." button and selecting an arbitrary directory +// +// all plugins are checked until one returns true for looksValid(); if none +// of them do, this is an error +// +// 2) by checking the "Show all supported games" checkbox and clicking one +// of the games on the list +// +// if the selected plugin doesn't recognize the directory, the user is +// warned, but is allowed to continue; there's also some logic to try to +// find another plugin that can manage this directory and suggest it +// instead +// class GamePage : public Page { public: GamePage(CreateInstanceDialog& dlg); + // whether a game has been selected + // bool ready() const override; + + // returns the selected game + // MOBase::IPluginGame* selectedGame() const override; + + // returns the selected game directory QString selectedGameLocation() const override; + + // selects the given game and toggles its associated button; the game + // directory can be overridden + // + // pops up a directory selection dialog if `dir` is empty and the plugin + // hasn't detected the game + // void select(MOBase::IPluginGame* game, const QString& dir={}); + + // pops up a directory selection dialog and looks for a plugin to manage + // it + // void selectCustom(); + // pops up a warning dialog that the game at the given path is not supported + // by any plugin, includes a list of all game plugins in the details section + // of the dialog + // void warnUnrecognized(const QString& path); private: + // a single game, with its button and custom directory, if any + // struct Game { + // game plugin MOBase::IPluginGame* game = nullptr; + + // button on the ui QCommandLinkButton* button = nullptr; + + // game directory; set in ctor if the plugin has detected the game, or + // set later when the user selects a directory QString dir; + + // whether a directory has been set for this game, either auto detected + // or by the user bool installed = false; + Game(MOBase::IPluginGame* g); Game(const Game&) = delete; Game& operator=(const Game&) = delete; }; + // list of all game plugins, even if they're not installed; those are filtered + // from the ui if the checkbox isn't checked std::vector<std::unique_ptr<Game>> m_games; + + // current selection Game* m_selection; + + // filter MOBase::FilterWidget m_filter; + + // returns a list of all the game plugins sorted with natsort + // std::vector<MOBase::IPluginGame*> sortedGamePlugins() const; - Game* findGame(MOBase::IPluginGame* game); + + // creates the m_games list + // void createGames(); + + // finds the game struct associated with the given game + // + Game* findGame(MOBase::IPluginGame* game); + + + // creates the ui for the given game button + // + void createGameButton(Game* g); + + // adds the given button to the ui + // + void addButton(QAbstractButton* b); + + // updates the given button on the ui, sets the text, icon, etc. + // void updateButton(Game* g); + + // called when a button has been clicked; selects the game or asks the user + // for directory, depending + // void selectButton(Game* g); + + // removes all buttons from the ui + // void clearButtons(); - void addButton(QAbstractButton* b); + + // creates the "Browse" button + // QCommandLinkButton* createCustomButton(); - void createGameButton(Game* g); + + + // clears the button list and adds all the buttons to it, depending on + // filtering and stuff + // void fillList(); - void onFilter(); + + // checks whether the given path looks valid to the given game plugin + // + // if the plugin doesn't like the path, allows the user to override and + // accept, but also attempts to find another plugin that wants it and + // propose that as an alternative, if there's one + // + // returns: + // - if the user selects the alternative plugin, returns that plugin + // instead; + // - if the path is bad but the user overrides, returns the given plugin + // - if the user cancels or if no plugins can manage the directory, returns + // null + // Game* checkInstallation(const QString& path, Game* g); - MOBase::IPluginGame* findAnotherGame(const QString& path); + + // tells the user that the path cannot be handled by any game plugin, returns + // true if the user decides to accept anyway + // bool confirmUnknown(const QString& path, MOBase::IPluginGame* game); + + // tells the user that the path can be handled by a different plugin than the + // selected one and allows them to either + // 1) use the alternative, guessedGame is returned; + // 2) use the selection anyway, selectedGame is returned; or + // 3) cancel, null is returned + // MOBase::IPluginGame* confirmOtherGame( const QString& path, MOBase::IPluginGame* selectedGame, MOBase::IPluginGame* guessedGame); }; +// game variants page; displays a list of command buttons for game variants, as +// reported by the game plugin +// +// this page is always skipped if the game plugin reports no variants +// class VariantsPage : public Page { public: VariantsPage(CreateInstanceDialog& dlg); + // whether a variant has been selected or the game plugin reports no variants + // bool ready() const override; + + // uses the game selected in the previous page to fill the list, this must be + // called every time because the user may go back in forth in the wizard + // void activated() override; + + // returns the selected variant, if any + // QString selectedGameVariant(MOBase::IPluginGame* game) const override; + // selects the given variant + // void select(const QString& variant); protected: + // returns true if the game has no variants + // bool doSkip() const override; private: + // game that was selected the last time this page was active MOBase::IPluginGame* m_previousGame; + + // buttons std::vector<QCommandLinkButton*> m_buttons; + + // selected variant QString m_selection; + + // fills the list with buttons void fillList(); }; +// instance name page; displays a textbox where the user can enter a name and +// does basic checks to make sure the name is valid and not a duplicate +// +// skipped for portable instances +// class NamePage : public Page { public: NamePage(CreateInstanceDialog& dlg); + // whether a valid name has been entered + // bool ready() const override; + + // uses the selected game to generate an instance name + // + // as long as the user hasn't modified the textbox, this will regenerate a new + // instance name every time the selected game changes + // void activated() override; + + // returns the instance name + // QString selectedInstanceName() const override; protected: + // returns true for portable instances + // bool doSkip() const override; private: - mutable PlaceholderLabel m_label, m_exists, m_invalid; + // game label, replaces %1 with the game name + PlaceholderLabel m_label; + + // "instance already exists" label, replaces %1 with instance name + PlaceholderLabel m_exists; + + // "instance name invalid" label, replaces %1 with instance name + PlaceholderLabel m_invalid; + + // whether the user has modified the text, prevents auto generation when the + // selected game changes bool m_modified; + + // whether the instance name is valid bool m_okay; + + // called when the user modifies the textbox, remember that it has changed and + // calls verify() + // void onChanged(); - void updateWarnings(); + + // check if the entered name is valid, sets m_okay and calls checkName() + // + void verify(); + + // updates the ui depending on whether the given instance name is valid in + // the given directory; returns false if the name is invalid + // bool checkName(QString parentDir, QString name); }; +// instance paths page; shows a single textbox for the base directory, or a +// series of textboxes for all the configurable paths if the advanced checkbox +// is checked +// class PathsPage : public Page { public: PathsPage(CreateInstanceDialog& dlg); + // whether all paths make sense + // bool ready() const override; + + // resets all the paths if the instance type or instance name have changed, + // the current values are kept as long as these don't change; also updates the + // game name in the ui + // void activated() override; + // returns the selected paths + // CreateInstanceDialog::Paths selectedPaths() const override; private: + // instance name the last time this page was active QString m_lastInstanceName; + + // instance type the last time this page was active CreateInstanceDialog::Types m_lastType; + + // help label, replaces %1 by the game name PlaceholderLabel m_label; - mutable PlaceholderLabel m_simpleExists, m_simpleInvalid; - mutable PlaceholderLabel m_advancedExists, m_advancedInvalid; + // path exists/is invalid labels for the simple page, replaces %1 with the + // path + PlaceholderLabel m_simpleExists, m_simpleInvalid; + + // path exists/is invalid labels for the advanced page, replaces %1 with the + // path + PlaceholderLabel m_advancedExists, m_advancedInvalid; + + // whether the paths are valid + bool m_okay; + + + // called when the user changes any textbox, checks the path and updates nav + // void onChanged(); - bool checkPaths() const; - bool checkAdvancedPath(const QString& path) const; + + // checks the simple or advanced paths, sets m_okay + // + void checkPaths(); + + // checks a simple path, forwards to checkPath() with the simple labels + // + bool checkSimplePath(const QString& path); + + // checks an advanced path, forwards to checkPath() with the advanced labels + // + bool checkAdvancedPath(const QString& path); + + // returns false if the path is invalid or already exists, sets the given + // labels accordingly + // + bool checkPath( + QString path, + PlaceholderLabel& existsLabel, PlaceholderLabel& invalidLabel); + + // replaces %base_dir% in the given path by whatever's in the base path + // textbox + // QString resolve(const QString& path) const; + + // called when the advanced checkbox is toggled, switches the active page + // and checks the paths + // void onAdvanced(); + + // called whenever the page becomes active + // + // this normally doesn't change the textboxes unless they're empty, but if the + // instance name or type have changed, `force` is true, which forces all paths + // to reset + // void setPaths(const QString& name, bool force); + + // sets the given textbox to the path if it's empty or if `force` is true + // void setIfEmpty(QLineEdit* e, const QString& path, bool force); - bool checkPath( - QString path, - PlaceholderLabel& existsLabel, PlaceholderLabel& invalidLabel) const; }; +// nexus connection page; this reuses the ui found in the settings dialog and +// is skipped if there's already an api key in the credentials manager +// class NexusPage : public Page { public: NexusPage(CreateInstanceDialog& dlg); ~NexusPage(); + // always returns true, this is an optional page + // bool ready() const override; - void activated() override; protected: + // returns true if the api key was already detected + // bool doSkip() const override; private: + // connection ui std::unique_ptr<NexusConnectionUI> m_connectionUI; + + // set to true only if the api key was detected when opening the dialog, or + // going back and forth would skip the page after the process is completed, + // which would be unexpected bool m_skip; }; +// shows a text log of all the creation parameters +// class ConfirmationPage : public Page { public: ConfirmationPage(CreateInstanceDialog& dlg); + // recreates the log with the latest settings + // void activated() override; - QString toLocalizedString(CreateInstanceDialog::Types t) const; + // returns the text for the log + // QString makeReview() const; + +private: + // returns a log line with the given caption and path, something like + // " - caption: path" + // QString dirLine(const QString& caption, const QString& path) const; }; |
