From bc157095a56596e697d40371688926953b2c5533 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Fri, 6 Nov 2020 14:43:33 -0500 Subject: moved getInstanceName() up removed individual getters in CreateInstanceDialog, they were duplicating the work in creationInfo() only VariantsPage needs access to the game plugin during creationInfo(), so pass it instead selecting an undetected game, choosing a valid path and going back to the games page would not display the selected path in the list --- src/createinstancedialog.h | 99 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 82 insertions(+), 17 deletions(-) (limited to 'src/createinstancedialog.h') diff --git a/src/createinstancedialog.h b/src/createinstancedialog.h index 25e383eb..d541f45e 100644 --- a/src/createinstancedialog.h +++ b/src/createinstancedialog.h @@ -10,11 +10,25 @@ namespace cid { class Page; } class PluginContainer; class Settings; +// this is a wizard for creating a new instance, it is made out of Page objects, +// see createinstancedialogpages.h +// +// each page can give back one or more pieces of information that is collected +// in creationInfo() and used by finish() to do the actual creation +// +// pages can be disabled if they return true in skip(), which happens globally +// for some (IntroPage has a setting in the registry), depending on context +// (NexusPage is skipped if the API key already exists) or explicitly (when +// only some info about the instance is missing on startup, such as a game +// variant) +// class CreateInstanceDialog : public QDialog { Q_OBJECT public: + // instance type + // enum Types { NoType = 0, @@ -22,6 +36,10 @@ public: Portable }; + // all the paths required by the instance, some may be empty, such as + // basically all of them except for `base` when the user doesn't use the + // "Advanced" part of the paths page + // struct Paths { QString base; @@ -34,6 +52,8 @@ public: auto operator<=>(const Paths&) const = default; }; + // all the info filled in the various pages + // struct CreationInfo { Types type; @@ -53,10 +73,11 @@ public: ~CreateInstanceDialog(); Ui::CreateInstanceDialog* getUI(); - const PluginContainer& pluginContainer(); Settings* settings(); + // disables all the pages except for the given one, used on startup when some + // specific info is missing template void setSinglePage(const QString& instanceName) { @@ -71,6 +92,8 @@ public: setSinglePageImpl(instanceName); } + // returns the page having the give path, or null + // template Page* getPage() { @@ -83,24 +106,59 @@ public: return nullptr; } + + // moves to the next page calls finish() if on the last one + // void next(); + + // moves to the previous page, if any + // void back(); + + // whether the current page reports that it is ready; if this is the last + // page, next() would call finish() + // + bool canNext() const; + + // whether the current page is not the first one and there is an enabled page + // prior + // + bool canBack() const; + + // selects the given page by index; this doesn't check if the page should be + // skipped + // void selectPage(std::size_t i); + + // moves by `d` pages, can be negative to move back + // void changePage(int d); + + // creates the instance and closes the dialog + // void finish(); + + // updates the navigation buttons based on the current page + // void updateNavigation(); + + // whether this is the last enabled page + // bool isOnLastPage() const; - Types instanceType() const; - MOBase::IPluginGame* game() const; - QString gameLocation() const; - QString gameVariant() const; - QString instanceName() const; - QString dataPath() const; - Paths paths() const; + // returns whether the user has requested to switch to the new instance + // bool switching() const; + // gathers the info from all the pages as it appears, paths are not fixed; + // see creationInfo() + // + CreationInfo rawCreationInfo() const; + + // gathers the info from all the pages: paths are converted to absolute and + // the base dir variable is expanded everywhere; see rawCreationInfo() + // CreationInfo creationInfo() const; private: @@ -113,13 +171,26 @@ private: bool m_singlePage; + // called from setSinglePage(), does whatever doesn't need the T + // void setSinglePageImpl(const QString& instanceName); - template - T getSelected(T (cid::Page::*mf)() const) const + // adds a line to the creation log + // + void logCreation(const QString& s); + void logCreation(const std::wstring& s); + + // calls the given member function on all pages until one returns an object + // that's not empty; used by gatherInfo() + // + template + auto getSelected(MF mf, Args&&... args) const { + // return type + using T = decltype((std::declval().*mf)(std::forward(args)...)); + for (auto&& p : m_pages) { - const auto t = (p.get()->*mf)(); + const auto t = (p.get()->*mf)(std::forward(args)...); if (t != T()) { return t; } @@ -127,12 +198,6 @@ private: return T(); } - - void logCreation(const QString& s); - void logCreation(const std::wstring& s); - - bool canNext() const; - bool canBack() const; }; #endif // MODORGANIZER_CREATEINSTANCEDIALOG_INCLUDED -- cgit v1.3.1 From 3cc568a852e9b64bb6e0f34f74869965d04f6a0c Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sun, 8 Nov 2020 23:55:17 -0500 Subject: keyboard nav for create instance dialog change the skip intro setting on click instead of on completion ctrl+f in game page --- src/createinstancedialog.cpp | 62 +++++++++++++++---- src/createinstancedialog.h | 23 +++++++- src/createinstancedialog.ui | 6 ++ src/createinstancedialogpages.cpp | 121 +++++++++++++++++++++++++++++--------- src/createinstancedialogpages.h | 66 +++++++++++++++------ 5 files changed, 220 insertions(+), 58 deletions(-) (limited to 'src/createinstancedialog.h') diff --git a/src/createinstancedialog.cpp b/src/createinstancedialog.cpp index 1c1b62d0..47cb9d5c 100644 --- a/src/createinstancedialog.cpp +++ b/src/createinstancedialog.cpp @@ -141,8 +141,16 @@ CreateInstanceDialog::CreateInstanceDialog( next(); } + ui->next->setFocus(); + updateNavigation(); + addShortcutAction(QKeySequence::Find, Actions::Find); + + addShortcut(Qt::ALT+Qt::Key_Left, [&]{ back(); }); + addShortcut(Qt::ALT+Qt::Key_Right, [&]{ next(false); }); + addShortcut(Qt::CTRL+Qt::Key_Return, [&]{ next(); }); + connect(ui->next, &QPushButton::clicked, [&]{ next(); }); connect(ui->back, &QPushButton::clicked, [&]{ back(); }); connect(ui->cancel, &QPushButton::clicked, [&]{ reject(); }); @@ -176,17 +184,23 @@ bool CreateInstanceDialog::isOnLastPage() const return true; } -void CreateInstanceDialog::next() +void CreateInstanceDialog::next(bool allowFinish) { + if (!canNext()) { + return; + } + const auto i = ui->pages->currentIndex(); const auto last = isOnLastPage(); if (last) { - if (m_singlePage) { - // just close the dialog - accept(); - } else { - finish(); + if (allowFinish) { + if (m_singlePage) { + // just close the dialog + accept(); + } else { + finish(); + } } } else { changePage(+1); @@ -195,9 +209,40 @@ void CreateInstanceDialog::next() void CreateInstanceDialog::back() { + if (!canBack()) { + return; + } + changePage(-1); } +void CreateInstanceDialog::addShortcut( + QKeySequence seq, std::function f) +{ + auto* sc = new QShortcut(seq, this); + + sc->setAutoRepeat(false); + sc->setContext(Qt::WidgetWithChildrenShortcut); + + QObject::connect(sc, &QShortcut::activated, f); +} + +void CreateInstanceDialog::addShortcutAction(QKeySequence seq, Actions a) +{ + addShortcut(seq, [this, a]{ doAction(a); }); +} + +void CreateInstanceDialog::doAction(Actions a) +{ + std::size_t i = static_cast(ui->pages->currentIndex()); + + if (i >= m_pages.size()) { + return; + } + + m_pages[i]->action(a); +} + void CreateInstanceDialog::setSinglePageImpl(const QString& instanceName) { m_singlePage = true; @@ -347,11 +392,6 @@ void CreateInstanceDialog::finish() logCreation(tr("Done.")); - // remember settings - if (ui->hideIntro->isChecked()) { - GlobalSettings::setHideCreateInstanceIntro(true); - } - // launch the new instance if (ui->launch->isChecked()) { InstanceManager::singleton().setCurrentInstance(ci.instanceName); diff --git a/src/createinstancedialog.h b/src/createinstancedialog.h index d541f45e..8e8fe517 100644 --- a/src/createinstancedialog.h +++ b/src/createinstancedialog.h @@ -27,6 +27,11 @@ class CreateInstanceDialog : public QDialog Q_OBJECT public: + enum class Actions + { + Find = 1 + }; + // instance type // enum Types @@ -107,9 +112,10 @@ public: } - // moves to the next page calls finish() if on the last one + // moves to the next page; if `allowFinish` is true, calls finish() if + // currently on the last page // - void next(); + void next(bool allowFinish=true); // moves to the previous page, if any // @@ -171,6 +177,19 @@ private: bool m_singlePage; + // creates a shortcut for the given sequence + // + void addShortcut(QKeySequence seq, std::function f); + + // creates a shortcut for the given sequence and executes the action when + // activated + // + void addShortcutAction(QKeySequence seq, Actions a); + + // calls action() with the given action on the selected page, if any + // + void doAction(Actions a); + // called from setSinglePage(), does whatever doesn't need the T // void setSinglePageImpl(const QString& instanceName); diff --git a/src/createinstancedialog.ui b/src/createinstancedialog.ui index 4d1eda1f..86fa900b 100644 --- a/src/createinstancedialog.ui +++ b/src/createinstancedialog.ui @@ -1102,6 +1102,9 @@ QTextEdit::NoWrap + + true + @@ -1112,6 +1115,9 @@ Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + Instance creation log + diff --git a/src/createinstancedialogpages.cpp b/src/createinstancedialogpages.cpp index 49b118f7..882776df 100644 --- a/src/createinstancedialogpages.cpp +++ b/src/createinstancedialogpages.cpp @@ -59,8 +59,9 @@ void PlaceholderLabel::setVisible(bool b) -Page::Page(CreateInstanceDialog& dlg) - : ui(dlg.getUI()), m_dlg(dlg), m_pc(dlg.pluginContainer()), m_skip(false) +Page::Page(CreateInstanceDialog& dlg) : + ui(dlg.getUI()), m_dlg(dlg), m_pc(dlg.pluginContainer()), + m_skip(false), m_firstActivation(true) { } @@ -80,11 +81,17 @@ bool Page::doSkip() const return false; } -void Page::activated() +void Page::doActivated(bool) { // no-op } +void Page::activated() +{ + doActivated(m_firstActivation); + m_firstActivation = false; +} + void Page::setSkip(bool b) { m_skip = b; @@ -100,6 +107,12 @@ void Page::next() m_dlg.next(); } +bool Page::action(CreateInstanceDialog::Actions a) +{ + // no-op + return false; +} + CreateInstanceDialog::Types Page::selectedInstanceType() const { @@ -139,13 +152,16 @@ CreateInstanceDialog::Paths Page::selectedPaths() const IntroPage::IntroPage(CreateInstanceDialog& dlg) - : Page(dlg) + : Page(dlg), m_skip(GlobalSettings::hideCreateInstanceIntro()) { + QObject::connect(ui->hideIntro, &QCheckBox::toggled, [&] { + GlobalSettings::setHideCreateInstanceIntro(ui->hideIntro->isChecked()); + }); } bool IntroPage::doSkip() const { - return GlobalSettings::hideCreateInstanceIntro(); + return m_skip; } @@ -207,6 +223,13 @@ void TypePage::portable() next(); } +void TypePage::doActivated(bool firstTime) +{ + if (firstTime) { + ui->createGlobal->setFocus(); + } +} + GamePage::Game::Game(IPluginGame* g) : game(g), installed(g->isInstalled()) @@ -224,6 +247,7 @@ GamePage::GamePage(CreateInstanceDialog& dlg) fillList(); m_filter.setEdit(ui->gamesFilter); + m_filter.setUpdateDelay(0); QObject::connect(&m_filter, &FilterWidget::changed, [&]{ fillList(); }); QObject::connect(ui->showAllGames, &QCheckBox::clicked, [&]{ fillList(); }); @@ -234,6 +258,18 @@ bool GamePage::ready() const return (m_selection != nullptr); } +bool GamePage::action(CreateInstanceDialog::Actions a) +{ + using Actions = CreateInstanceDialog::Actions; + + if (a == Actions::Find) { + ui->gamesFilter->setFocus(); + return true; + } + + return false; +} + IPluginGame* GamePage::selectedGame() const { if (!m_selection) { @@ -263,7 +299,8 @@ void GamePage::select(IPluginGame* game, const QString& dir) // ask the user const auto path = QFileDialog::getExistingDirectory( - &m_dlg, QObject::tr("Find game installation")); + &m_dlg, QObject::tr("Find game installation for %1") + .arg(game->gameName())); if (path.isEmpty()) { // cancelled @@ -291,11 +328,13 @@ void GamePage::select(IPluginGame* game, const QString& dir) // select this plugin, if any m_selection = checked; - selectButton(checked); // update the button associated with it in case the paths have changed updateButton(checked); + // toggle it on + selectButton(checked); + updateNavigation(); if (checked) { @@ -519,6 +558,8 @@ void GamePage::fillList() clearButtons(); + Game* firstButton = nullptr; + for (auto& g : m_games) { if (!showAll && !g->installed) { // not installed @@ -532,10 +573,18 @@ void GamePage::fillList() createGameButton(g.get()); addButton(g->button); + + if (!firstButton) { + firstButton = g.get(); + } } // browse button addButton(createCustomButton()); + + if (firstButton) { + firstButton->button->setDefault(true); + } } GamePage::Game* GamePage::checkInstallation(const QString& path, Game* g) @@ -680,7 +729,7 @@ bool VariantsPage::doSkip() const return (g->gameVariants().size() < 2); } -void VariantsPage::activated() +void VariantsPage::doActivated(bool) { auto* g = m_dlg.rawCreationInfo().game; @@ -749,6 +798,10 @@ void VariantsPage::fillList() ui->editions->addButton(b, QDialogButtonBox::AcceptRole); m_buttons.push_back(b); } + + if (!m_buttons.empty()) { + m_buttons[0]->setDefault(true); + } } @@ -759,6 +812,9 @@ NamePage::NamePage(CreateInstanceDialog& dlg) : { QObject::connect( ui->instanceName, &QLineEdit::textEdited, [&]{ onChanged(); }); + + QObject::connect( + ui->instanceName, &QLineEdit::returnPressed, [&]{ next(); }); } bool NamePage::ready() const @@ -773,7 +829,7 @@ bool NamePage::doSkip() const return (m_dlg.rawCreationInfo().type == CreateInstanceDialog::Portable); } -void NamePage::activated() +void NamePage::doActivated(bool) { auto* g = m_dlg.rawCreationInfo().game; if (!g) { @@ -863,23 +919,28 @@ PathsPage::PathsPage(CreateInstanceDialog& dlg) : m_advancedInvalid(ui->advancedDirInvalid), m_okay(false) { - using O = QObject; - using E = QLineEdit; - using B = QAbstractButton; - - O::connect(ui->location, &E::textEdited, [&]{ onChanged(); }); - O::connect(ui->base, &E::textEdited, [&]{ onChanged(); }); - O::connect(ui->downloads, &E::textEdited, [&]{ onChanged(); }); - O::connect(ui->mods, &E::textEdited, [&]{ onChanged(); }); - O::connect(ui->profiles, &E::textEdited, [&]{ onChanged(); }); - O::connect(ui->overwrite, &E::textEdited, [&]{ onChanged(); }); - - O::connect(ui->browseLocation, &B::clicked, [&]{ browse(ui->location); }); - O::connect(ui->browseBase, &B::clicked, [&]{ browse(ui->base); }); - O::connect(ui->browseDownloads, &B::clicked, [&]{ browse(ui->downloads); }); - O::connect(ui->browseMods, &B::clicked, [&]{ browse(ui->mods); }); - O::connect(ui->browseProfiles, &B::clicked, [&]{ browse(ui->profiles); }); - O::connect(ui->browseOverwrite, &B::clicked, [&]{ browse(ui->overwrite); }); + auto setEdit = [&](QLineEdit* e) { + QObject::connect(e, &QLineEdit::textEdited, [&]{ onChanged(); }); + QObject::connect(e, &QLineEdit::returnPressed, [&]{ next(); }); + }; + + auto setBrowse = [&](QAbstractButton* b, QLineEdit* e) { + QObject::connect(b, &QAbstractButton::clicked, [&]{ browse(e); }); + }; + + setEdit(ui->location); + setEdit(ui->base); + setEdit(ui->downloads); + setEdit(ui->mods); + setEdit(ui->profiles); + setEdit(ui->overwrite); + + setBrowse(ui->browseLocation, ui->location); + setBrowse(ui->browseBase, ui->base); + setBrowse(ui->browseDownloads, ui->downloads); + setBrowse(ui->browseMods, ui->mods); + setBrowse(ui->browseProfiles, ui->profiles); + setBrowse(ui->browseOverwrite, ui->overwrite); QObject::connect( ui->advancedPathOptions, &QCheckBox::clicked, [&]{ onAdvanced(); }); @@ -894,7 +955,7 @@ bool PathsPage::ready() const return m_okay; } -void PathsPage::activated() +void PathsPage::doActivated(bool firstTime) { const auto name = m_dlg.rawCreationInfo().instanceName; const auto type = m_dlg.rawCreationInfo().type; @@ -913,6 +974,10 @@ void PathsPage::activated() m_label.setText(m_dlg.rawCreationInfo().game->gameName()); m_lastInstanceName = name; m_lastType = type; + + if (firstTime) { + ui->location->setFocus(); + } } CreateInstanceDialog::Paths PathsPage::selectedPaths() const @@ -1120,7 +1185,7 @@ ConfirmationPage::ConfirmationPage(CreateInstanceDialog& dlg) { } -void ConfirmationPage::activated() +void ConfirmationPage::doActivated(bool) { ui->review->setPlainText(makeReview()); ui->creationLog->clear(); diff --git a/src/createinstancedialogpages.h b/src/createinstancedialogpages.h index 4fde089b..08953a74 100644 --- a/src/createinstancedialogpages.h +++ b/src/createinstancedialogpages.h @@ -60,7 +60,7 @@ public: // called every time a page is shown in the screen // - virtual void activated(); + void activated(); // overrides whether this page should be skipped; this is used by // CreateInstanceDialog::setSinglePage() to disable all other pages @@ -82,6 +82,12 @@ public: void next(); + // called from the dialog when an action is requested on the current page; + // returns true when handled + // + virtual bool action(CreateInstanceDialog::Actions a); + + // returns the instance type // virtual CreateInstanceDialog::Types selectedInstanceType() const; @@ -111,8 +117,14 @@ protected: CreateInstanceDialog& m_dlg; const PluginContainer& m_pc; bool m_skip; + bool m_firstActivation; + // called every time a page is shown in the screen; `firstTime` is true for + // first activation + // + virtual void doActivated(bool firstTime); + // implemented by derived classes, overridden by setSkip(true) // virtual bool doSkip() const; @@ -128,6 +140,12 @@ public: protected: bool doSkip() const override; + +private: + // the setting is only checked once when opening the dialog, or going forwards + // then back after checking the box wouldn't show the intro page any more, + // which would be unexpected + bool m_skip; }; @@ -154,6 +172,11 @@ public: // void portable(); +protected: + // focuses global instance button on first activation + // + void doActivated(bool firstTime) override; + private: CreateInstanceDialog::Types m_type; }; @@ -187,6 +210,10 @@ public: // bool ready() const override; + // handles find + // + bool action(CreateInstanceDialog::Actions a) override; + // returns the selected game // MOBase::IPluginGame* selectedGame() const override; @@ -262,7 +289,6 @@ private: // Game* findGame(MOBase::IPluginGame* game); - // creates the ui for the given game button // void createGameButton(Game* g); @@ -275,8 +301,13 @@ private: // void updateButton(Game* g); - // called when a button has been clicked; selects the game or asks the user - // for directory, depending + // game buttons are toggles, this creates the button for the given game if + // it doesn't exist and toggles it on + // + // the button might not exist if, for example: + // 1) this game is currently filtered out (not installed, doesn't match + // filter text, etc) and, + // 2) the user browses to a directory that a hidden plugin can use // void selectButton(Game* g); @@ -343,7 +374,7 @@ public: // uses the game selected in the previous page to fill the list, this must be // called every time because the user may go back in forth in the wizard // - void activated() override; + void doActivated(bool firstTime) override; // returns the selected variant, if any // @@ -388,18 +419,18 @@ public: // bool ready() const override; + // returns the instance name + // + QString selectedInstanceName() const override; + +protected: // uses the selected game to generate an instance name // // as long as the user hasn't modified the textbox, this will regenerate a new // instance name every time the selected game changes // - void activated() override; + void doActivated(bool firstTime) override; - // returns the instance name - // - QString selectedInstanceName() const override; - -protected: // returns true for portable instances // bool doSkip() const override; @@ -451,15 +482,16 @@ public: // bool ready() const override; + // returns the selected paths + // + CreateInstanceDialog::Paths selectedPaths() const override; + +protected: // resets all the paths if the instance type or instance name have changed, // the current values are kept as long as these don't change; also updates the // game name in the ui // - void activated() override; - - // returns the selected paths - // - CreateInstanceDialog::Paths selectedPaths() const override; + void doActivated(bool firstTime) override; private: // instance name the last time this page was active @@ -572,7 +604,7 @@ public: // recreates the log with the latest settings // - void activated() override; + void doActivated(bool firstTime) override; // returns the text for the log // -- cgit v1.3.1