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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
std::vector<QString> parseHotkeyArguments(QString argumentString);
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user