Add custom hotkeys. (#2340)

Co-authored-by: LosFarmosCTL <80157503+LosFarmosCTL@users.noreply.github.com>
Co-authored-by: Paweł <zneix@zneix.eu>
Co-authored-by: Felanbird <41973452+Felanbird@users.noreply.github.com>
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
Mm2PL
2021-11-21 17:46:21 +00:00
committed by GitHub
parent b94e21a600
commit 703f3717e2
54 changed files with 3613 additions and 617 deletions
+203
View File
@@ -0,0 +1,203 @@
#pragma once
#include "HotkeyCategory.hpp"
#include <QString>
#include <map>
namespace chatterino {
// ActionDefinition is an action that can be performed with a hotkey
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;
QString argumentDescription = "";
// minCountArguments is the minimum amount of arguments the action accepts
// Example action: "Select Tab" in a popup window accepts 1 argument for which tab to select
uint8_t minCountArguments = 0;
// maxCountArguments is the maximum amount of arguments the action accepts
uint8_t maxCountArguments = minCountArguments;
};
using ActionDefinitionMap = std::map<QString, ActionDefinition>;
inline const std::map<HotkeyCategory, ActionDefinitionMap> actionNames{
{HotkeyCategory::PopupWindow,
{
{"reject", ActionDefinition{"Confirmable popups: Cancel"}},
{"accept", ActionDefinition{"Confirmable popups: Confirm"}},
{"delete", ActionDefinition{"Close"}},
{"openTab",
ActionDefinition{
"Select Tab",
"<next, previous, or index of tab to select>",
1,
}},
{"scrollPage",
ActionDefinition{
"Scroll",
"<up or down>",
1,
}},
{"search", ActionDefinition{"Focus search box"}},
}},
{HotkeyCategory::Split,
{
{"changeChannel", ActionDefinition{"Change channel"}},
{"clearMessages", ActionDefinition{"Clear messages"}},
{"createClip", ActionDefinition{"Create a clip"}},
{"delete", ActionDefinition{"Close"}},
{"focus",
ActionDefinition{
"Focus neighbouring split",
"<up, down, left, or right>",
1,
}},
{"openInBrowser", ActionDefinition{"Open channel in browser"}},
{"openInCustomPlayer",
ActionDefinition{"Open stream in custom player"}},
{"openInStreamlink", ActionDefinition{"Open stream in streamlink"}},
{"openModView", ActionDefinition{"Open mod view in browser"}},
{"openViewerList", ActionDefinition{"Open viewer list"}},
{"pickFilters", ActionDefinition{"Pick filters"}},
{"reconnect", ActionDefinition{"Reconnect to chat"}},
{"reloadEmotes",
ActionDefinition{
"Reload emotes",
"[channel or subscriber]",
0,
1,
}},
{"runCommand",
ActionDefinition{
"Run a command",
"<name of command>",
1,
}},
{"scrollPage",
ActionDefinition{
"Scroll",
"<up or down>",
1,
}},
{"scrollToBottom", ActionDefinition{"Scroll to the bottom"}},
{"setChannelNotification",
ActionDefinition{
"Set channel live notification",
"[on or off. default: toggle]",
0,
1,
}},
{"setModerationMode",
ActionDefinition{
"Set moderation mode",
"[on or off. default: toggle]",
0,
1,
}},
{"showSearch", ActionDefinition{"Search"}},
{"startWatching", ActionDefinition{"Start watching"}},
{"debug", ActionDefinition{"Show debug popup"}},
}},
{HotkeyCategory::SplitInput,
{
{"clear", ActionDefinition{"Clear message"}},
{"copy",
ActionDefinition{
"Copy",
"<source of text: split, splitInput or auto>",
1,
}},
{"cursorToStart",
ActionDefinition{
"To start of message",
"<withSelection or withoutSelection>",
1,
}},
{"cursorToEnd",
ActionDefinition{
"To end of message",
"<withSelection or withoutSelection>",
1,
}},
{"nextMessage", ActionDefinition{"Choose next sent message"}},
{"openEmotesPopup", ActionDefinition{"Open emotes list"}},
{"paste", ActionDefinition{"Paste"}},
{"previousMessage",
ActionDefinition{"Choose previously sent message"}},
{"redo", ActionDefinition{"Redo"}},
{"selectAll", ActionDefinition{"Select all"}},
{"sendMessage",
ActionDefinition{
"Send message",
"[keepInput to not clear the text after sending]",
0,
1,
}},
{"undo", ActionDefinition{"Undo"}},
}},
{HotkeyCategory::Window,
{
#ifdef C_DEBUG
{"addCheerMessage", ActionDefinition{"Debug: Add cheer test message"}},
{"addEmoteMessage", ActionDefinition{"Debug: Add emote test message"}},
{"addLinkMessage",
ActionDefinition{"Debug: Add test message with a link"}},
{"addMiscMessage", ActionDefinition{"Debug: Add misc test message"}},
{"addRewardMessage",
ActionDefinition{"Debug: Add reward test message"}},
#endif
{"moveTab",
ActionDefinition{
"Move tab",
"<next, previous, or new index of tab>",
1,
}},
{"newSplit", ActionDefinition{"Create a new split"}},
{"newTab", ActionDefinition{"Create a new tab"}},
{"openSettings", ActionDefinition{"Open settings"}},
{"openTab",
ActionDefinition{
"Select tab",
"<last, next, previous, or index of tab to select>",
1,
}},
{"openQuickSwitcher", ActionDefinition{"Open the quick switcher"}},
{"popup",
ActionDefinition{
"New popup",
"<split or window>",
1,
}},
{"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,
}},
{"toggleLocalR9K", ActionDefinition{"Toggle local R9K"}},
{"zoom",
ActionDefinition{
"Zoom in/out",
"<in, out, or reset>",
1,
}},
{"setTabVisibility",
ActionDefinition{
"Set tab visibility",
"[on, off, or toggle. default: toggle]",
0,
1,
}}}},
};
} // namespace chatterino
+93
View File
@@ -0,0 +1,93 @@
#include "controllers/hotkeys/Hotkey.hpp"
#include "Application.hpp"
#include "common/QLogging.hpp"
#include "controllers/hotkeys/ActionNames.hpp"
#include "controllers/hotkeys/HotkeyController.hpp"
namespace chatterino {
Hotkey::Hotkey(HotkeyCategory category, QKeySequence keySequence,
QString action, std::vector<QString> arguments, QString name)
: category_(category)
, keySequence_(keySequence)
, action_(action)
, arguments_(arguments)
, name_(name)
{
}
const QKeySequence &Hotkey::keySequence() const
{
return this->keySequence_;
}
QString Hotkey::name() const
{
return this->name_;
}
HotkeyCategory Hotkey::category() const
{
return this->category_;
}
QString Hotkey::action() const
{
return this->action_;
}
bool Hotkey::validAction() const
{
auto categoryActionsIt = actionNames.find(this->category_);
if (categoryActionsIt == actionNames.end())
{
// invalid category
return false;
}
auto actionDefinitionIt = categoryActionsIt->second.find(this->action());
return actionDefinitionIt != categoryActionsIt->second.end();
}
std::vector<QString> Hotkey::arguments() const
{
return this->arguments_;
}
QString Hotkey::getCategory() const
{
return getApp()->hotkeys->categoryDisplayName(this->category_);
}
Qt::ShortcutContext Hotkey::getContext() const
{
switch (this->category_)
{
case HotkeyCategory::Window:
return Qt::WindowShortcut;
case HotkeyCategory::Split:
return Qt::WidgetWithChildrenShortcut;
case HotkeyCategory::SplitInput:
return Qt::WidgetWithChildrenShortcut;
case HotkeyCategory::PopupWindow:
return Qt::WindowShortcut;
}
qCDebug(chatterinoHotkeys)
<< "Using default shortcut context for" << this->getCategory()
<< "and hopeing for the best.";
return Qt::WidgetShortcut;
}
QString Hotkey::toString() const
{
return this->keySequence().toString(QKeySequence::NativeText);
}
QString Hotkey::toPortableString() const
{
return this->keySequence().toString(QKeySequence::PortableText);
}
} // namespace chatterino
+95
View File
@@ -0,0 +1,95 @@
#pragma once
#include "controllers/hotkeys/HotkeyCategory.hpp"
#include <QKeySequence>
#include <QString>
namespace chatterino {
class Hotkey
{
public:
Hotkey(HotkeyCategory category, QKeySequence keySequence, QString action,
std::vector<QString> arguments, QString name);
virtual ~Hotkey() = default;
/**
* @brief Returns the OS-specific string representation of the hotkey
*
* Suitable for showing in the GUI
* e.g. Ctrl+F5 or Command+F5
*/
QString toString() const;
/**
* @brief Returns the portable string representation of the hotkey
*
* Suitable for saving to/loading from file
* e.g. Ctrl+F5 or Shift+Ctrl+R
*/
QString toPortableString() const;
/**
* @brief Returns the category where this hotkey is active. This is labeled the "Category" in the UI.
*
* See enum HotkeyCategory for more information about the various hotkey categories
*/
HotkeyCategory category() const;
/**
* @brief Returns the action which describes what this Hotkey is meant to do
*
* For example, in the Window category there's a "showSearch" action which opens a search popup
*/
QString action() const;
bool validAction() const;
/**
* @brief Returns a list of arguments this hotkey has bound to it
*
* Some actions require a set of arguments that the user can provide, for example the "openTab" action takes an argument for which tab to switch to. can be a number or a word like next or previous
*/
std::vector<QString> arguments() const;
/**
* @brief Returns the display name of the hotkey
*
* For example, in the Split category there's a "showSearch" action that has a default hotkey with the name "default show search"
*/
QString name() const;
/**
* @brief Returns the user-friendly text representation of the hotkeys category
*
* Suitable for showing in the GUI.
* e.g. Split input box for HotkeyCategory::SplitInput
*/
QString getCategory() const;
/**
* @brief Returns the programmating key sequence of the hotkey
*
* The actual key codes required for the hotkey to trigger specifically on e.g CTRL+F5
*/
const QKeySequence &keySequence() const;
private:
HotkeyCategory category_;
QKeySequence keySequence_;
QString action_;
std::vector<QString> arguments_;
QString name_;
/**
* @brief Returns the programmatic context of the hotkey to help Qt decide how to apply the hotkey
*
* The returned value is based off the hotkeys given category
*/
Qt::ShortcutContext getContext() const;
friend class HotkeyController;
};
} // namespace chatterino
@@ -0,0 +1,22 @@
#pragma once
#include <QString>
namespace chatterino {
// HotkeyCategory describes where the hotkeys action takes place.
// Each HotkeyCategory represents a widget that has customizable hotkeys. This
// is needed because more than one widget can have the same or similar action.
enum class HotkeyCategory {
PopupWindow,
Split,
SplitInput,
Window,
};
struct HotkeyCategoryData {
QString name;
QString displayName;
};
} // namespace chatterino
@@ -0,0 +1,531 @@
#include "controllers/hotkeys/HotkeyController.hpp"
#include "common/QLogging.hpp"
#include "controllers/hotkeys/HotkeyModel.hpp"
#include "singletons/Settings.hpp"
#include <QShortcut>
namespace chatterino {
static bool hotkeySortCompare_(const std::shared_ptr<Hotkey> &a,
const std::shared_ptr<Hotkey> &b)
{
if (a->category() == b->category())
{
return a->name() < b->name();
}
return a->category() < b->category();
}
HotkeyController::HotkeyController()
: hotkeys_(hotkeySortCompare_)
{
this->loadHotkeys();
this->signalHolder_.managedConnect(
this->hotkeys_.delayedItemsChanged, [this]() {
qCDebug(chatterinoHotkeys) << "Reloading hotkeys!";
this->onItemsUpdated.invoke();
});
}
HotkeyModel *HotkeyController::createModel(QObject *parent)
{
HotkeyModel *model = new HotkeyModel(parent);
model->initialize(&this->hotkeys_);
return model;
}
std::vector<QShortcut *> HotkeyController::shortcutsForCategory(
HotkeyCategory category,
std::map<QString, std::function<QString(std::vector<QString>)>> actionMap,
QWidget *parent)
{
std::vector<QShortcut *> output;
for (const auto &hotkey : this->hotkeys_)
{
if (hotkey->category() != category)
{
continue;
}
auto target = actionMap.find(hotkey->action());
if (target == actionMap.end())
{
qCDebug(chatterinoHotkeys)
<< qPrintable(parent->objectName())
<< "Unimplemeneted hotkey action:" << hotkey->action() << "in "
<< hotkey->getCategory();
continue;
}
if (!target->second)
{
// Widget has chosen to explicitly not handle this action
continue;
}
auto createShortcutFromKeySeq = [&](QKeySequence qs) {
auto s = new QShortcut(qs, parent);
s->setContext(hotkey->getContext());
auto functionPointer = target->second;
QObject::connect(s, &QShortcut::activated, parent,
[functionPointer, hotkey, this]() {
QString output =
functionPointer(hotkey->arguments());
if (!output.isEmpty())
{
this->showHotkeyError(hotkey, output);
}
});
output.push_back(s);
};
auto qs = QKeySequence(hotkey->keySequence());
auto stringified = qs.toString(QKeySequence::NativeText);
if (stringified.contains("Return"))
{
stringified.replace("Return", "Enter");
auto copy = QKeySequence(stringified, QKeySequence::NativeText);
createShortcutFromKeySeq(copy);
}
createShortcutFromKeySeq(qs);
}
return output;
}
void HotkeyController::save()
{
this->saveHotkeys();
}
std::shared_ptr<Hotkey> HotkeyController::getHotkeyByName(QString name)
{
for (auto &hotkey : this->hotkeys_)
{
if (hotkey->name() == name)
{
return hotkey;
}
}
return nullptr;
}
int HotkeyController::replaceHotkey(QString oldName,
std::shared_ptr<Hotkey> newHotkey)
{
int i = 0;
for (auto &hotkey : this->hotkeys_)
{
if (hotkey->name() == oldName)
{
this->hotkeys_.removeAt(i);
break;
}
i++;
}
return this->hotkeys_.append(newHotkey);
}
boost::optional<HotkeyCategory> HotkeyController::hotkeyCategoryFromName(
QString categoryName)
{
for (const auto &[category, data] : this->categories())
{
if (data.name == categoryName)
{
return category;
}
}
qCDebug(chatterinoHotkeys) << "Unknown category: " << categoryName;
return {};
}
bool HotkeyController::isDuplicate(std::shared_ptr<Hotkey> hotkey,
QString ignoreNamed)
{
for (const auto &shared : this->hotkeys_)
{
if (shared->name() == ignoreNamed || shared->name() == hotkey->name())
{
// Given hotkey is the same as shared, just before it was being edited.
continue;
}
if (shared->category() == hotkey->category() &&
shared->keySequence() == hotkey->keySequence())
{
return true;
}
}
return false;
}
QString HotkeyController::categoryDisplayName(HotkeyCategory category) const
{
if (this->hotkeyCategories_.count(category) == 0)
{
qCWarning(chatterinoHotkeys) << "Invalid HotkeyCategory passed to "
"categoryDisplayName function";
return QString();
}
const auto &categoryData = this->hotkeyCategories_.at(category);
return categoryData.displayName;
}
QString HotkeyController::categoryName(HotkeyCategory category) const
{
if (this->hotkeyCategories_.count(category) == 0)
{
qCWarning(chatterinoHotkeys) << "Invalid HotkeyCategory passed to "
"categoryName function";
return QString();
}
const auto &categoryData = this->hotkeyCategories_.at(category);
return categoryData.name;
}
const std::map<HotkeyCategory, HotkeyCategoryData>
&HotkeyController::categories() const
{
return this->hotkeyCategories_;
}
void HotkeyController::loadHotkeys()
{
auto defaultHotkeysAdded =
pajlada::Settings::Setting<std::vector<QString>>::get(
"/hotkeys/addedDefaults");
auto set = std::set<QString>(defaultHotkeysAdded.begin(),
defaultHotkeysAdded.end());
auto keys = pajlada::Settings::SettingManager::getObjectKeys("/hotkeys");
this->addDefaults(set);
pajlada::Settings::Setting<std::vector<QString>>::set(
"/hotkeys/addedDefaults", std::vector<QString>(set.begin(), set.end()));
qCDebug(chatterinoHotkeys) << "Loading hotkeys...";
for (const auto &key : keys)
{
if (key == "addedDefaults")
{
continue;
}
auto section = "/hotkeys/" + key;
auto categoryName =
pajlada::Settings::Setting<QString>::get(section + "/category");
auto keySequence =
pajlada::Settings::Setting<QString>::get(section + "/keySequence");
auto action =
pajlada::Settings::Setting<QString>::get(section + "/action");
auto arguments = pajlada::Settings::Setting<std::vector<QString>>::get(
section + "/arguments");
qCDebug(chatterinoHotkeys)
<< "Hotkey " << categoryName << keySequence << action << arguments;
if (categoryName.isEmpty() || keySequence.isEmpty() || action.isEmpty())
{
continue;
}
auto category = this->hotkeyCategoryFromName(categoryName);
if (!category)
{
continue;
}
this->hotkeys_.append(std::make_shared<Hotkey>(
*category, QKeySequence(keySequence), action, arguments,
QString::fromStdString(key)));
}
}
void HotkeyController::saveHotkeys()
{
auto defaultHotkeysAdded =
pajlada::Settings::Setting<std::vector<QString>>::get(
"/hotkeys/addedDefaults");
// make sure that hotkeys are deleted
pajlada::Settings::SettingManager::getInstance()->set(
"/hotkeys", rapidjson::Value(rapidjson::kObjectType));
// re-add /hotkeys/addedDefaults as previous set call deleted that key
pajlada::Settings::Setting<std::vector<QString>>::set(
"/hotkeys/addedDefaults",
std::vector<QString>(defaultHotkeysAdded.begin(),
defaultHotkeysAdded.end()));
for (const auto &hotkey : this->hotkeys_)
{
auto section = "/hotkeys/" + hotkey->name().toStdString();
pajlada::Settings::Setting<QString>::set(section + "/action",
hotkey->action());
pajlada::Settings::Setting<QString>::set(
section + "/keySequence", hotkey->keySequence().toString());
auto categoryName = this->categoryName(hotkey->category());
pajlada::Settings::Setting<QString>::set(section + "/category",
categoryName);
pajlada::Settings::Setting<std::vector<QString>>::set(
section + "/arguments", hotkey->arguments());
}
}
void HotkeyController::addDefaults(std::set<QString> &addedHotkeys)
{
// popup window
{
this->tryAddDefault(addedHotkeys, HotkeyCategory::PopupWindow,
QKeySequence("Escape"), "delete",
std::vector<QString>(), "close popup window");
for (int i = 0; i < 8; i++)
{
this->tryAddDefault(addedHotkeys, HotkeyCategory::PopupWindow,
QKeySequence(QString("Ctrl+%1").arg(i + 1)),
"openTab", {QString::number(i)},
QString("popup select tab #%1").arg(i + 1));
}
this->tryAddDefault(addedHotkeys, HotkeyCategory::PopupWindow,
QKeySequence("Ctrl+9"), "openTab", {"last"},
"popup select last tab");
this->tryAddDefault(addedHotkeys, HotkeyCategory::PopupWindow,
QKeySequence("Ctrl+Tab"), "openTab", {"next"},
"popup select next tab");
this->tryAddDefault(addedHotkeys, HotkeyCategory::PopupWindow,
QKeySequence("Ctrl+Shift+Tab"), "openTab",
{"previous"}, "popup select previous tab");
this->tryAddDefault(addedHotkeys, HotkeyCategory::PopupWindow,
QKeySequence("PgUp"), "scrollPage", {"up"},
"popup scroll up");
this->tryAddDefault(addedHotkeys, HotkeyCategory::PopupWindow,
QKeySequence("PgDown"), "scrollPage", {"down"},
"popup scroll down");
this->tryAddDefault(addedHotkeys, HotkeyCategory::PopupWindow,
QKeySequence("Return"), "accept",
std::vector<QString>(), "popup accept");
this->tryAddDefault(addedHotkeys, HotkeyCategory::PopupWindow,
QKeySequence("Escape"), "reject",
std::vector<QString>(), "popup reject");
this->tryAddDefault(addedHotkeys, HotkeyCategory::PopupWindow,
QKeySequence("Ctrl+F"), "search",
std::vector<QString>(), "popup focus search box");
}
// split
{
this->tryAddDefault(addedHotkeys, HotkeyCategory::Split,
QKeySequence("Ctrl+W"), "delete",
std::vector<QString>(), "delete");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Split,
QKeySequence("Ctrl+R"), "changeChannel",
std::vector<QString>(), "change channel");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Split,
QKeySequence("Ctrl+F"), "showSearch",
std::vector<QString>(), "show search");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Split,
QKeySequence("Ctrl+F5"), "reconnect",
std::vector<QString>(), "reconnect");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Split,
QKeySequence("F5"), "reloadEmotes",
std::vector<QString>(), "reload emotes");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Split,
QKeySequence("Alt+x"), "createClip",
std::vector<QString>(), "create clip");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Split,
QKeySequence("Alt+left"), "focus", {"left"},
"focus left");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Split,
QKeySequence("Alt+down"), "focus", {"down"},
"focus down");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Split,
QKeySequence("Alt+up"), "focus", {"up"},
"focus up");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Split,
QKeySequence("Alt+right"), "focus", {"right"},
"focus right");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Split,
QKeySequence("PgUp"), "scrollPage", {"up"},
"scroll page up");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Split,
QKeySequence("PgDown"), "scrollPage", {"down"},
"scroll page down");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Split,
QKeySequence("Ctrl+End"), "scrollToBottom",
std::vector<QString>(), "scroll to bottom");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Split,
QKeySequence("F10"), "debug",
std::vector<QString>(), "open debug popup");
}
// split input
{
this->tryAddDefault(addedHotkeys, HotkeyCategory::SplitInput,
QKeySequence("Ctrl+E"), "openEmotesPopup",
std::vector<QString>(), "emote picker");
// all variations of send message :)
{
this->tryAddDefault(addedHotkeys, HotkeyCategory::SplitInput,
QKeySequence("Return"), "sendMessage",
std::vector<QString>(), "send message");
this->tryAddDefault(addedHotkeys, HotkeyCategory::SplitInput,
QKeySequence("Ctrl+Return"), "sendMessage",
{"keepInput"}, "send message and keep text");
this->tryAddDefault(addedHotkeys, HotkeyCategory::SplitInput,
QKeySequence("Shift+Return"), "sendMessage",
std::vector<QString>(), "send message");
this->tryAddDefault(addedHotkeys, HotkeyCategory::SplitInput,
QKeySequence("Ctrl+Shift+Return"),
"sendMessage", {"keepInput"},
"send message and keep text");
}
this->tryAddDefault(addedHotkeys, HotkeyCategory::SplitInput,
QKeySequence("Home"), "cursorToStart",
{"withoutSelection"}, "go to start of input");
this->tryAddDefault(addedHotkeys, HotkeyCategory::SplitInput,
QKeySequence("End"), "cursorToEnd",
{"withoutSelection"}, "go to end of input");
this->tryAddDefault(addedHotkeys, HotkeyCategory::SplitInput,
QKeySequence("Shift+Home"), "cursorToStart",
{"withSelection"},
"go to start of input with selection");
this->tryAddDefault(addedHotkeys, HotkeyCategory::SplitInput,
QKeySequence("Shift+End"), "cursorToEnd",
{"withSelection"},
"go to end of input with selection");
this->tryAddDefault(addedHotkeys, HotkeyCategory::SplitInput,
QKeySequence("Up"), "previousMessage",
std::vector<QString>(), "previous message");
this->tryAddDefault(addedHotkeys, HotkeyCategory::SplitInput,
QKeySequence("Down"), "nextMessage",
std::vector<QString>(), "next message");
}
// window
{
this->tryAddDefault(addedHotkeys, HotkeyCategory::Window,
QKeySequence("Ctrl+P"), "openSettings",
std::vector<QString>(), "open settings");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Window,
QKeySequence("Ctrl+T"), "newSplit",
std::vector<QString>(), "new split");
for (int i = 0; i < 8; i++)
{
this->tryAddDefault(addedHotkeys, HotkeyCategory::Window,
QKeySequence(QString("Ctrl+%1").arg(i + 1)),
"openTab", {QString::number(i)},
QString("select tab #%1").arg(i + 1));
}
this->tryAddDefault(addedHotkeys, HotkeyCategory::Window,
QKeySequence("Ctrl+9"), "openTab", {"last"},
"select last tab");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Window,
QKeySequence("Ctrl+Tab"), "openTab", {"next"},
"select next tab");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Window,
QKeySequence("Ctrl+Shift+Tab"), "openTab",
{"previous"}, "select previous tab");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Window,
QKeySequence("Ctrl+N"), "popup", {"split"},
"new popup window");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Window,
QKeySequence("Ctrl+Shift+N"), "popup", {"window"},
"new popup window from tab");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Window,
QKeySequence::ZoomIn, "zoom", {"in"}, "zoom in");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Window,
QKeySequence::ZoomOut, "zoom", {"out"}, "zoom out");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Window,
QKeySequence("CTRL+0"), "zoom", {"reset"},
"zoom reset");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Window,
QKeySequence("Ctrl+Shift+T"), "newTab",
std::vector<QString>(), "new tab");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Window,
QKeySequence("Ctrl+Shift+W"), "removeTab",
std::vector<QString>(), "remove tab");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Window,
QKeySequence("Ctrl+G"), "reopenSplit",
std::vector<QString>(), "reopen split");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Window,
QKeySequence("Ctrl+H"), "toggleLocalR9K",
std::vector<QString>(), "toggle local r9k");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Window,
QKeySequence("Ctrl+K"), "openQuickSwitcher",
std::vector<QString>(), "open quick switcher");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Window,
QKeySequence("Ctrl+U"), "setTabVisibility",
{"toggle"}, "toggle tab visibility");
}
}
void HotkeyController::resetToDefaults()
{
std::set<QString> addedSet;
pajlada::Settings::Setting<std::vector<QString>>::set(
"/hotkeys/addedDefaults",
std::vector<QString>(addedSet.begin(), addedSet.end()));
auto size = this->hotkeys_.raw().size();
for (unsigned long i = 0; i < size; i++)
{
this->hotkeys_.removeAt(0);
}
// add defaults back
this->saveHotkeys();
this->loadHotkeys();
}
void HotkeyController::tryAddDefault(std::set<QString> &addedHotkeys,
HotkeyCategory category,
QKeySequence keySequence, QString action,
std::vector<QString> args, QString name)
{
qCDebug(chatterinoHotkeys) << "Try add default" << name;
if (addedHotkeys.count(name) != 0)
{
qCDebug(chatterinoHotkeys) << "Already exists";
return; // hotkey was added before
}
qCDebug(chatterinoHotkeys) << "Inserted";
this->hotkeys_.append(
std::make_shared<Hotkey>(category, keySequence, action, args, name));
addedHotkeys.insert(name);
}
void HotkeyController::showHotkeyError(const std::shared_ptr<Hotkey> &hotkey,
QString warning)
{
auto msgBox = new QMessageBox(
QMessageBox::Icon::Warning, "Hotkey error",
QString(
"There was an error while executing your hotkey named \"%1\": \n%2")
.arg(hotkey->name(), warning),
QMessageBox::Ok);
msgBox->exec();
}
} // namespace chatterino
@@ -0,0 +1,131 @@
#pragma once
#include "common/SignalVector.hpp"
#include "common/Singleton.hpp"
#include "controllers/hotkeys/HotkeyCategory.hpp"
#include <boost/optional.hpp>
#include <pajlada/signals/signal.hpp>
#include <pajlada/signals/signalholder.hpp>
#include <set>
class QShortcut;
namespace chatterino {
class Hotkey;
class HotkeyModel;
class HotkeyController final : public Singleton
{
public:
using HotkeyFunction = std::function<QString(std::vector<QString>)>;
using HotkeyMap = std::map<QString, HotkeyFunction>;
HotkeyController();
HotkeyModel *createModel(QObject *parent);
std::vector<QShortcut *> shortcutsForCategory(HotkeyCategory category,
HotkeyMap actionMap,
QWidget *parent);
void save() override;
std::shared_ptr<Hotkey> getHotkeyByName(QString name);
/**
* @brief removes the hotkey with the oldName and inserts newHotkey at the end
*
* @returns the new index in the SignalVector
**/
int replaceHotkey(QString oldName, std::shared_ptr<Hotkey> newHotkey);
boost::optional<HotkeyCategory> hotkeyCategoryFromName(
QString categoryName);
/**
* @brief checks if the hotkey is duplicate
*
* @param hotkey the hotkey to check
* @param ignoreNamed name of hotkey to ignore. Useful for ensuring we don't fail if the hotkey's name is being edited
*
* @returns true if the given hotkey is a duplicate, false if it's not
**/
[[nodiscard]] bool isDuplicate(std::shared_ptr<Hotkey> hotkey,
QString ignoreNamed);
/**
* @brief Returns the display name of the given hotkey category
*
* @returns the display name, or an empty string if an invalid hotkey category was given
**/
[[nodiscard]] QString categoryDisplayName(HotkeyCategory category) const;
/**
* @brief Returns the name of the given hotkey category
*
* @returns the name, or an empty string if an invalid hotkey category was given
**/
[[nodiscard]] QString categoryName(HotkeyCategory category) const;
/**
* @returns a const map with the HotkeyCategory enum as its key, and HotkeyCategoryData as the value.
**/
[[nodiscard]] const std::map<HotkeyCategory, HotkeyCategoryData>
&categories() const;
pajlada::Signals::NoArgSignal onItemsUpdated;
private:
/**
* @brief load hotkeys from under the /hotkeys settings path
**/
void loadHotkeys();
/**
* @brief save hotkeys to the /hotkeys path
*
* This is done by first fully clearing the /hotkeys object, then reapplying all hotkeys
* from the hotkeys_ object
**/
void saveHotkeys();
/**
* @brief try to load all default hotkeys
*
* New hotkeys must be added to this function
**/
void addDefaults(std::set<QString> &addedHotkeys);
/**
* @brief remove all user-made changes to hotkeys and reset to the default hotkeys
**/
void resetToDefaults();
/**
* @brief try to add a hotkey if it hasn't already been added or modified by the user
**/
void tryAddDefault(std::set<QString> &addedHotkeys, HotkeyCategory category,
QKeySequence keySequence, QString action,
std::vector<QString> args, QString name);
/**
* @brief show an error dialog about a hotkey in a standard format
**/
static void showHotkeyError(const std::shared_ptr<Hotkey> &hotkey,
QString warning);
friend class KeyboardSettingsPage;
SignalVector<std::shared_ptr<Hotkey>> hotkeys_;
pajlada::Signals::SignalHolder signalHolder_;
const std::map<HotkeyCategory, HotkeyCategoryData> hotkeyCategories_ = {
{HotkeyCategory::PopupWindow, {"popupWindow", "Popup Windows"}},
{HotkeyCategory::Split, {"split", "Split"}},
{HotkeyCategory::SplitInput, {"splitInput", "Split input box"}},
{HotkeyCategory::Window, {"window", "Window"}},
};
};
} // namespace chatterino
+30
View File
@@ -0,0 +1,30 @@
#include "controllers/hotkeys/HotkeyHelpers.hpp"
#include <QStringList>
namespace chatterino {
std::vector<QString> parseHotkeyArguments(QString argumentString)
{
std::vector<QString> arguments;
argumentString = argumentString.trimmed();
if (argumentString.isEmpty())
{
// argumentString is empty, early out to ensure we don't end up with a vector with one empty element
return arguments;
}
auto argList = argumentString.split("\n");
// convert the QStringList to our preferred std::vector
for (const auto &arg : argList)
{
arguments.push_back(arg.trimmed());
}
return arguments;
}
} // namespace chatterino
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <QString>
#include <vector>
namespace chatterino {
std::vector<QString> parseHotkeyArguments(QString argumentString);
} // namespace chatterino
+121
View File
@@ -0,0 +1,121 @@
#include "controllers/hotkeys/HotkeyModel.hpp"
#include "common/QLogging.hpp"
#include "util/StandardItemHelper.hpp"
namespace chatterino {
HotkeyModel::HotkeyModel(QObject *parent)
: SignalVectorModel<std::shared_ptr<Hotkey>>(2, parent)
{
}
// turn a vector item into a model row
std::shared_ptr<Hotkey> HotkeyModel::getItemFromRow(
std::vector<QStandardItem *> &row, const std::shared_ptr<Hotkey> &original)
{
return original;
}
// turns a row in the model into a vector item
void HotkeyModel::getRowFromItem(const std::shared_ptr<Hotkey> &item,
std::vector<QStandardItem *> &row)
{
QFont font("Segoe UI", 10);
if (!item->validAction())
{
font.setStrikeOut(true);
}
setStringItem(row[0], item->name(), false);
row[0]->setData(font, Qt::FontRole);
setStringItem(row[1], item->toString(), false);
row[1]->setData(font, Qt::FontRole);
}
int HotkeyModel::beforeInsert(const std::shared_ptr<Hotkey> &item,
std::vector<QStandardItem *> &row,
int proposedIndex)
{
const auto category = item->getCategory();
if (this->categoryCount_[category]++ == 0)
{
auto newRow = this->createRow();
setStringItem(newRow[0], category, false, false);
newRow[0]->setData(QFont("Segoe UI Light", 16), Qt::FontRole);
// make sure category headers aren't editable
for (unsigned long i = 1; i < newRow.size(); i++)
{
setStringItem(newRow[i], "", false, false);
}
this->insertCustomRow(std::move(newRow), proposedIndex);
return proposedIndex + 1;
}
auto [currentCategoryModelIndex, nextCategoryModelIndex] =
this->getCurrentAndNextCategoryModelIndex(category);
if (nextCategoryModelIndex != -1 && proposedIndex >= nextCategoryModelIndex)
{
// The proposed index would have landed under the wrong category, we offset by -1 to compensate
return proposedIndex - 1;
}
return proposedIndex;
}
void HotkeyModel::afterRemoved(const std::shared_ptr<Hotkey> &item,
std::vector<QStandardItem *> &row, int index)
{
auto it = this->categoryCount_.find(item->getCategory());
assert(it != this->categoryCount_.end());
if (it->second <= 1)
{
this->categoryCount_.erase(it);
this->removeCustomRow(index - 1);
}
else
{
it->second--;
}
}
std::tuple<int, int> HotkeyModel::getCurrentAndNextCategoryModelIndex(
const QString &category) const
{
int modelIndex = 0;
int currentCategoryModelIndex = -1;
int nextCategoryModelIndex = -1;
for (const auto &row : this->rows())
{
if (row.isCustomRow)
{
QString customRowValue =
row.items[0]->data(Qt::EditRole).toString();
if (currentCategoryModelIndex != -1)
{
nextCategoryModelIndex = modelIndex;
break;
}
if (customRowValue == category)
{
currentCategoryModelIndex = modelIndex;
}
}
modelIndex += 1;
}
return {currentCategoryModelIndex, nextCategoryModelIndex};
}
} // namespace chatterino
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include "common/SignalVectorModel.hpp"
#include "controllers/hotkeys/Hotkey.hpp"
#include "util/QStringHash.hpp"
#include <unordered_map>
namespace chatterino {
class HotkeyController;
class HotkeyModel : public SignalVectorModel<std::shared_ptr<Hotkey>>
{
public:
HotkeyModel(QObject *parent);
protected:
// turn a vector item into a model row
virtual std::shared_ptr<Hotkey> getItemFromRow(
std::vector<QStandardItem *> &row,
const std::shared_ptr<Hotkey> &original) override;
// turns a row in the model into a vector item
virtual void getRowFromItem(const std::shared_ptr<Hotkey> &item,
std::vector<QStandardItem *> &row) override;
virtual int beforeInsert(const std::shared_ptr<Hotkey> &item,
std::vector<QStandardItem *> &row,
int proposedIndex) override;
virtual void afterRemoved(const std::shared_ptr<Hotkey> &item,
std::vector<QStandardItem *> &row,
int index) override;
friend class HotkeyController;
private:
std::tuple<int, int> getCurrentAndNextCategoryModelIndex(
const QString &category) const;
std::unordered_map<QString, int> categoryCount_;
};
} // namespace chatterino
+95
View File
@@ -0,0 +1,95 @@
# Custom Hotkeys
## Table of Contents
- [Glossary](#Glossary)
- [Adding new hotkeys](#Adding_new_hotkeys)
- [Adding new hotkey categories](#Adding_new_hotkey_categories)
## Glossary
| Word | Meaning |
| ----------------------- | ----------------------------------------------------------------------------------------- |
| Shortcut | `QShortcut` object created from a hotkey. |
| Hotkey | Template for creating shortcuts in the right categories. See [Hotkey object][hotkey.hpp]. |
| Category | Place where hotkeys' actions are executed. |
| Action | Code that makes a hotkey do something. |
| Keybinding or key combo | The keys you press on the keyboard to do something. |
## Adding new hotkeys
Adding new hotkeys to a widget that already has hotkeys is quite easy.
### Add an action
1. Locate the call to `getApp()->hotkeys->shortcutsForCategory(...)`, it is located in the `addShortcuts()` method
2. Above that should be a `HotkeyController::HotkeyMap` named `actions`
3. Add your new action inside that map, it should return a non-empty QString only when configuration errors are found.
4. Go to `ActionNames.hpp` and add a definition for your hotkey with a nice user-friendly name. Be sure to double-check the argument count.
### Add a default
Defaults are stored in `HotkeyController.cpp` in the `resetToDefaults()` method. To add a default just add a call to `tryAddDefault` in the appropriate section. Make sure that the name you gave the hotkey is unique.
```cpp
void HotkeyController::tryAddDefault(std::set<QString> &addedHotkeys,
HotkeyCategory category,
QKeySequence keySequence, QString action,
std::vector<QString> args, QString name)
```
- where `action` is the action you added before,
- `category` — same category that is in the `shortcutsForCategory` call
- `name`**unique** name of the default hotkey
- `keySequence` - key combo for the hotkey
## Adding new hotkey categories
If you want to add hotkeys to new widget that doesn't already have them it's a bit more work.
### Add the `HotkeyCategory` value
Add a value for the `HotkeyCategory` enum in [`HotkeyCategory.hpp`][hotkeycategory.hpp]. If you widget is a popup, it's best to use the existing `PopupWindow` category.
### Add a nice name for the category
Add a string name and display name for the category in [`HotkeyController.hpp`][hotkeycontroller.hpp] to `hotkeyCategoryNames` and `hotkeyCategoryDisplayNames`.
### Add a shortcut context
To make sure shortcuts created from your hotkeys are only executed in the right places, you need to add a shortcut context for Qt. This is done in `Hotkey.cpp` in `Hotkey::getContext()`.
See the [ShortcutContext enum docs for possible values](https://doc.qt.io/qt-5/qt.html#ShortcutContext-enum)
### Override `addShortcuts`
If the widget you're adding Hotkeys is a `BaseWidget` or a `BaseWindow`. You can override the `addShortcuts()` method. You should also add a call to it in the constructor. Here is some template/example code:
```cpp
void YourWidget::addShortcuts()
{
HotkeyController::HotkeyMap actions{
{"barrelRoll", // replace this with your action code
[this](std::vector<QString> arguments) -> QString {
// DO A BARREL ROLL
return ""; // only return text if there is a configuration error.
}},
};
this->shortcuts_ = getApp()->hotkeys->shortcutsForCategory(HotkeyCategory::PopupWindow /* or your category name */,
actions, this);
}
```
## Renaming defaults
Renaming defaults is currently not possible. If you were to rename one, it would get recreated for everyone probably leading to broken shortcuts, don't do this until a proper mechanism has been made.
<!-- big list of links -->
[actionnames.hpp]: https://github.com/Chatterino/chatterino2/blob/custom_hotkeys/src/controllers/hotkeys/ActionNames.hpp
[hotkey.cpp]: https://github.com/Chatterino/chatterino2/blob/custom_hotkeys/src/controllers/hotkeys/Hotkey.cpp
[hotkey.hpp]: https://github.com/Chatterino/chatterino2/blob/custom_hotkeys/src/controllers/hotkeys/Hotkey.hpp
[hotkeycontroller.cpp]: https://github.com/Chatterino/chatterino2/blob/custom_hotkeys/src/controllers/hotkeys/HotkeyController.cpp
[hotkeycontroller.hpp]: https://github.com/Chatterino/chatterino2/blob/custom_hotkeys/src/controllers/hotkeys/HotkeyController.hpp
[hotkeymodel.cpp]: https://github.com/Chatterino/chatterino2/blob/custom_hotkeys/src/controllers/hotkeys/HotkeyModel.cpp
[hotkeymodel.hpp]: https://github.com/Chatterino/chatterino2/blob/custom_hotkeys/src/controllers/hotkeys/HotkeyModel.hpp
[hotkeycategory.hpp]: https://github.com/Chatterino/chatterino2/blob/custom_hotkeys/src/controllers/hotkeys/HotkeyCategory.hpp