aseprite/src/app/ui/keyboard_shortcuts.cpp

783 lines
23 KiB
C++

// Aseprite
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/ui/keyboard_shortcuts.h"
#include "app/app.h"
#include "app/app_menus.h"
#include "app/commands/command.h"
#include "app/commands/commands.h"
#include "app/commands/params.h"
#include "app/doc.h"
#include "app/i18n/strings.h"
#include "app/tools/active_tool.h"
#include "app/tools/ink.h"
#include "app/tools/tool.h"
#include "app/tools/tool_box.h"
#include "app/ui/editor/editor.h"
#include "app/ui/key.h"
#include "app/ui/timeline/timeline.h"
#include "app/ui_context.h"
#include "app/xml_document.h"
#include "app/xml_exception.h"
#include "fmt/format.h"
#include "ui/message.h"
#include "ui/shortcut.h"
#include "tinyxml2.h"
#include <algorithm>
#include <set>
#include <vector>
#define XML_KEYBOARD_FILE_VERSION "1"
using namespace tinyxml2;
namespace {
const char* get_shortcut(XMLElement* elem)
{
const char* shortcut = NULL;
#ifdef _WIN32
if (!shortcut)
shortcut = elem->Attribute("win");
#elif defined __APPLE__
if (!shortcut)
shortcut = elem->Attribute("mac");
#else
if (!shortcut)
shortcut = elem->Attribute("linux");
#endif
if (!shortcut)
shortcut = elem->Attribute("shortcut");
return shortcut;
}
} // namespace
namespace app {
using namespace ui;
static std::unique_ptr<KeyboardShortcuts> g_singleton;
// static
KeyboardShortcuts* KeyboardShortcuts::instance()
{
if (!g_singleton)
g_singleton = std::make_unique<KeyboardShortcuts>();
return g_singleton.get();
}
// static
void KeyboardShortcuts::destroyInstance()
{
g_singleton.reset();
}
KeyboardShortcuts::KeyboardShortcuts()
{
ASSERT(Strings::instance());
Strings::instance()->LanguageChange.connect([] { reset_key_tables_that_depends_on_language(); });
}
KeyboardShortcuts::~KeyboardShortcuts()
{
clear();
}
void KeyboardShortcuts::setKeys(const KeyboardShortcuts& keys, const bool cloneKeys)
{
if (cloneKeys) {
for (const KeyPtr& key : keys)
m_keys.push_back(std::make_shared<Key>(*key));
}
else {
m_keys = keys.m_keys;
}
UserChange();
}
void KeyboardShortcuts::clear()
{
m_keys.clear();
}
void KeyboardShortcuts::importFile(XMLElement* rootElement, KeySource source)
{
// <keyboard><commands><key>
XMLHandle handle(rootElement);
XMLElement* xmlKey = handle.FirstChildElement("commands").FirstChildElement("key").ToElement();
while (xmlKey) {
const char* command_name = xmlKey->Attribute("command");
const char* command_key = get_shortcut(xmlKey);
bool removed = bool_attr(xmlKey, "removed", false);
if (command_name) {
Command* command = Commands::instance()->byId(command_name);
if (command) {
// Read context
KeyContext keycontext = KeyContext::Any;
const char* keycontextstr = xmlKey->Attribute("context");
if (keycontextstr)
keycontext = base::convert_to<KeyContext>(std::string(keycontextstr));
// Read params
Params params;
XMLElement* xmlParam = xmlKey->FirstChildElement("param");
while (xmlParam) {
const char* param_name = xmlParam->Attribute("name");
const char* param_value = xmlParam->Attribute("value");
if (param_name && param_value)
params.set(param_name, param_value);
xmlParam = xmlParam->NextSiblingElement();
}
// add the keyboard shortcut to the command
KeyPtr key = this->command(command_name, params, keycontext);
if (key && command_key) {
Shortcut shortcut(command_key);
if (!removed) {
key->add(shortcut, source, *this);
// Add the shortcut to the menuitems with this command
// (this is only visual, the
// "CustomizedGuiManager::onProcessMessage" is the only
// one that process keyboard shortcuts)
if (key->shortcuts().size() == 1) {
AppMenus::instance()->applyShortcutToMenuitemsWithCommand(command, params, key);
}
}
else
key->disableShortcut(shortcut, source);
}
}
}
xmlKey = xmlKey->NextSiblingElement();
}
// Load keyboard shortcuts for tools
// <keyboard><tools><key>
xmlKey = handle.FirstChildElement("tools").FirstChildElement("key").ToElement();
while (xmlKey) {
const char* tool_id = xmlKey->Attribute("tool");
const char* tool_key = get_shortcut(xmlKey);
bool removed = bool_attr(xmlKey, "removed", false);
if (tool_id) {
tools::Tool* tool = App::instance()->toolBox()->getToolById(tool_id);
if (tool) {
KeyPtr key = this->tool(tool);
if (key && tool_key) {
LOG(VERBOSE, "KEYS: Shortcut for tool %s: %s\n", tool_id, tool_key);
Shortcut shortcut(tool_key);
if (!removed)
key->add(shortcut, source, *this);
else
key->disableShortcut(shortcut, source);
}
}
}
xmlKey = xmlKey->NextSiblingElement();
}
// Load keyboard shortcuts for quicktools
// <keyboard><quicktools><key>
xmlKey = handle.FirstChildElement("quicktools").FirstChildElement("key").ToElement();
while (xmlKey) {
const char* tool_id = xmlKey->Attribute("tool");
const char* tool_key = get_shortcut(xmlKey);
bool removed = bool_attr(xmlKey, "removed", false);
if (tool_id) {
tools::Tool* tool = App::instance()->toolBox()->getToolById(tool_id);
if (tool) {
KeyPtr key = this->quicktool(tool);
if (key && tool_key) {
LOG(VERBOSE, "KEYS: Shortcut for quicktool %s: %s\n", tool_id, tool_key);
Shortcut shortcut(tool_key);
if (!removed)
key->add(shortcut, source, *this);
else
key->disableShortcut(shortcut, source);
}
}
}
xmlKey = xmlKey->NextSiblingElement();
}
// Load special keyboard shortcuts for sprite editor customization
// <keyboard><actions><key>
xmlKey = handle.FirstChildElement("actions").FirstChildElement("key").ToElement();
while (xmlKey) {
const char* action_id = xmlKey->Attribute("action");
const char* action_key = get_shortcut(xmlKey);
bool removed = bool_attr(xmlKey, "removed", false);
if (action_id) {
KeyAction action = base::convert_to<KeyAction, std::string>(action_id);
if (action != KeyAction::None) {
// Read context
KeyContext keycontext = KeyContext::Any;
const char* keycontextstr = xmlKey->Attribute("context");
if (keycontextstr)
keycontext = base::convert_to<KeyContext>(std::string(keycontextstr));
KeyPtr key = this->action(action, keycontext);
if (key && action_key) {
LOG(VERBOSE,
"KEYS: Shortcut for action %s/%s: %s\n",
action_id,
(keycontextstr ? keycontextstr : "Any"),
action_key);
Shortcut shortcut(action_key);
if (!removed)
key->add(shortcut, source, *this);
else
key->disableShortcut(shortcut, source);
}
}
}
xmlKey = xmlKey->NextSiblingElement();
}
// Load special keyboard shortcuts for mouse wheel customization
// <keyboard><wheel><key>
xmlKey = handle.FirstChildElement("wheel").FirstChildElement("key").ToElement();
while (xmlKey) {
const char* action_id = xmlKey->Attribute("action");
const char* action_key = get_shortcut(xmlKey);
bool removed = bool_attr(xmlKey, "removed", false);
if (action_id) {
WheelAction action = base::convert_to<WheelAction, std::string>(action_id);
if (action != WheelAction::None) {
KeyPtr key = this->wheelAction(action);
if (key && action_key) {
LOG(VERBOSE, "KEYS: Shortcut for wheel action %s: %s\n", action_id, action_key);
Shortcut shortcut(action_key);
if (!removed)
key->add(shortcut, source, *this);
else
key->disableShortcut(shortcut, source);
}
}
}
xmlKey = xmlKey->NextSiblingElement();
}
// Load special keyboard shortcuts to simulate mouse wheel actions
// <keyboard><drag><key>
xmlKey = handle.FirstChildElement("drag").FirstChildElement("key").ToElement();
while (xmlKey) {
const char* action_id = xmlKey->Attribute("action");
const char* action_key = get_shortcut(xmlKey);
bool removed = bool_attr(xmlKey, "removed", false);
if (action_id) {
WheelAction action = base::convert_to<WheelAction, std::string>(action_id);
if (action != WheelAction::None) {
KeyPtr key = this->dragAction(action);
if (key && action_key) {
if (auto vector_str = xmlKey->Attribute("vector")) {
double x, y = 0.0;
// Parse a string like "double,double"
x = std::strtod(vector_str, (char**)&vector_str);
if (vector_str && *vector_str == ',') {
++vector_str;
y = std::strtod(vector_str, nullptr);
}
key->setDragVector(DragVector(x, y));
}
LOG(VERBOSE, "KEYS: Shortcut for drag action %s: %s\n", action_id, action_key);
Shortcut shortcut(action_key);
if (!removed)
key->add(shortcut, source, *this);
else
key->disableShortcut(shortcut, source);
}
}
}
xmlKey = xmlKey->NextSiblingElement();
}
}
void KeyboardShortcuts::importFile(const std::string& filename, KeySource source)
{
XMLDocumentRef doc = app::open_xml(filename);
XMLHandle handle(doc.get());
XMLElement* xmlKey = handle.FirstChildElement("keyboard").ToElement();
importFile(xmlKey, source);
}
void KeyboardShortcuts::exportFile(const std::string& filename)
{
auto doc = std::make_unique<XMLDocument>();
XMLElement* keyboard = doc->NewElement("keyboard");
XMLElement* commands = keyboard->InsertNewChildElement("commands");
XMLElement* tools = keyboard->InsertNewChildElement("tools");
XMLElement* quicktools = keyboard->InsertNewChildElement("quicktools");
XMLElement* actions = keyboard->InsertNewChildElement("actions");
XMLElement* wheel = keyboard->InsertNewChildElement("wheel");
XMLElement* drag = keyboard->InsertNewChildElement("drag");
keyboard->SetAttribute("version", XML_KEYBOARD_FILE_VERSION);
exportKeys(commands, KeyType::Command);
exportKeys(tools, KeyType::Tool);
exportKeys(quicktools, KeyType::Quicktool);
exportKeys(actions, KeyType::Action);
exportKeys(wheel, KeyType::WheelAction);
exportKeys(drag, KeyType::DragAction);
doc->InsertEndChild(doc->NewDeclaration("xml version=\"1.0\" encoding=\"utf-8\""));
doc->InsertEndChild(keyboard);
save_xml(doc.get(), filename);
}
void KeyboardShortcuts::exportKeys(XMLElement* parent, KeyType type)
{
for (KeyPtr& key : m_keys) {
// Save only user defined shortcuts.
if (key->type() != type)
continue;
for (const auto& kv : key->delsKeys())
if (kv.first == KeySource::UserDefined)
exportShortcut(parent, key.get(), kv.second, true);
for (const auto& kv : key->addsKeys())
if (kv.first == KeySource::UserDefined)
exportShortcut(parent, key.get(), kv.second, false);
}
}
// static
void KeyboardShortcuts::exportShortcut(XMLElement* parent,
const Key* key,
const ui::Shortcut& shortcut,
bool removed)
{
XMLElement* elem = parent->InsertNewChildElement("key");
switch (key->type()) {
case KeyType::Command: {
elem->SetAttribute("command", key->command()->id().c_str());
if (key->keycontext() != KeyContext::Any) {
elem->SetAttribute("context", base::convert_to<std::string>(key->keycontext()).c_str());
}
for (const auto& param : key->params()) {
if (param.second.empty())
continue;
XMLElement* paramElem = elem->InsertNewChildElement("param");
paramElem->SetAttribute("name", param.first.c_str());
paramElem->SetAttribute("value", param.second.c_str());
}
break;
}
case KeyType::Tool:
case KeyType::Quicktool: elem->SetAttribute("tool", key->tool()->getId().c_str()); break;
case KeyType::Action:
elem->SetAttribute("action", base::convert_to<std::string>(key->action()).c_str());
if (key->keycontext() != KeyContext::Any)
elem->SetAttribute("context", base::convert_to<std::string>(key->keycontext()).c_str());
break;
case KeyType::WheelAction:
elem->SetAttribute("action", base::convert_to<std::string>(key->wheelAction()).c_str());
break;
case KeyType::DragAction:
elem->SetAttribute("action", base::convert_to<std::string>(key->wheelAction()).c_str());
elem->SetAttribute("vector",
fmt::format("{},{}", key->dragVector().x, key->dragVector().y).c_str());
break;
}
elem->SetAttribute("shortcut", shortcut.toString().c_str());
if (removed)
elem->SetAttribute("removed", "true");
}
void KeyboardShortcuts::reset()
{
for (KeyPtr& key : m_keys)
key->reset();
}
KeyPtr KeyboardShortcuts::command(const char* commandName,
const Params& params,
const KeyContext keyContext) const
{
Command* command = Commands::instance()->byId(commandName);
if (!command)
return nullptr;
for (KeyPtr& key : m_keys) {
if (key->type() == KeyType::Command && key->keycontext() == keyContext &&
key->command() == command && key->params() == params) {
return key;
}
}
KeyPtr key = std::make_shared<Key>(command, params, keyContext);
m_keys.push_back(key);
return key;
}
KeyPtr KeyboardShortcuts::tool(tools::Tool* tool) const
{
for (KeyPtr& key : m_keys) {
if (key->type() == KeyType::Tool && key->tool() == tool) {
return key;
}
}
KeyPtr key = std::make_shared<Key>(KeyType::Tool, tool);
m_keys.push_back(key);
return key;
}
KeyPtr KeyboardShortcuts::quicktool(tools::Tool* tool) const
{
for (KeyPtr& key : m_keys) {
if (key->type() == KeyType::Quicktool && key->tool() == tool) {
return key;
}
}
KeyPtr key = std::make_shared<Key>(KeyType::Quicktool, tool);
m_keys.push_back(key);
return key;
}
KeyPtr KeyboardShortcuts::action(const KeyAction action, const KeyContext keyContext) const
{
for (KeyPtr& key : m_keys) {
if (key->type() == KeyType::Action && key->action() == action &&
key->keycontext() == keyContext) {
return key;
}
}
KeyPtr key = std::make_shared<Key>(action, keyContext);
m_keys.push_back(key);
return key;
}
KeyPtr KeyboardShortcuts::wheelAction(const WheelAction wheelAction) const
{
for (KeyPtr& key : m_keys) {
if (key->type() == KeyType::WheelAction && key->wheelAction() == wheelAction) {
return key;
}
}
KeyPtr key = std::make_shared<Key>(wheelAction);
m_keys.push_back(key);
return key;
}
KeyPtr KeyboardShortcuts::dragAction(const WheelAction dragAction) const
{
for (KeyPtr& key : m_keys) {
if (key->type() == KeyType::DragAction && key->wheelAction() == dragAction) {
return key;
}
}
KeyPtr key = Key::MakeDragAction(dragAction);
m_keys.push_back(key);
return key;
}
void KeyboardShortcuts::disableShortcut(const ui::Shortcut& shortcut,
const KeySource source,
const KeyContext keyContext,
const Key* newKey)
{
for (KeyPtr& key : m_keys) {
if (key.get() != newKey && key->keycontext() == keyContext && key->hasShortcut(shortcut) &&
// Tools can contain the same keyboard shortcut
(key->type() != KeyType::Tool || newKey == nullptr || newKey->type() != KeyType::Tool) &&
// DragActions can share the same keyboard shortcut (e.g. to
// change different values using different DragVectors)
(key->type() != KeyType::DragAction || newKey == nullptr ||
newKey->type() != KeyType::DragAction)) {
key->disableShortcut(shortcut, source);
}
}
}
// static
KeyContext KeyboardShortcuts::getCurrentKeyContext()
{
// For shortcuts to Apply/Cancel transformation/moving pixels state.
auto* editor = Editor::activeEditor();
if (editor && editor->isMovingPixels()) {
return KeyContext::Transformation;
}
auto* ctx = UIContext::instance();
Doc* doc = ctx->activeDocument();
if (doc && doc->isMaskVisible() &&
// The active key context will be the selectedTool() (in the
// toolbox) instead of the activeTool() (which depends on the
// quick tool shortcuts).
//
// E.g. If we have the rectangular marquee tool selected
// (selectedTool()) are going to press keys like alt+left or
// alt+right to move the selection edge in the selection
// context, the alt key switches the activeTool() to the
// eyedropper, but we want to use alt+left and alt+right in the
// original context (the selection tool).
App::instance()->activeToolManager()->selectedTool()->getInk(0)->isSelection()) {
return KeyContext::SelectionTool;
}
const view::RealRange& range = ctx->range();
if (doc && !range.selectedFrames().empty() &&
(range.type() == view::Range::kFrames || range.type() == view::Range::kCels)) {
return KeyContext::FramesSelection;
}
return KeyContext::Normal;
}
bool KeyboardShortcuts::getCommandFromKeyMessage(const Message* msg,
Command** command,
Params* params)
{
const KeyContext contexts[] = { getCurrentKeyContext(), KeyContext::Normal };
int n = (contexts[0] != contexts[1] ? 2 : 1);
for (int i = 0; i < n; ++i) {
for (KeyPtr& key : m_keys) {
if (key->type() == KeyType::Command && key->isPressed(msg, contexts[i])) {
if (command)
*command = key->command();
if (params)
*params = key->params();
return true;
}
}
}
return false;
}
tools::Tool* KeyboardShortcuts::getCurrentQuicktool(tools::Tool* currentTool)
{
if (currentTool && currentTool->getInk(0)->isSelection()) {
KeyPtr key = action(KeyAction::CopySelection, KeyContext::TranslatingSelection);
if (key && key->isPressed())
return NULL;
}
tools::ToolBox* toolbox = App::instance()->toolBox();
// Iterate over all tools
for (tools::Tool* tool : *toolbox) {
KeyPtr key = quicktool(tool);
// Collect all tools with the pressed keyboard-shortcut
if (key && key->isPressed()) {
return tool;
}
}
return NULL;
}
KeyAction KeyboardShortcuts::getCurrentActionModifiers(KeyContext context)
{
KeyAction flags = KeyAction::None;
for (const KeyPtr& key : m_keys) {
if (key->type() == KeyType::Action && key->keycontext() == context && key->isLooselyPressed()) {
flags = static_cast<KeyAction>(int(flags) | int(key->action()));
}
}
return flags;
}
WheelAction KeyboardShortcuts::getWheelActionFromMouseMessage(const KeyContext context,
const ui::Message* msg)
{
WheelAction wheelAction = WheelAction::None;
const ui::Shortcut* bestShortcut = nullptr;
for (const KeyPtr& key : m_keys) {
if (key->type() == KeyType::WheelAction && key->keycontext() == context) {
const ui::Shortcut* shortcut = key->isPressed(msg);
if ((shortcut) && (!bestShortcut || bestShortcut->modifiers() < shortcut->modifiers())) {
bestShortcut = shortcut;
wheelAction = key->wheelAction();
}
}
}
return wheelAction;
}
Keys KeyboardShortcuts::getDragActionsFromKeyMessage(const ui::Message* msg)
{
KeyPtr bestKey = nullptr;
Keys keys;
for (const KeyPtr& key : m_keys) {
if (key->type() == KeyType::DragAction) {
const ui::Shortcut* shortcut = key->isPressed(msg);
if (shortcut) {
keys.push_back(key);
}
}
}
return keys;
}
bool KeyboardShortcuts::hasMouseWheelCustomization() const
{
return std::any_of(m_keys.begin(), m_keys.end(), [](const KeyPtr& key) {
return (key->type() == KeyType::WheelAction && key->hasUserDefinedShortcuts());
});
}
void KeyboardShortcuts::clearMouseWheelKeys()
{
for (auto it = m_keys.begin(); it != m_keys.end();) {
if ((*it)->type() == KeyType::WheelAction)
it = m_keys.erase(it);
else
++it;
}
}
void KeyboardShortcuts::addMissingMouseWheelKeys()
{
for (int action = int(WheelAction::First); action <= int(WheelAction::Last); ++action) {
// Wheel actions
auto it = std::find_if(m_keys.begin(), m_keys.end(), [action](const KeyPtr& key) -> bool {
return key->type() == KeyType::WheelAction && key->wheelAction() == (WheelAction)action;
});
if (it == m_keys.end()) {
KeyPtr key = std::make_shared<Key>((WheelAction)action);
m_keys.push_back(key);
}
// Drag actions
it = std::find_if(m_keys.begin(), m_keys.end(), [action](const KeyPtr& key) -> bool {
return key->type() == KeyType::DragAction && key->wheelAction() == (WheelAction)action;
});
if (it == m_keys.end()) {
KeyPtr key = Key::MakeDragAction((WheelAction)action);
m_keys.push_back(key);
}
}
}
void KeyboardShortcuts::setDefaultMouseWheelKeys(const bool zoomWithWheel)
{
clearMouseWheelKeys();
KeyPtr key;
key = std::make_shared<Key>(WheelAction::Zoom);
key->add(Shortcut(zoomWithWheel ? kKeyNoneModifier : kKeyCtrlModifier, kKeyNil, 0),
KeySource::Original,
*this);
m_keys.push_back(key);
if (!zoomWithWheel) {
key = std::make_shared<Key>(WheelAction::VScroll);
key->add(Shortcut(kKeyNoneModifier, kKeyNil, 0), KeySource::Original, *this);
m_keys.push_back(key);
}
key = std::make_shared<Key>(WheelAction::HScroll);
key->add(Shortcut(kKeyShiftModifier, kKeyNil, 0), KeySource::Original, *this);
m_keys.push_back(key);
key = std::make_shared<Key>(WheelAction::FgColor);
key->add(Shortcut(kKeyAltModifier, kKeyNil, 0), KeySource::Original, *this);
m_keys.push_back(key);
key = std::make_shared<Key>(WheelAction::BgColor);
key->add(Shortcut((KeyModifiers)(kKeyAltModifier | kKeyShiftModifier), kKeyNil, 0),
KeySource::Original,
*this);
m_keys.push_back(key);
if (zoomWithWheel) {
key = std::make_shared<Key>(WheelAction::BrushSize);
key->add(Shortcut(kKeyCtrlModifier, kKeyNil, 0), KeySource::Original, *this);
m_keys.push_back(key);
key = std::make_shared<Key>(WheelAction::Frame);
key->add(Shortcut((KeyModifiers)(kKeyCtrlModifier | kKeyShiftModifier), kKeyNil, 0),
KeySource::Original,
*this);
m_keys.push_back(key);
}
}
void KeyboardShortcuts::addMissingKeysForCommands()
{
std::set<std::string> commandsAlreadyAdded;
for (const KeyPtr& key : m_keys) {
if (key->type() != KeyType::Command)
continue;
if (key->params().empty())
commandsAlreadyAdded.insert(key->command()->id());
}
std::vector<std::string> ids;
Commands* commands = Commands::instance();
commands->getAllIds(ids);
for (const std::string& id : ids) {
Command* command = commands->byId(id.c_str());
// Don't add commands that need params (they will be added to
// the list using the list of keyboard shortcuts from gui.xml).
if (command->needsParams())
continue;
auto it = commandsAlreadyAdded.find(command->id());
if (it != commandsAlreadyAdded.end())
continue;
// Create the new Key element in KeyboardShortcuts for this
// command without params.
this->command(command->id().c_str());
}
}
} // namespace app