From c7b22939d56152ab35067780b2fdf944cc9fc2c4 Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Sat, 27 May 2023 14:04:30 +0000 Subject: [PATCH] Improve editing of hotkeys (#4628) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + src/controllers/hotkeys/ActionNames.hpp | 229 +++++++++++++++++----- src/controllers/hotkeys/HotkeyHelpers.cpp | 20 ++ src/controllers/hotkeys/HotkeyHelpers.hpp | 5 + src/widgets/dialogs/EditHotkeyDialog.cpp | 226 +++++++++++++++------ src/widgets/dialogs/EditHotkeyDialog.hpp | 1 + src/widgets/dialogs/EditHotkeyDialog.ui | 55 ++++-- 7 files changed, 412 insertions(+), 125 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45e57cddd..68eadc707 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Minor: Add an icon showing when streamer mode is enabled (#4410) - Minor: Added `/shoutout ` commands to shoutout specified user. (#4638) +- Minor: Improved editing hotkeys. (#4628) - Dev: Added command to set Qt's logging filter/rules at runtime (`/c2-set-logging-rules`). (#4637) - Dev: Added the ability to see & load custom themes from the Themes directory. No stable promises are made of this feature, changes might be made that breaks custom themes without notice. (#4570) - Dev: Added test cases for emote and tab completion. (#4644) diff --git a/src/controllers/hotkeys/ActionNames.hpp b/src/controllers/hotkeys/ActionNames.hpp index 8d5700ac4..58a2bb323 100644 --- a/src/controllers/hotkeys/ActionNames.hpp +++ b/src/controllers/hotkeys/ActionNames.hpp @@ -5,6 +5,20 @@ #include #include +#include + +inline const std::vector>> + HOTKEY_ARG_ON_OFF_TOGGLE = { + {"Toggle", {}}, + {"Set to on", {"on"}}, + {"Set to off", {"off"}}, +}; + +inline const std::vector>> + HOTKEY_ARG_WITH_OR_WITHOUT_SELECTION = { + {"No", {"withoutSelection"}}, + {"Yes", {"withSelection"}}, +}; namespace chatterino { @@ -13,6 +27,9 @@ struct ActionDefinition { // displayName is the value that would be shown to a user when they edit or create a hotkey for an action QString displayName; + // argumentDescription is a description of the arguments in a format of + // " [optional arg: possible + // values]" QString argumentDescription = ""; // minCountArguments is the minimum amount of arguments the action accepts @@ -21,6 +38,20 @@ struct ActionDefinition { // maxCountArguments is the maximum amount of arguments the action accepts uint8_t maxCountArguments = minCountArguments; + + // possibleArguments is empty or contains all possible argument values, + // it is an ordered mapping from option name (what the user sees) to + // arguments (what the action code will see). + // As std::map does not guarantee order this is a std::vector<...> + std::vector>> possibleArguments = + {}; + + // When possibleArguments are present this should be a string like + // "Direction:" which will be shown before the values from + // possibleArguments in the UI. Otherwise, it should be empty. + QString argumentsPrompt = ""; + // A more detailed description of what argumentsPrompt means + QString argumentsPromptHover = ""; }; using ActionDefinitionMap = std::map; @@ -39,9 +70,15 @@ inline const std::map actionNames{ }}, {"scrollPage", ActionDefinition{ - "Scroll", - "", - 1, + .displayName = "Scroll", + .argumentDescription = "", + .minCountArguments = 1, + .maxCountArguments = 1, + .possibleArguments{ + {"Up", {"up"}}, + {"Down", {"down"}}, + }, + .argumentsPrompt = "Direction:", }}, {"search", ActionDefinition{"Focus search box"}}, {"execModeratorAction", @@ -57,9 +94,19 @@ inline const std::map actionNames{ {"delete", ActionDefinition{"Close"}}, {"focus", ActionDefinition{ - "Focus neighbouring split", - "", - 1, + .displayName = "Focus neighbouring split", + .argumentDescription = "", + .minCountArguments = 1, + .maxCountArguments = 1, + .possibleArguments{ + {"Up", {"up"}}, + {"Down", {"down"}}, + {"Left", {"left"}}, + {"Right", {"right"}}, + }, + .argumentsPrompt = "Direction:", + .argumentsPromptHover = + "Which direction to look for a split to focus?", }}, {"openInBrowser", ActionDefinition{"Open channel in browser"}}, {"openInCustomPlayer", @@ -71,10 +118,18 @@ inline const std::map actionNames{ {"reconnect", ActionDefinition{"Reconnect to chat"}}, {"reloadEmotes", ActionDefinition{ - "Reload emotes", - "[channel or subscriber]", - 0, - 1, + .displayName = "Reload emotes", + .argumentDescription = + "[type: channel or subscriber; default: all emotes]", + .minCountArguments = 0, + .maxCountArguments = 1, + .possibleArguments{ + {"All emotes", {}}, + {"Channel emotes only", {"channel"}}, + {"Subscriber emotes only", {"subscriber"}}, + }, + .argumentsPrompt = "Emote type:", + .argumentsPromptHover = "Which emotes should Chatterino reload", }}, {"runCommand", ActionDefinition{ @@ -84,25 +139,41 @@ inline const std::map actionNames{ }}, {"scrollPage", ActionDefinition{ - "Scroll", - "", - 1, + .displayName = "Scroll", + .argumentDescription = "", + .minCountArguments = 1, + .maxCountArguments = 1, + .possibleArguments{ + {"Up", {"up"}}, + {"Down", {"down"}}, + }, + .argumentsPrompt = "Direction:", + .argumentsPromptHover = + "Which direction do you want to see more messages", }}, {"scrollToBottom", ActionDefinition{"Scroll to the bottom"}}, {"scrollToTop", ActionDefinition{"Scroll to the top"}}, {"setChannelNotification", ActionDefinition{ - "Set channel live notification", - "[on or off. default: toggle]", - 0, - 1, + .displayName = "Set channel live notification", + .argumentDescription = "[on or off. default: toggle]", + .minCountArguments = 0, + .maxCountArguments = 1, + .possibleArguments = HOTKEY_ARG_ON_OFF_TOGGLE, + .argumentsPrompt = "New value:", + .argumentsPromptHover = "Should the channel live notification be " + "enabled, disabled or toggled", }}, {"setModerationMode", ActionDefinition{ - "Set moderation mode", - "[on or off. default: toggle]", - 0, - 1, + .displayName = "Set moderation mode", + .argumentDescription = "[on or off. default: toggle]", + .minCountArguments = 0, + .maxCountArguments = 1, + .possibleArguments = HOTKEY_ARG_ON_OFF_TOGGLE, + .argumentsPrompt = "New value:", + .argumentsPromptHover = + "Should the moderation mode be enabled, disabled or toggled", }}, {"showSearch", ActionDefinition{"Search current channel"}}, {"showGlobalSearch", ActionDefinition{"Search all channels"}}, @@ -114,21 +185,38 @@ inline const std::map actionNames{ {"clear", ActionDefinition{"Clear message"}}, {"copy", ActionDefinition{ - "Copy", - "", - 1, + .displayName = "Copy", + .argumentDescription = + "", + .minCountArguments = 1, + .possibleArguments{ + {"Automatic", {"auto"}}, + {"Split", {"split"}}, + {"Split Input", {"splitInput"}}, + }, + .argumentsPrompt = "Source of text:", }}, {"cursorToStart", ActionDefinition{ - "To start of message", - "", - 1, + .displayName = "To start of message", + .argumentDescription = + "", + .minCountArguments = 1, + .maxCountArguments = 1, + .possibleArguments = HOTKEY_ARG_WITH_OR_WITHOUT_SELECTION, + .argumentsPrompt = "Select text from cursor to start:", + // XXX: write a hover for this that doesn't suck }}, {"cursorToEnd", ActionDefinition{ - "To end of message", - "", - 1, + .displayName = "To end of message", + .argumentDescription = + "", + .minCountArguments = 1, + .maxCountArguments = 1, + .possibleArguments = HOTKEY_ARG_WITH_OR_WITHOUT_SELECTION, + .argumentsPrompt = "Select text from cursor to end:", + // XXX: write a hover for this that doesn't suck }}, {"nextMessage", ActionDefinition{"Choose next sent message"}}, {"openEmotesPopup", ActionDefinition{"Open emotes list"}}, @@ -140,10 +228,16 @@ inline const std::map actionNames{ {"selectWord", ActionDefinition{"Select word"}}, {"sendMessage", ActionDefinition{ - "Send message", - "[keepInput to not clear the text after sending]", - 0, - 1, + .displayName = "Send message", + .argumentDescription = + "[keepInput to not clear the text after sending]", + .minCountArguments = 0, + .maxCountArguments = 1, + .possibleArguments{ + {"Default behavior", {}}, + {"Keep message in input after sending it", {"keepInput"}}, + }, + .argumentsPrompt = "Behavior:", }}, {"undo", ActionDefinition{"Undo"}}, @@ -163,7 +257,7 @@ inline const std::map actionNames{ {"moveTab", ActionDefinition{ "Move tab", - "", + "", 1, }}, {"newSplit", ActionDefinition{"Create a new split"}}, @@ -172,40 +266,73 @@ inline const std::map actionNames{ {"openTab", ActionDefinition{ "Select tab", - "", + "", 1, }}, {"openQuickSwitcher", ActionDefinition{"Open the quick switcher"}}, {"popup", ActionDefinition{ - "New popup", - "", - 1, + .displayName = "New popup", + .argumentDescription = "", + .minCountArguments = 1, + .maxCountArguments = 1, + .possibleArguments{ + {"Focused Split", {"split"}}, + {"Entire Tab", {"window"}}, + }, + .argumentsPrompt = "Include:", + .argumentsPromptHover = + "What should be included in the new popup", }}, {"quit", ActionDefinition{"Quit Chatterino"}}, {"removeTab", ActionDefinition{"Remove current tab"}}, {"reopenSplit", ActionDefinition{"Reopen closed split"}}, {"setStreamerMode", ActionDefinition{ - "Set streamer mode", - "[on, off, toggle, or auto. default: toggle]", - 0, - 1, + .displayName = "Set streamer mode", + .argumentDescription = + "[on, off, toggle, or auto. default: toggle]", + .minCountArguments = 0, + .maxCountArguments = 1, + .possibleArguments = + { + {"Toggle on/off", {}}, + {"Set to on", {"on"}}, + {"Set to off", {"off"}}, + {"Set to automatic", {"auto"}}, + }, + .argumentsPrompt = "New value:", + .argumentsPromptHover = + "Should streamer mode be enabled, disabled, toggled (on/off) " + "or set to auto", }}, {"toggleLocalR9K", ActionDefinition{"Toggle local R9K"}}, {"zoom", ActionDefinition{ - "Zoom in/out", - "", - 1, + .displayName = "Zoom in/out", + .argumentDescription = "Argument:", + .minCountArguments = 1, + .maxCountArguments = 1, + .possibleArguments = + { + {"Zoom in", {"in"}}, + {"Zoom out", {"out"}}, + {"Reset zoom", {"reset"}}, + }, + .argumentsPrompt = "Option:", }}, {"setTabVisibility", ActionDefinition{ - "Set tab visibility", - "[on, off, or toggle. default: toggle]", - 0, - 1, - }}}}, + .displayName = "Set tab visibility", + .argumentDescription = "[on, off, or toggle. default: toggle]", + .minCountArguments = 0, + .maxCountArguments = 1, + .possibleArguments = HOTKEY_ARG_ON_OFF_TOGGLE, + .argumentsPrompt = "New value:", + .argumentsPromptHover = + "Should the tabs be enabled, disabled or toggled.", + }}, + }}, }; } // namespace chatterino diff --git a/src/controllers/hotkeys/HotkeyHelpers.cpp b/src/controllers/hotkeys/HotkeyHelpers.cpp index d998d7665..2859ad6d2 100644 --- a/src/controllers/hotkeys/HotkeyHelpers.cpp +++ b/src/controllers/hotkeys/HotkeyHelpers.cpp @@ -1,5 +1,9 @@ #include "controllers/hotkeys/HotkeyHelpers.hpp" +#include "controllers/hotkeys/ActionNames.hpp" +#include "controllers/hotkeys/HotkeyCategory.hpp" + +#include #include namespace chatterino { @@ -27,4 +31,20 @@ std::vector parseHotkeyArguments(QString argumentString) return arguments; } +boost::optional findHotkeyActionDefinition( + HotkeyCategory category, const QString &action) +{ + auto allActions = actionNames.find(category); + if (allActions != actionNames.end()) + { + const auto &actionsMap = allActions->second; + auto definition = actionsMap.find(action); + if (definition != actionsMap.end()) + { + return {definition->second}; + } + } + return {}; +} + } // namespace chatterino diff --git a/src/controllers/hotkeys/HotkeyHelpers.hpp b/src/controllers/hotkeys/HotkeyHelpers.hpp index 4e63569ff..dfbdb6f2d 100644 --- a/src/controllers/hotkeys/HotkeyHelpers.hpp +++ b/src/controllers/hotkeys/HotkeyHelpers.hpp @@ -1,5 +1,8 @@ #pragma once +#include "controllers/hotkeys/ActionNames.hpp" + +#include #include #include @@ -7,5 +10,7 @@ namespace chatterino { std::vector parseHotkeyArguments(QString argumentString); +boost::optional findHotkeyActionDefinition( + HotkeyCategory category, const QString &action); } // namespace chatterino diff --git a/src/widgets/dialogs/EditHotkeyDialog.cpp b/src/widgets/dialogs/EditHotkeyDialog.cpp index d1bac90cc..e0fe23291 100644 --- a/src/widgets/dialogs/EditHotkeyDialog.cpp +++ b/src/widgets/dialogs/EditHotkeyDialog.cpp @@ -17,6 +17,14 @@ EditHotkeyDialog::EditHotkeyDialog(const std::shared_ptr hotkey, , data_(hotkey) { this->ui_->setupUi(this); + this->setStyleSheet(R"(QToolTip { + padding: 2px; + background-color: #333333; + border: 1px solid #545454; + color: white; +})"); + this->ui_->easyArgsPicker->setVisible(false); + this->ui_->easyArgsLabel->setVisible(false); // dynamically add category names to the category picker for (const auto &[_, hotkeyCategory] : getApp()->hotkeys->categories()) { @@ -28,34 +36,7 @@ EditHotkeyDialog::EditHotkeyDialog(const std::shared_ptr hotkey, if (hotkey) { - if (!hotkey->validAction()) - { - this->showEditError("Invalid action, make sure you select the " - "correct action before saving."); - } - - // editing a hotkey - - // update pickers/input boxes to values from Hotkey object - this->ui_->categoryPicker->setCurrentIndex(size_t(hotkey->category())); - this->ui_->keyComboEdit->setKeySequence( - QKeySequence::fromString(hotkey->keySequence().toString())); - this->ui_->nameEdit->setText(hotkey->name()); - // update arguments - QString argsText; - bool first = true; - for (const auto &arg : hotkey->arguments()) - { - if (!first) - { - argsText += '\n'; - } - - argsText += arg; - - first = false; - } - this->ui_->argumentsEdit->setPlainText(argsText); + this->setFromHotkey(hotkey); } else { @@ -66,6 +47,96 @@ EditHotkeyDialog::EditHotkeyDialog(const std::shared_ptr hotkey, this->ui_->argumentsEdit->setPlainText(""); } } +void EditHotkeyDialog::setFromHotkey(std::shared_ptr hotkey) +{ + if (!hotkey->validAction()) + { + this->showEditError("Invalid action, make sure you select the " + "correct action before saving."); + } + + // editing a hotkey + + // update pickers/input boxes to values from Hotkey object + this->ui_->categoryPicker->setCurrentIndex(size_t(hotkey->category())); + this->ui_->keyComboEdit->setKeySequence( + QKeySequence::fromString(hotkey->keySequence().toString())); + this->ui_->nameEdit->setText(hotkey->name()); + + auto def = findHotkeyActionDefinition(hotkey->category(), hotkey->action()); + if (def.has_value() && !def->possibleArguments.empty()) + { + qCDebug(chatterinoHotkeys) << "Enabled easy picker and arg edit " + "because we have arguments from hotkey"; + this->ui_->easyArgsLabel->setVisible(true); + this->ui_->easyArgsPicker->setVisible(true); + + this->ui_->argumentsEdit->setVisible(false); + this->ui_->argumentsLabel->setVisible(false); + this->ui_->argumentsDescription->setVisible(false); + + this->ui_->easyArgsPicker->clear(); + this->ui_->easyArgsLabel->setText(def->argumentsPrompt); + this->ui_->easyArgsLabel->setToolTip(def->argumentsPromptHover); + int matchIdx = -1; + for (int i = 0; i < def->possibleArguments.size(); i++) + { + const auto &[displayText, argData] = def->possibleArguments.at(i); + this->ui_->easyArgsPicker->addItem(displayText); + + // check if matches + if (argData.size() != hotkey->arguments().size()) + { + continue; + } + bool matches = true; + for (int j = 0; j < argData.size(); j++) + { + if (argData.at(j) != hotkey->arguments().at(j)) + { + matches = false; + break; + } + } + if (matches) + { + matchIdx = i; + } + } + if (matchIdx != -1) + { + this->ui_->easyArgsPicker->setCurrentIndex(matchIdx); + return; + } + + qCDebug(chatterinoHotkeys) + << "Did not match hotkey arguments for " << hotkey->toString() + << "using text edit instead of easy picker"; + this->showEditError("Arguments do not match what's expected. The " + "argument picker is not available."); + this->ui_->easyArgsLabel->setVisible(false); + this->ui_->easyArgsPicker->setVisible(false); + + this->ui_->argumentsEdit->setVisible(true); + this->ui_->argumentsLabel->setVisible(true); + this->ui_->argumentsDescription->setVisible(true); + } + // update arguments + QString argsText; + bool first = true; + for (const auto &arg : hotkey->arguments()) + { + if (!first) + { + argsText += '\n'; + } + + argsText += arg; + + first = false; + } + this->ui_->argumentsEdit->setPlainText(argsText); +} EditHotkeyDialog::~EditHotkeyDialog() { @@ -151,6 +222,14 @@ void EditHotkeyDialog::afterEdit() action = actionTemp.toString(); } + auto def = findHotkeyActionDefinition(*category, action); + if (def.has_value() && this->ui_->easyArgsPicker->isVisible()) + { + arguments = + def->possibleArguments.at(this->ui_->easyArgsPicker->currentIndex()) + .second; + } + auto hotkey = std::make_shared( *category, this->ui_->keyComboEdit->keySequence(), action, arguments, nameText); @@ -263,44 +342,69 @@ void EditHotkeyDialog::updateArgumentsInput() } const ActionDefinition &def = definition->second; - if (def.maxCountArguments != 0) - { - QString text = - "Arguments wrapped in <> are required.\nArguments wrapped in " - "[] " - "are optional.\nArguments are separated by a newline."; - if (!def.argumentDescription.isEmpty()) - { - this->ui_->argumentsDescription->setVisible(true); - this->ui_->argumentsDescription->setText( - def.argumentDescription); - } - else - { - this->ui_->argumentsDescription->setVisible(false); - } - - text = QString("Arguments wrapped in <> are required."); - if (def.maxCountArguments != def.minCountArguments) - { - text += QString("\nArguments wrapped in [] are optional."); - } - - text += "\nArguments are separated by a newline."; - - this->ui_->argumentsEdit->setEnabled(true); - this->ui_->argumentsEdit->setPlaceholderText(text); - - this->ui_->argumentsLabel->setVisible(true); - this->ui_->argumentsDescription->setVisible(true); - this->ui_->argumentsEdit->setVisible(true); - } - else + if (def.maxCountArguments == 0) { + qCDebug(chatterinoHotkeys) << "Disabled easy picker and arg edit " + "because we don't have any arguments"; this->ui_->argumentsLabel->setVisible(false); this->ui_->argumentsDescription->setVisible(false); this->ui_->argumentsEdit->setVisible(false); + + this->ui_->easyArgsLabel->setVisible(false); + this->ui_->easyArgsPicker->setVisible(false); + return; } + if (!def.argumentDescription.isEmpty()) + { + this->ui_->argumentsDescription->setVisible(true); + this->ui_->argumentsDescription->setText(def.argumentDescription); + } + else + { + this->ui_->argumentsDescription->setVisible(false); + } + + QString text = "Arguments wrapped in <> are required."; + if (def.maxCountArguments != def.minCountArguments) + { + text += QString("\nArguments wrapped in [] are optional."); + } + + text += "\nArguments are separated by a newline."; + + this->ui_->argumentsEdit->setEnabled(true); + this->ui_->argumentsEdit->setPlaceholderText(text); + + this->ui_->argumentsLabel->setVisible(true); + this->ui_->argumentsDescription->setVisible(true); + this->ui_->argumentsEdit->setVisible(true); + + // update easy picker + if (def.possibleArguments.empty()) + { + qCDebug(chatterinoHotkeys) + << "Disabled easy picker because we have possible arguments"; + this->ui_->easyArgsPicker->setVisible(false); + this->ui_->easyArgsLabel->setVisible(false); + return; + } + qCDebug(chatterinoHotkeys) + << "Enabled easy picker because we have possible arguments"; + this->ui_->easyArgsPicker->setVisible(true); + this->ui_->easyArgsLabel->setVisible(true); + + this->ui_->argumentsLabel->setVisible(false); + this->ui_->argumentsEdit->setVisible(false); + this->ui_->argumentsDescription->setVisible(false); + + this->ui_->easyArgsPicker->clear(); + for (const auto &[displayText, _] : def.possibleArguments) + { + this->ui_->easyArgsPicker->addItem(displayText); + } + this->ui_->easyArgsPicker->setCurrentIndex(0); + this->ui_->easyArgsLabel->setText(def.argumentsPrompt); + this->ui_->easyArgsLabel->setToolTip(def.argumentsPromptHover); } } diff --git a/src/widgets/dialogs/EditHotkeyDialog.hpp b/src/widgets/dialogs/EditHotkeyDialog.hpp index d2c1f5e85..3f8bed158 100644 --- a/src/widgets/dialogs/EditHotkeyDialog.hpp +++ b/src/widgets/dialogs/EditHotkeyDialog.hpp @@ -49,6 +49,7 @@ protected slots: private: void showEditError(QString errorText); + void setFromHotkey(std::shared_ptr hotkey); Ui::EditHotkeyDialog *ui_; std::shared_ptr data_; diff --git a/src/widgets/dialogs/EditHotkeyDialog.ui b/src/widgets/dialogs/EditHotkeyDialog.ui index d7f265b0d..7ddb8a21d 100644 --- a/src/widgets/dialogs/EditHotkeyDialog.ui +++ b/src/widgets/dialogs/EditHotkeyDialog.ui @@ -7,7 +7,7 @@ 0 0 400 - 300 + 400 @@ -42,6 +42,9 @@ see this message :) + + Set a name for the hotkey so you will be able to identify it later + Name: @@ -76,6 +79,9 @@ see this message :) + + + @@ -95,6 +101,9 @@ see this message :) + + Pressing this keybinding will invoke the hotkey + Keybinding: @@ -107,6 +116,16 @@ see this message :) + + + You are not supposed to see this, please report this! + + + Argument: + + + + Arguments: @@ -116,7 +135,7 @@ see this message :) - + You should never see this message :) @@ -126,7 +145,7 @@ see this message :) - + @@ -136,8 +155,18 @@ see this message :) - - + + + + + 0 + 0 + + + + + + @@ -169,8 +198,8 @@ see this message :) afterEdit() - 257 - 290 + 263 + 352 157 @@ -185,8 +214,8 @@ see this message :) reject() - 325 - 290 + 331 + 352 286 @@ -201,8 +230,8 @@ see this message :) updatePossibleActions() - 246 - 85 + 172 + 118 75 @@ -217,8 +246,8 @@ see this message :) updateArgumentsInput() - 148 - 119 + 172 + 156 74