summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorisanae <14251494+isanae@users.noreply.github.com>2020-11-08 23:55:17 -0500
committerisanae <14251494+isanae@users.noreply.github.com>2020-11-08 23:55:17 -0500
commit3cc568a852e9b64bb6e0f34f74869965d04f6a0c (patch)
tree16e4c247803ffb84e58e4e06f4e7696364ab822b
parentd7ae42d3775f88d5cd63fa2f8860bd31e18f981e (diff)
keyboard nav for create instance dialog
change the skip intro setting on click instead of on completion ctrl+f in game page
-rw-r--r--src/createinstancedialog.cpp62
-rw-r--r--src/createinstancedialog.h23
-rw-r--r--src/createinstancedialog.ui6
-rw-r--r--src/createinstancedialogpages.cpp117
-rw-r--r--src/createinstancedialogpages.h66
5 files changed, 218 insertions, 56 deletions
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<void ()> f)
+{
+ auto* sc = new QShortcut(seq, this);
+
+ sc->setAutoRepeat(false);
+ sc->setContext(Qt::WidgetWithChildrenShortcut);
+
+ QObject::connect(sc, &QShortcut::activated, f);
+}
+
+void CreateInstanceDialog::addShortcutAction(QKeySequence seq, Actions a)
+{
+ addShortcut(seq, [this, a]{ doAction(a); });
+}
+
+void CreateInstanceDialog::doAction(Actions a)
+{
+ std::size_t i = static_cast<std::size_t>(ui->pages->currentIndex());
+
+ if (i >= m_pages.size()) {
+ return;
+ }
+
+ m_pages[i]->action(a);
+}
+
void CreateInstanceDialog::setSinglePageImpl(const QString& instanceName)
{
m_singlePage = true;
@@ -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<void ()> f);
+
+ // creates a shortcut for the given sequence and executes the action when
+ // activated
+ //
+ void addShortcutAction(QKeySequence seq, Actions a);
+
+ // calls action() with the given action on the selected page, if any
+ //
+ void doAction(Actions a);
+
// called from setSinglePage(), does whatever doesn't need the T
//
void setSinglePageImpl(const QString& instanceName);
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 @@
<property name="lineWrapMode">
<enum>QTextEdit::NoWrap</enum>
</property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
</widget>
</item>
<item>
@@ -1112,6 +1115,9 @@
<property name="textInteractionFlags">
<set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
+ <property name="placeholderText">
+ <string>Instance creation log</string>
+ </property>
</widget>
</item>
<item>
diff --git a/src/createinstancedialogpages.cpp b/src/createinstancedialogpages.cpp
index 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;
+ 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); });
+ };
- 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(); });
+ setEdit(ui->location);
+ setEdit(ui->base);
+ setEdit(ui->downloads);
+ setEdit(ui->mods);
+ setEdit(ui->profiles);
+ setEdit(ui->overwrite);
- 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); });
+ 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
//