diff --git a/CHANGELOG.md b/CHANGELOG.md index a1050b3e..0476967c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ - Minor: Migrated /ban to Helix API. (#4049) - Minor: Migrated /timeout to Helix API. (#4049) - Minor: Migrated /w to Helix API. Chat command will continue to be used until February 11th 2023. (#4052) +- Minor: Make menus and placeholders display appropriate custom key combos. (#4045) - Bugfix: Connection to Twitch PubSub now recovers more reliably. (#3643, #3716) - Bugfix: Fixed `Smooth scrolling on new messages` setting sometimes hiding messages. (#4028) - Bugfix: Fixed a crash that can occur when closing and quickly reopening a split, then running a command. (#3852) diff --git a/src/controllers/hotkeys/HotkeyController.cpp b/src/controllers/hotkeys/HotkeyController.cpp index 757197f6..f45b98a6 100644 --- a/src/controllers/hotkeys/HotkeyController.cpp +++ b/src/controllers/hotkeys/HotkeyController.cpp @@ -1,6 +1,7 @@ #include "controllers/hotkeys/HotkeyController.hpp" #include "common/QLogging.hpp" +#include "controllers/hotkeys/HotkeyCategory.hpp" #include "controllers/hotkeys/HotkeyModel.hpp" #include "singletons/Settings.hpp" @@ -547,4 +548,40 @@ void HotkeyController::showHotkeyError(const std::shared_ptr &hotkey, msgBox->exec(); } +QKeySequence HotkeyController::getDisplaySequence( + HotkeyCategory category, const QString &action, + const std::optional> &arguments) const +{ + const auto &found = this->findLike(category, action, arguments); + if (found != nullptr) + { + return found->keySequence(); + } + return {}; +} + +std::shared_ptr HotkeyController::findLike( + HotkeyCategory category, const QString &action, + const std::optional> &arguments) const +{ + for (auto other : this->hotkeys_) + { + if (other->category() == category && other->action() == action) + { + if (arguments) + { + if (other->arguments() == *arguments) + { + return other; + } + } + else + { + return other; + } + } + } + return nullptr; +} + } // namespace chatterino diff --git a/src/controllers/hotkeys/HotkeyController.hpp b/src/controllers/hotkeys/HotkeyController.hpp index 069c14bf..1ea48540 100644 --- a/src/controllers/hotkeys/HotkeyController.hpp +++ b/src/controllers/hotkeys/HotkeyController.hpp @@ -8,6 +8,7 @@ #include #include +#include #include class QShortcut; @@ -33,6 +34,17 @@ public: void save() override; std::shared_ptr getHotkeyByName(QString name); + /** + * @brief returns a QKeySequence that perfoms the actions requested. + * Accepted if and only if the category matches, the action matches and arguments match. + * When arguments is present, contents of arguments must match the checked hotkey, otherwise arguments are ignored. + * For example: + * - std::nullopt (or {}) will match any hotkey satisfying category, action values, + * - {{"foo", "bar"}} will only match a hotkey that has these arguments and these arguments only + */ + QKeySequence getDisplaySequence( + HotkeyCategory category, const QString &action, + const std::optional> &arguments = {}) const; /** * @brief removes the hotkey with the oldName and inserts newHotkey at the end @@ -114,6 +126,17 @@ private: **/ static void showHotkeyError(const std::shared_ptr &hotkey, QString warning); + /** + * @brief finds a Hotkey matching category, action and arguments. + * Accepted if and only if the category matches, the action matches and arguments match. + * When arguments is present, contents of arguments must match the checked hotkey, otherwise arguments are ignored. + * For example: + * - std::nullopt (or {}) will match any hotkey satisfying category, action values, + * - {{"foo", "bar"}} will only match a hotkey that has these arguments and these arguments only + */ + std::shared_ptr findLike( + HotkeyCategory category, const QString &action, + const std::optional> &arguments = {}) const; friend class KeyboardSettingsPage; diff --git a/src/widgets/Notebook.cpp b/src/widgets/Notebook.cpp index b1ccf4a7..d90043ee 100644 --- a/src/widgets/Notebook.cpp +++ b/src/widgets/Notebook.cpp @@ -2,6 +2,8 @@ #include "Application.hpp" #include "common/QLogging.hpp" +#include "controllers/hotkeys/HotkeyCategory.hpp" +#include "controllers/hotkeys/HotkeyController.hpp" #include "singletons/Settings.hpp" #include "singletons/Theme.hpp" #include "singletons/WindowManager.hpp" @@ -48,6 +50,11 @@ Notebook::Notebook(QWidget *parent) [this](bool value) { this->setLockNotebookLayout(value); }); + this->showTabsAction_ = new QAction("Toggle visibility of tabs"); + QObject::connect(this->showTabsAction_, &QAction::triggered, [this]() { + this->setShowTabs(!this->getShowTabs()); + }); + this->updateTabVisibilityMenuAction(); this->addNotebookActionsToMenu(&this->menu_); @@ -374,12 +381,32 @@ void Notebook::setShowTabs(bool value) // show a popup upon hiding tabs if (!value && getSettings()->informOnTabVisibilityToggle.getValue()) { + auto unhideSeq = getApp()->hotkeys->getDisplaySequence( + HotkeyCategory::Window, "setTabVisibility", {{}}); + if (unhideSeq.isEmpty()) + { + unhideSeq = getApp()->hotkeys->getDisplaySequence( + HotkeyCategory::Window, "setTabVisibility", {{"toggle"}}); + } + if (unhideSeq.isEmpty()) + { + unhideSeq = getApp()->hotkeys->getDisplaySequence( + HotkeyCategory::Window, "setTabVisibility", {{"on"}}); + } + QString hotkeyInfo = "(currently unbound)"; + if (!unhideSeq.isEmpty()) + { + hotkeyInfo = + "(" + + unhideSeq.toString(QKeySequence::SequenceFormat::NativeText) + + ")"; + } QMessageBox msgBox(this->window()); msgBox.window()->setWindowTitle("Chatterino - hidden tabs"); msgBox.setText("You've just hidden your tabs."); msgBox.setInformativeText( - "You can toggle tabs by using the keyboard shortcut (Ctrl+U by " - "default) or right-clicking the tab area and selecting \"Toggle " + "You can toggle tabs by using the keyboard shortcut " + hotkeyInfo + + " or right-clicking the tab area and selecting \"Toggle " "visibility of tabs\"."); msgBox.addButton(QMessageBox::Ok); auto *dsaButton = @@ -394,6 +421,34 @@ void Notebook::setShowTabs(bool value) getSettings()->informOnTabVisibilityToggle.setValue(false); } } + updateTabVisibilityMenuAction(); +} + +void Notebook::updateTabVisibilityMenuAction() +{ + auto toggleSeq = getApp()->hotkeys->getDisplaySequence( + HotkeyCategory::Window, "setTabVisibility", {{}}); + if (toggleSeq.isEmpty()) + { + toggleSeq = getApp()->hotkeys->getDisplaySequence( + HotkeyCategory::Window, "setTabVisibility", {{"toggle"}}); + } + + if (toggleSeq.isEmpty()) + { + // show contextual shortcuts + if (this->getShowTabs()) + { + toggleSeq = getApp()->hotkeys->getDisplaySequence( + HotkeyCategory::Window, "setTabVisibility", {{"off"}}); + } + else if (!this->getShowTabs()) + { + toggleSeq = getApp()->hotkeys->getDisplaySequence( + HotkeyCategory::Window, "setTabVisibility", {{"on"}}); + } + } + this->showTabsAction_->setShortcut(toggleSeq); } bool Notebook::getShowAddButton() const @@ -919,12 +974,7 @@ void Notebook::setLockNotebookLayout(bool value) void Notebook::addNotebookActionsToMenu(QMenu *menu) { - menu->addAction( - "Toggle visibility of tabs", - [this]() { - this->setShowTabs(!this->getShowTabs()); - }, - QKeySequence("Ctrl+U")); + menu->addAction(this->showTabsAction_); menu->addAction(this->lockNotebookLayoutAction_); } diff --git a/src/widgets/Notebook.hpp b/src/widgets/Notebook.hpp index f69b90b8..743b3314 100644 --- a/src/widgets/Notebook.hpp +++ b/src/widgets/Notebook.hpp @@ -86,6 +86,7 @@ protected: } private: + void updateTabVisibilityMenuAction(); void resizeAddButton(); bool containsPage(QWidget *page); @@ -111,6 +112,7 @@ private: bool lockNotebookLayout_ = false; NotebookTabLocation tabLocation_ = NotebookTabLocation::Top; QAction *lockNotebookLayoutAction_; + QAction *showTabsAction_; }; class SplitNotebook : public Notebook diff --git a/src/widgets/dialogs/SettingsDialog.cpp b/src/widgets/dialogs/SettingsDialog.cpp index 00f2bd59..d6969cbf 100644 --- a/src/widgets/dialogs/SettingsDialog.cpp +++ b/src/widgets/dialogs/SettingsDialog.cpp @@ -54,6 +54,7 @@ SettingsDialog::SettingsDialog(QWidget *parent) void SettingsDialog::addShortcuts() { + this->setSearchPlaceholderText(); HotkeyController::HotkeyMap actions{ {"search", [this](std::vector) -> QString { @@ -71,6 +72,19 @@ void SettingsDialog::addShortcuts() this->shortcuts_ = getApp()->hotkeys->shortcutsForCategory( HotkeyCategory::PopupWindow, actions, this); } +void SettingsDialog::setSearchPlaceholderText() +{ + QString searchHotkey; + auto searchSeq = getApp()->hotkeys->getDisplaySequence( + HotkeyCategory::PopupWindow, "search"); + if (!searchSeq.isEmpty()) + { + searchHotkey = + "(" + searchSeq.toString(QKeySequence::SequenceFormat::NativeText) + + ")"; + } + this->ui_.search->setPlaceholderText("Find in settings... " + searchHotkey); +} void SettingsDialog::initUi() { @@ -85,7 +99,7 @@ void SettingsDialog::initUi() .withoutMargin() .emplace() .assign(&this->ui_.search); - edit->setPlaceholderText("Find in settings... (Ctrl+F by default)"); + this->setSearchPlaceholderText(); edit->setClearButtonEnabled(true); edit->findChild()->setIcon( QPixmap(":/buttons/clearSearch.png")); diff --git a/src/widgets/dialogs/SettingsDialog.hpp b/src/widgets/dialogs/SettingsDialog.hpp index 5176b76d..b4705bc9 100644 --- a/src/widgets/dialogs/SettingsDialog.hpp +++ b/src/widgets/dialogs/SettingsDialog.hpp @@ -61,6 +61,7 @@ private: void onOkClicked(); void onCancelClicked(); void addShortcuts() override; + void setSearchPlaceholderText(); struct { QWidget *tabContainerContainer{}; diff --git a/src/widgets/helper/NotebookTab.cpp b/src/widgets/helper/NotebookTab.cpp index 8fdbe74e..b9276889 100644 --- a/src/widgets/helper/NotebookTab.cpp +++ b/src/widgets/helper/NotebookTab.cpp @@ -2,6 +2,8 @@ #include "Application.hpp" #include "common/Common.hpp" +#include "controllers/hotkeys/HotkeyCategory.hpp" +#include "controllers/hotkeys/HotkeyController.hpp" #include "singletons/Fonts.hpp" #include "singletons/Settings.hpp" #include "singletons/Theme.hpp" @@ -60,12 +62,15 @@ NotebookTab::NotebookTab(Notebook *notebook) this->showRenameDialog(); }); + // XXX: this doesn't update after changing hotkeys + this->menu_.addAction( "Close Tab", [=]() { this->notebook_->removePage(this->page); }, - QKeySequence("Ctrl+Shift+W")); + getApp()->hotkeys->getDisplaySequence(HotkeyCategory::Window, + "removeTab")); this->menu_.addAction( "Popup Tab", @@ -75,7 +80,8 @@ NotebookTab::NotebookTab(Notebook *notebook) container->popup(); } }, - QKeySequence("Ctrl+Shift+N")); + getApp()->hotkeys->getDisplaySequence(HotkeyCategory::Window, "popup", + {{"window"}})); highlightNewMessagesAction_ = new QAction("Mark Tab as Unread on New Messages", &this->menu_); diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index 1e704bb4..22104129 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -3,6 +3,8 @@ #include "Application.hpp" #include "common/QLogging.hpp" #include "common/Version.hpp" +#include "controllers/hotkeys/HotkeyCategory.hpp" +#include "controllers/hotkeys/HotkeyController.hpp" #include "singletons/Fonts.hpp" #include "singletons/NativeMessaging.hpp" #include "singletons/Paths.hpp" @@ -193,7 +195,17 @@ void GeneralPage::initLayout(GeneralPageView &layout) #endif if (!BaseWindow::supportsCustomWindowFrame()) { - layout.addCheckbox("Show preferences button (Ctrl+P to show)", + auto settingsSeq = getApp()->hotkeys->getDisplaySequence( + HotkeyCategory::Window, "openSettings"); + QString shortcut = " (no key bound to open them otherwise)"; + // TODO: maybe prevent the user from locking themselves out of the settings? + if (!settingsSeq.isEmpty()) + { + shortcut = QStringLiteral(" (%1 to show)") + .arg(settingsSeq.toString( + QKeySequence::SequenceFormat::NativeText)); + } + layout.addCheckbox("Show preferences button" + shortcut, s.hidePreferencesButton, true); layout.addCheckbox("Show user button", s.hideUserButton, true); } @@ -559,8 +571,18 @@ void GeneralPage::initLayout(GeneralPageView &layout) layout.addCheckbox("Title", s.headerStreamTitle); layout.addSubtitle("R9K"); + auto toggleLocalr9kSeq = getApp()->hotkeys->getDisplaySequence( + HotkeyCategory::Window, "toggleLocalR9K"); + QString toggleLocalr9kShortcut = + "an assigned hotkey (Window -> Toggle local R9K)"; + if (!toggleLocalr9kSeq.isEmpty()) + { + toggleLocalr9kShortcut = toggleLocalr9kSeq.toString( + QKeySequence::SequenceFormat::NativeText); + } layout.addDescription("Hide similar messages. Toggle hidden " - "messages by pressing Ctrl+H."); + "messages by pressing " + + toggleLocalr9kShortcut + "."); layout.addCheckbox("Hide similar messages", s.similarityEnabled); //layout.addCheckbox("Gray out matches", s.colorSimilarDisabled); layout.addCheckbox("By the same user", s.hideSimilarBySameUser); diff --git a/src/widgets/splits/SplitHeader.cpp b/src/widgets/splits/SplitHeader.cpp index 622218d3..a1ff9448 100644 --- a/src/widgets/splits/SplitHeader.cpp +++ b/src/widgets/splits/SplitHeader.cpp @@ -3,6 +3,9 @@ #include "Application.hpp" #include "controllers/accounts/AccountController.hpp" #include "controllers/commands/CommandController.hpp" +#include "controllers/hotkeys/Hotkey.hpp" +#include "controllers/hotkeys/HotkeyCategory.hpp" +#include "controllers/hotkeys/HotkeyController.hpp" #include "controllers/notifications/NotificationController.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchIrcServer.hpp" @@ -337,21 +340,27 @@ void SplitHeader::initializeLayout() std::unique_ptr SplitHeader::createMainMenu() { // top level menu + const auto &h = getApp()->hotkeys; auto menu = std::make_unique(); - menu->addAction("Change channel", this->split_, &Split::changeChannel, - QKeySequence("Ctrl+R")); + menu->addAction( + "Change channel", this->split_, &Split::changeChannel, + h->getDisplaySequence(HotkeyCategory::Split, "changeChannel")); menu->addAction("Close", this->split_, &Split::deleteFromContainer, - QKeySequence("Ctrl+W")); + h->getDisplaySequence(HotkeyCategory::Split, "delete")); menu->addSeparator(); - menu->addAction("Popup", this->split_, &Split::popup, - QKeySequence("Ctrl+N")); + menu->addAction( + "Popup", this->split_, &Split::popup, + h->getDisplaySequence(HotkeyCategory::Window, "popup", {{"split"}})); menu->addAction("Search", this->split_, &Split::showSearch, - QKeySequence("Ctrl+F")); - menu->addAction("Set filters", this->split_, &Split::setFiltersDialog); + h->getDisplaySequence(HotkeyCategory::Split, "showSearch")); + menu->addAction( + "Set filters", this->split_, &Split::setFiltersDialog, + h->getDisplaySequence(HotkeyCategory::Split, "pickFilters")); menu->addSeparator(); #ifdef USEWEBENGINE - this->dropdownMenu.addAction("Start watching", this->split_, - &Split::startWatching); + this->dropdownMenu.addAction( + "Start watching", this->split_, &Split::startWatching; + h->getDisplaySequence(HotkeyCategory::Split, "startWatching")); #endif auto *twitchChannel = @@ -359,24 +368,31 @@ std::unique_ptr SplitHeader::createMainMenu() if (twitchChannel) { - menu->addAction(OPEN_IN_BROWSER, this->split_, &Split::openInBrowser); + menu->addAction( + OPEN_IN_BROWSER, this->split_, &Split::openInBrowser, + h->getDisplaySequence(HotkeyCategory::Split, "openInBrowser")); #ifndef USEWEBENGINE menu->addAction(OPEN_PLAYER_IN_BROWSER, this->split_, &Split::openBrowserPlayer); #endif - menu->addAction(OPEN_IN_STREAMLINK, this->split_, - &Split::openInStreamlink); + menu->addAction( + OPEN_IN_STREAMLINK, this->split_, &Split::openInStreamlink, + h->getDisplaySequence(HotkeyCategory::Split, "openInStreamlink")); if (!getSettings()->customURIScheme.getValue().isEmpty()) { menu->addAction("Open in custom player", this->split_, - &Split::openWithCustomScheme); + &Split::openWithCustomScheme, + h->getDisplaySequence(HotkeyCategory::Split, + "openInCustomPlayer")); } if (this->split_->getChannel()->hasModRights()) { - menu->addAction(OPEN_MOD_VIEW_IN_BROWSER, this->split_, - &Split::openModViewInBrowser); + menu->addAction( + OPEN_MOD_VIEW_IN_BROWSER, this->split_, + &Split::openModViewInBrowser, + h->getDisplaySequence(HotkeyCategory::Split, "openModView")); } menu->addAction( @@ -384,7 +400,7 @@ std::unique_ptr SplitHeader::createMainMenu() [twitchChannel] { twitchChannel->createClip(); }, - QKeySequence("Alt+X")) + h->getDisplaySequence(HotkeyCategory::Split, "createClip")) ->setVisible(twitchChannel->isLive()); menu->addSeparator(); @@ -392,24 +408,35 @@ std::unique_ptr SplitHeader::createMainMenu() if (this->split_->getChannel()->getType() == Channel::Type::TwitchWhispers) { - menu->addAction(OPEN_WHISPERS_IN_BROWSER, this->split_, - &Split::openWhispersInBrowser); + menu->addAction( + OPEN_WHISPERS_IN_BROWSER, this->split_, + &Split::openWhispersInBrowser, + h->getDisplaySequence(HotkeyCategory::Split, "openInBrowser")); menu->addSeparator(); } // reload / reconnect if (this->split_->getChannel()->canReconnect()) { - menu->addAction("Reconnect", this, SLOT(reconnect()), - QKeySequence("Ctrl+F5")); + menu->addAction( + "Reconnect", this, SLOT(reconnect()), + h->getDisplaySequence(HotkeyCategory::Split, "reconnect")); } if (twitchChannel) { + auto bothSeq = + h->getDisplaySequence(HotkeyCategory::Split, "reloadEmotes", {{}}); + auto channelSeq = h->getDisplaySequence(HotkeyCategory::Split, + "reloadEmotes", {{"channel"}}); + auto subSeq = h->getDisplaySequence(HotkeyCategory::Split, + "reloadEmotes", {{"subscriber"}}); menu->addAction("Reload channel emotes", this, - SLOT(reloadChannelEmotes()), QKeySequence("F5")); + SLOT(reloadChannelEmotes()), + channelSeq.isEmpty() ? bothSeq : channelSeq); menu->addAction("Reload subscriber emotes", this, - SLOT(reloadSubscriberEmotes()), QKeySequence("F5")); + SLOT(reloadSubscriberEmotes()), + subSeq.isEmpty() ? bothSeq : subSeq); } menu->addSeparator(); @@ -427,9 +454,20 @@ std::unique_ptr SplitHeader::createMainMenu() // sub menu auto moreMenu = new QMenu("More", this); - moreMenu->addAction("Toggle moderation mode", this->split_, [this]() { - this->split_->setModerationMode(!this->split_->getModerationMode()); - }); + auto modModeSeq = h->getDisplaySequence(HotkeyCategory::Split, + "setModerationMode", {{"toggle"}}); + if (modModeSeq.isEmpty()) + { + modModeSeq = h->getDisplaySequence(HotkeyCategory::Split, + "setModerationMode", {{}}); + // this makes a full std::optional<> with an empty vector inside + } + moreMenu->addAction( + "Toggle moderation mode", this->split_, + [this]() { + this->split_->setModerationMode(!this->split_->getModerationMode()); + }, + modModeSeq); if (this->split_->getChannel()->getType() == Channel::Type::TwitchMentions) { @@ -450,8 +488,9 @@ std::unique_ptr SplitHeader::createMainMenu() if (twitchChannel) { - moreMenu->addAction("Show viewer list", this->split_, - &Split::showViewerList); + moreMenu->addAction( + "Show viewer list", this->split_, &Split::showViewerList, + h->getDisplaySequence(HotkeyCategory::Split, "openViewerList")); moreMenu->addAction("Subscribe", this->split_, &Split::openSubPage); @@ -459,6 +498,16 @@ std::unique_ptr SplitHeader::createMainMenu() action->setText("Notify when live"); action->setCheckable(true); + auto notifySeq = h->getDisplaySequence( + HotkeyCategory::Split, "setChannelNotification", {{"toggle"}}); + if (notifySeq.isEmpty()) + { + notifySeq = h->getDisplaySequence(HotkeyCategory::Split, + "setChannelNotification", {{}}); + // this makes a full std::optional<> with an empty vector inside + } + action->setShortcut(notifySeq); + QObject::connect(moreMenu, &QMenu::aboutToShow, this, [action, this]() { action->setChecked(getApp()->notifications->isChannelNotified( this->split_->getChannel()->getName(), Platform::Twitch)); @@ -490,7 +539,9 @@ std::unique_ptr SplitHeader::createMainMenu() } moreMenu->addSeparator(); - moreMenu->addAction("Clear messages", this->split_, &Split::clear); + moreMenu->addAction( + "Clear messages", this->split_, &Split::clear, + h->getDisplaySequence(HotkeyCategory::Split, "clearMessages")); // moreMenu->addSeparator(); // moreMenu->addAction("Show changelog", this, // SLOT(moreMenuShowChangelog()));