1// Aseprite
2// Copyright (C) 2018-2022 Igara Studio S.A.
3// Copyright (C) 2001-2018 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/ui/keyboard_shortcuts.h"
13
14#include "app/app.h"
15#include "app/app_menus.h"
16#include "app/commands/command.h"
17#include "app/commands/commands.h"
18#include "app/commands/params.h"
19#include "app/doc.h"
20#include "app/i18n/strings.h"
21#include "app/tools/active_tool.h"
22#include "app/tools/ink.h"
23#include "app/tools/tool.h"
24#include "app/tools/tool_box.h"
25#include "app/ui/key.h"
26#include "app/ui_context.h"
27#include "app/xml_document.h"
28#include "app/xml_exception.h"
29#include "fmt/format.h"
30#include "ui/accelerator.h"
31#include "ui/message.h"
32
33#include <algorithm>
34#include <set>
35#include <vector>
36
37#define XML_KEYBOARD_FILE_VERSION "1"
38
39#define I18N_KEY(a) app::Strings::keyboard_shortcuts_##a()
40
41namespace {
42
43 struct KeyShortcutAction {
44 const char* name;
45 std::string userfriendly;
46 app::KeyAction action;
47 app::KeyContext context;
48 };
49
50 static std::vector<KeyShortcutAction> g_actions;
51
52 static const std::vector<KeyShortcutAction>& actions() {
53 if (g_actions.empty()) {
54 g_actions = std::vector<KeyShortcutAction> {
55 { "CopySelection" , I18N_KEY(copy_selection) , app::KeyAction::CopySelection, app::KeyContext::TranslatingSelection },
56 { "SnapToGrid" , I18N_KEY(snap_to_grid) , app::KeyAction::SnapToGrid, app::KeyContext::TranslatingSelection },
57 { "LockAxis" , I18N_KEY(lock_axis) , app::KeyAction::LockAxis, app::KeyContext::TranslatingSelection },
58 { "FineControl" , I18N_KEY(fine_translating) , app::KeyAction::FineControl , app::KeyContext::TranslatingSelection },
59 { "MaintainAspectRatio" , I18N_KEY(maintain_aspect_ratio) , app::KeyAction::MaintainAspectRatio, app::KeyContext::ScalingSelection },
60 { "ScaleFromCenter" , I18N_KEY(scale_from_center) , app::KeyAction::ScaleFromCenter, app::KeyContext::ScalingSelection },
61 { "FineControl" , I18N_KEY(fine_scaling) , app::KeyAction::FineControl , app::KeyContext::ScalingSelection },
62 { "AngleSnap" , I18N_KEY(angle_snap) , app::KeyAction::AngleSnap, app::KeyContext::RotatingSelection },
63 { "AddSelection" , I18N_KEY(add_selection) , app::KeyAction::AddSelection, app::KeyContext::SelectionTool },
64 { "SubtractSelection" , I18N_KEY(subtract_selection) , app::KeyAction::SubtractSelection, app::KeyContext::SelectionTool },
65 { "IntersectSelection" , I18N_KEY(intersect_selection) , app::KeyAction::IntersectSelection, app::KeyContext::SelectionTool },
66 { "AutoSelectLayer" , I18N_KEY(auto_select_layer) , app::KeyAction::AutoSelectLayer, app::KeyContext::MoveTool },
67 { "StraightLineFromLastPoint", I18N_KEY(line_from_last_point) , app::KeyAction::StraightLineFromLastPoint, app::KeyContext::FreehandTool },
68 { "AngleSnapFromLastPoint", I18N_KEY(angle_from_last_point) , app::KeyAction::AngleSnapFromLastPoint, app::KeyContext::FreehandTool },
69 { "MoveOrigin" , I18N_KEY(move_origin) , app::KeyAction::MoveOrigin, app::KeyContext::ShapeTool },
70 { "SquareAspect" , I18N_KEY(square_aspect) , app::KeyAction::SquareAspect, app::KeyContext::ShapeTool },
71 { "DrawFromCenter" , I18N_KEY(draw_from_center) , app::KeyAction::DrawFromCenter, app::KeyContext::ShapeTool },
72 { "RotateShape" , I18N_KEY(rotate_shape) , app::KeyAction::RotateShape, app::KeyContext::ShapeTool },
73 { "LeftMouseButton" , I18N_KEY(trigger_left_mouse_button) , app::KeyAction::LeftMouseButton, app::KeyContext::Any },
74 { "RightMouseButton" , I18N_KEY(trigger_right_mouse_button) , app::KeyAction::RightMouseButton, app::KeyContext::Any }
75 };
76 }
77 return g_actions;
78 }
79
80 static struct {
81 const char* name;
82 app::KeyContext context;
83 } g_contexts[] = {
84 { "" , app::KeyContext::Any },
85 { "Normal" , app::KeyContext::Normal },
86 { "Selection" , app::KeyContext::SelectionTool },
87 { "TranslatingSelection" , app::KeyContext::TranslatingSelection },
88 { "ScalingSelection" , app::KeyContext::ScalingSelection },
89 { "RotatingSelection" , app::KeyContext::RotatingSelection },
90 { "MoveTool" , app::KeyContext::MoveTool },
91 { "FreehandTool" , app::KeyContext::FreehandTool },
92 { "ShapeTool" , app::KeyContext::ShapeTool },
93 { NULL , app::KeyContext::Any }
94 };
95
96 using Vec = app::DragVector;
97
98 struct KeyShortcutWheelAction {
99 const char* name;
100 const std::string userfriendly;
101 Vec vector;
102 };
103
104 static std::vector<KeyShortcutWheelAction> g_wheel_actions;
105
106 static const std::vector<KeyShortcutWheelAction>& wheel_actions() {
107 if (g_wheel_actions.empty()) {
108 g_wheel_actions = std::vector<KeyShortcutWheelAction> {
109 { "" , "" , Vec(0.0, 0.0) },
110 { "Zoom" , I18N_KEY(zoom) , Vec(8.0, 0.0) },
111 { "VScroll" , I18N_KEY(scroll_vertically) , Vec(4.0, 0.0) },
112 { "HScroll" , I18N_KEY(scroll_horizontally) , Vec(4.0, 0.0) },
113 { "FgColor" , I18N_KEY(fg_color) , Vec(8.0, 0.0) },
114 { "BgColor" , I18N_KEY(bg_color) , Vec(8.0, 0.0) },
115 { "FgTile" , I18N_KEY(fg_tile) , Vec(8.0, 0.0) },
116 { "BgTile" , I18N_KEY(bg_tile) , Vec(8.0, 0.0) },
117 { "Frame" , I18N_KEY(change_frame) , Vec(16.0, 0.0) },
118 { "BrushSize" , I18N_KEY(change_brush_size) , Vec(4.0, 0.0) },
119 { "BrushAngle" , I18N_KEY(change_brush_angle) , Vec(-4.0, 0.0) },
120 { "ToolSameGroup" , I18N_KEY(change_tool_same_group) , Vec(8.0, 0.0) },
121 { "ToolOtherGroup", I18N_KEY(change_tool) , Vec(0.0, -8.0) },
122 { "Layer" , I18N_KEY(change_layer) , Vec(0.0, 8.0) },
123 { "InkType" , I18N_KEY(change_ink_type) , Vec(0.0, -16.0) },
124 { "InkOpacity" , I18N_KEY(change_ink_opacity) , Vec(0.0, 1.0) },
125 { "LayerOpacity" , I18N_KEY(change_layer_opacity) , Vec(0.0, 1.0) },
126 { "CelOpacity" , I18N_KEY(change_cel_opacity) , Vec(0.0, 1.0) },
127 { "Alpha" , I18N_KEY(color_alpha) , Vec(4.0, 0.0) },
128 { "HslHue" , I18N_KEY(color_hsl_hue) , Vec(1.0, 0.0) },
129 { "HslSaturation" , I18N_KEY(color_hsl_saturation) , Vec(4.0, 0.0) },
130 { "HslLightness" , I18N_KEY(color_hsl_lightness) , Vec(0.0, 4.0) },
131 { "HsvHue" , I18N_KEY(color_hsv_hue) , Vec(1.0, 0.0) },
132 { "HsvSaturation" , I18N_KEY(color_hsv_saturation) , Vec(4.0, 0.0) },
133 { "HsvValue" , I18N_KEY(color_hsv_value) , Vec(0.0, 4.0) }
134 };
135 }
136 return g_wheel_actions;
137 }
138
139 const char* get_shortcut(TiXmlElement* elem) {
140 const char* shortcut = NULL;
141
142#ifdef _WIN32
143 if (!shortcut) shortcut = elem->Attribute("win");
144#elif defined __APPLE__
145 if (!shortcut) shortcut = elem->Attribute("mac");
146#else
147 if (!shortcut) shortcut = elem->Attribute("linux");
148#endif
149
150 if (!shortcut)
151 shortcut = elem->Attribute("shortcut");
152
153 return shortcut;
154 }
155
156 std::string get_user_friendly_string_for_keyaction(app::KeyAction action,
157 app::KeyContext context) {
158 for (const auto& a : actions()) {
159 if (action == a.action && context == a.context)
160 return a.userfriendly;
161 }
162 return std::string();
163 }
164
165 std::string get_user_friendly_string_for_wheelaction(app::WheelAction wheelAction) {
166 int c = int(wheelAction);
167 if (c >= int(app::WheelAction::First) &&
168 c <= int(app::WheelAction::Last)) {
169 return wheel_actions()[c].userfriendly;
170 }
171 else
172 return std::string();
173 }
174
175 void erase_accel(app::KeySourceAccelList& kvs,
176 const app::KeySource source,
177 const ui::Accelerator& accel) {
178 for (auto it=kvs.begin(); it!=kvs.end(); ) {
179 auto& kv = *it;
180 if (kv.first == source &&
181 kv.second == accel) {
182 it = kvs.erase(it);
183 }
184 else
185 ++it;
186 }
187 }
188
189 void erase_accels(app::KeySourceAccelList& kvs,
190 const app::KeySource source) {
191 for (auto it=kvs.begin(); it!=kvs.end(); ) {
192 auto& kv = *it;
193 if (kv.first == source) {
194 it = kvs.erase(it);
195 }
196 else
197 ++it;
198 }
199 }
200
201} // anonymous namespace
202
203namespace base {
204
205 template<> app::KeyAction convert_to(const std::string& from) {
206 for (const auto& a : actions()) {
207 if (from == a.name)
208 return a.action;
209 }
210 return app::KeyAction::None;
211 }
212
213 template<> std::string convert_to(const app::KeyAction& from) {
214 for (const auto& a : actions()) {
215 if (from == a.action)
216 return a.name;
217 }
218 return std::string();
219 }
220
221 template<> app::WheelAction convert_to(const std::string& from) {
222 for (int c=int(app::WheelAction::First);
223 c<=int(app::WheelAction::Last); ++c) {
224 if (from == wheel_actions()[c].name)
225 return (app::WheelAction)c;
226 }
227 return app::WheelAction::None;
228 }
229
230 template<> std::string convert_to(const app::WheelAction& from) {
231 int c = int(from);
232 if (c >= int(app::WheelAction::First) &&
233 c <= int(app::WheelAction::Last)) {
234 return wheel_actions()[c].name;
235 }
236 else
237 return std::string();
238 }
239
240 template<> app::KeyContext convert_to(const std::string& from) {
241 for (int c=0; g_contexts[c].name; ++c) {
242 if (from == g_contexts[c].name)
243 return g_contexts[c].context;
244 }
245 return app::KeyContext::Any;
246 }
247
248 template<> std::string convert_to(const app::KeyContext& from) {
249 for (int c=0; g_contexts[c].name; ++c) {
250 if (from == g_contexts[c].context)
251 return g_contexts[c].name;
252 }
253 return std::string();
254 }
255
256} // namespace base
257
258namespace app {
259
260using namespace ui;
261
262//////////////////////////////////////////////////////////////////////
263// Key
264
265Key::Key(const Key& k)
266 : m_type(k.m_type)
267 , m_adds(k.m_adds)
268 , m_dels(k.m_dels)
269 , m_keycontext(k.m_keycontext)
270{
271 switch (m_type) {
272 case KeyType::Command:
273 m_command = k.m_command;
274 m_params = k.m_params;
275 break;
276 case KeyType::Tool:
277 case KeyType::Quicktool:
278 m_tool = k.m_tool;
279 break;
280 case KeyType::Action:
281 m_action = k.m_action;
282 break;
283 case KeyType::WheelAction:
284 m_action = k.m_action;
285 m_wheelAction = k.m_wheelAction;
286 break;
287 case KeyType::DragAction:
288 m_action = k.m_action;
289 m_wheelAction = k.m_wheelAction;
290 m_dragVector = k.m_dragVector;
291 break;
292 }
293}
294
295Key::Key(Command* command, const Params& params, const KeyContext keyContext)
296 : m_type(KeyType::Command)
297 , m_keycontext(keyContext)
298 , m_command(command)
299 , m_params(params)
300{
301}
302
303Key::Key(const KeyType type, tools::Tool* tool)
304 : m_type(type)
305 , m_keycontext(KeyContext::Any)
306 , m_tool(tool)
307{
308}
309
310Key::Key(const KeyAction action,
311 const KeyContext keyContext)
312 : m_type(KeyType::Action)
313 , m_keycontext(keyContext)
314 , m_action(action)
315{
316 if (m_keycontext != KeyContext::Any)
317 return;
318
319 // Automatic key context
320 switch (action) {
321 case KeyAction::None:
322 m_keycontext = KeyContext::Any;
323 break;
324 case KeyAction::CopySelection:
325 case KeyAction::SnapToGrid:
326 case KeyAction::LockAxis:
327 case KeyAction::FineControl:
328 m_keycontext = KeyContext::TranslatingSelection;
329 break;
330 case KeyAction::AngleSnap:
331 m_keycontext = KeyContext::RotatingSelection;
332 break;
333 case KeyAction::MaintainAspectRatio:
334 case KeyAction::ScaleFromCenter:
335 m_keycontext = KeyContext::ScalingSelection;
336 break;
337 case KeyAction::AddSelection:
338 case KeyAction::SubtractSelection:
339 case KeyAction::IntersectSelection:
340 m_keycontext = KeyContext::SelectionTool;
341 break;
342 case KeyAction::AutoSelectLayer:
343 m_keycontext = KeyContext::MoveTool;
344 break;
345 case KeyAction::StraightLineFromLastPoint:
346 case KeyAction::AngleSnapFromLastPoint:
347 m_keycontext = KeyContext::FreehandTool;
348 break;
349 case KeyAction::MoveOrigin:
350 case KeyAction::SquareAspect:
351 case KeyAction::DrawFromCenter:
352 case KeyAction::RotateShape:
353 m_keycontext = KeyContext::ShapeTool;
354 break;
355 case KeyAction::LeftMouseButton:
356 m_keycontext = KeyContext::Any;
357 break;
358 case KeyAction::RightMouseButton:
359 m_keycontext = KeyContext::Any;
360 break;
361 }
362}
363
364Key::Key(const WheelAction wheelAction)
365 : m_type(KeyType::WheelAction)
366 , m_keycontext(KeyContext::MouseWheel)
367 , m_action(KeyAction::None)
368 , m_wheelAction(wheelAction)
369{
370}
371
372// static
373KeyPtr Key::MakeDragAction(WheelAction dragAction)
374{
375 KeyPtr k(new Key(dragAction));
376 k->m_type = KeyType::DragAction;
377 k->m_keycontext = KeyContext::Any;
378 k->m_dragVector = wheel_actions()[(int)dragAction].vector;
379 return k;
380}
381
382const ui::Accelerators& Key::accels() const
383{
384 if (!m_accels) {
385 m_accels = std::make_unique<ui::Accelerators>();
386
387 // Add default keys
388 for (const auto& kv : m_adds) {
389 if (kv.first == KeySource::Original)
390 m_accels->add(kv.second);
391 }
392
393 // Delete/add extension-defined keys
394 for (const auto& kv : m_dels) {
395 if (kv.first == KeySource::ExtensionDefined)
396 m_accels->remove(kv.second);
397 else {
398 ASSERT(kv.first != KeySource::Original);
399 }
400 }
401 for (const auto& kv : m_adds) {
402 if (kv.first == KeySource::ExtensionDefined)
403 m_accels->add(kv.second);
404 }
405
406 // Delete/add user-defined keys
407 for (const auto& kv : m_dels) {
408 if (kv.first == KeySource::UserDefined)
409 m_accels->remove(kv.second);
410 }
411 for (const auto& kv : m_adds) {
412 if (kv.first == KeySource::UserDefined)
413 m_accels->add(kv.second);
414 }
415 }
416 return *m_accels;
417}
418
419void Key::add(const ui::Accelerator& accel,
420 const KeySource source,
421 KeyboardShortcuts& globalKeys)
422{
423 m_adds.emplace_back(source, accel);
424 m_accels.reset();
425
426 // Remove the accelerator from other commands
427 if (source == KeySource::ExtensionDefined ||
428 source == KeySource::UserDefined) {
429 erase_accel(m_dels, source, accel);
430
431 globalKeys.disableAccel(accel, source, m_keycontext, this);
432 }
433}
434
435const ui::Accelerator* Key::isPressed(const Message* msg,
436 KeyboardShortcuts& globalKeys) const
437{
438 if (auto keyMsg = dynamic_cast<const KeyMessage*>(msg)) {
439 for (const Accelerator& accel : accels()) {
440 if (accel.isPressed(keyMsg->modifiers(),
441 keyMsg->scancode(),
442 keyMsg->unicodeChar()) &&
443 (m_keycontext == KeyContext::Any ||
444 m_keycontext == globalKeys.getCurrentKeyContext())) {
445 return &accel;
446 }
447 }
448 }
449 else if (auto mouseMsg = dynamic_cast<const MouseMessage*>(msg)) {
450 for (const Accelerator& accel : accels()) {
451 if ((accel.modifiers() == mouseMsg->modifiers()) &&
452 (m_keycontext == KeyContext::Any ||
453 // TODO we could have multiple mouse wheel key-context,
454 // like "sprite editor" context, or "timeline" context,
455 // etc.
456 m_keycontext == KeyContext::MouseWheel)) {
457 return &accel;
458 }
459 }
460 }
461 return nullptr;
462}
463
464bool Key::isPressed() const
465{
466 for (const Accelerator& accel : this->accels()) {
467 if (accel.isPressed())
468 return true;
469 }
470 return false;
471}
472
473bool Key::isLooselyPressed() const
474{
475 for (const Accelerator& accel : this->accels()) {
476 if (accel.isLooselyPressed())
477 return true;
478 }
479 return false;
480}
481
482bool Key::hasAccel(const ui::Accelerator& accel) const
483{
484 return accels().has(accel);
485}
486
487bool Key::hasUserDefinedAccels() const
488{
489 for (const auto& kv : m_adds) {
490 if (kv.first == KeySource::UserDefined)
491 return true;
492 }
493 return false;
494}
495
496void Key::disableAccel(const ui::Accelerator& accel,
497 const KeySource source)
498{
499 // It doesn't make sense that the default keyboard shortcuts file
500 // (gui.xml) removes some accelerator.
501 ASSERT(source != KeySource::Original);
502
503 erase_accel(m_adds, source, accel);
504 erase_accel(m_dels, source, accel);
505
506 m_dels.emplace_back(source, accel);
507 m_accels.reset();
508}
509
510void Key::reset()
511{
512 erase_accels(m_adds, KeySource::UserDefined);
513 erase_accels(m_dels, KeySource::UserDefined);
514 m_accels.reset();
515}
516
517void Key::copyOriginalToUser()
518{
519 // Erase all user-defined keys
520 erase_accels(m_adds, KeySource::UserDefined);
521 erase_accels(m_dels, KeySource::UserDefined);
522
523 // Then copy all original & extension-defined keys as user-defined
524 auto copy = m_adds;
525 for (const auto& kv : copy)
526 m_adds.emplace_back(KeySource::UserDefined, kv.second);
527 m_accels.reset();
528}
529
530std::string Key::triggerString() const
531{
532 switch (m_type) {
533 case KeyType::Command:
534 m_command->loadParams(m_params);
535 return m_command->friendlyName();
536 case KeyType::Tool:
537 case KeyType::Quicktool: {
538 std::string text = m_tool->getText();
539 if (m_type == KeyType::Quicktool)
540 text += " (quick)";
541 return text;
542 }
543 case KeyType::Action:
544 return get_user_friendly_string_for_keyaction(m_action,
545 m_keycontext);
546 case KeyType::WheelAction:
547 case KeyType::DragAction:
548 return get_user_friendly_string_for_wheelaction(m_wheelAction);
549 }
550 return "Unknown";
551}
552
553//////////////////////////////////////////////////////////////////////
554// KeyboardShortcuts
555
556static std::unique_ptr<KeyboardShortcuts> g_singleton;
557
558// static
559KeyboardShortcuts* KeyboardShortcuts::instance()
560{
561 if (!g_singleton)
562 g_singleton = std::make_unique<KeyboardShortcuts>();
563 return g_singleton.get();
564}
565
566// static
567void KeyboardShortcuts::destroyInstance()
568{
569 g_singleton.reset();
570}
571
572KeyboardShortcuts::KeyboardShortcuts()
573{
574 ASSERT(Strings::instance());
575 Strings::instance()->LanguageChange.connect([]{
576 // Clear collections so they are re-constructed with the new language
577 g_actions.clear();
578 g_wheel_actions.clear();
579 });
580}
581
582KeyboardShortcuts::~KeyboardShortcuts()
583{
584 clear();
585}
586
587void KeyboardShortcuts::setKeys(const KeyboardShortcuts& keys,
588 const bool cloneKeys)
589{
590 if (cloneKeys) {
591 for (const KeyPtr& key : keys)
592 m_keys.push_back(std::make_shared<Key>(*key));
593 }
594 else {
595 m_keys = keys.m_keys;
596 }
597 UserChange();
598}
599
600void KeyboardShortcuts::clear()
601{
602 m_keys.clear();
603}
604
605void KeyboardShortcuts::importFile(TiXmlElement* rootElement, KeySource source)
606{
607 // <keyboard><commands><key>
608 TiXmlHandle handle(rootElement);
609 TiXmlElement* xmlKey = handle
610 .FirstChild("commands")
611 .FirstChild("key").ToElement();
612 while (xmlKey) {
613 const char* command_name = xmlKey->Attribute("command");
614 const char* command_key = get_shortcut(xmlKey);
615 bool removed = bool_attr(xmlKey, "removed", false);
616
617 if (command_name) {
618 Command* command = Commands::instance()->byId(command_name);
619 if (command) {
620 // Read context
621 KeyContext keycontext = KeyContext::Any;
622 const char* keycontextstr = xmlKey->Attribute("context");
623 if (keycontextstr)
624 keycontext = base::convert_to<KeyContext>(std::string(keycontextstr));
625
626 // Read params
627 Params params;
628
629 TiXmlElement* xmlParam = xmlKey->FirstChildElement("param");
630 while (xmlParam) {
631 const char* param_name = xmlParam->Attribute("name");
632 const char* param_value = xmlParam->Attribute("value");
633
634 if (param_name && param_value)
635 params.set(param_name, param_value);
636
637 xmlParam = xmlParam->NextSiblingElement();
638 }
639
640 // add the keyboard shortcut to the command
641 KeyPtr key = this->command(command_name, params, keycontext);
642 if (key && command_key) {
643 Accelerator accel(command_key);
644
645 if (!removed) {
646 key->add(accel, source, *this);
647
648 // Add the shortcut to the menuitems with this command
649 // (this is only visual, the
650 // "CustomizedGuiManager::onProcessMessage" is the only
651 // one that process keyboard shortcuts)
652 if (key->accels().size() == 1) {
653 AppMenus::instance()->applyShortcutToMenuitemsWithCommand(
654 command, params, key);
655 }
656 }
657 else
658 key->disableAccel(accel, source);
659 }
660 }
661 }
662
663 xmlKey = xmlKey->NextSiblingElement();
664 }
665
666 // Load keyboard shortcuts for tools
667 // <keyboard><tools><key>
668 xmlKey = handle
669 .FirstChild("tools")
670 .FirstChild("key").ToElement();
671 while (xmlKey) {
672 const char* tool_id = xmlKey->Attribute("tool");
673 const char* tool_key = get_shortcut(xmlKey);
674 bool removed = bool_attr(xmlKey, "removed", false);
675
676 if (tool_id) {
677 tools::Tool* tool = App::instance()->toolBox()->getToolById(tool_id);
678 if (tool) {
679 KeyPtr key = this->tool(tool);
680 if (key && tool_key) {
681 LOG(VERBOSE, "KEYS: Shortcut for tool %s: %s\n", tool_id, tool_key);
682 Accelerator accel(tool_key);
683
684 if (!removed)
685 key->add(accel, source, *this);
686 else
687 key->disableAccel(accel, source);
688 }
689 }
690 }
691 xmlKey = xmlKey->NextSiblingElement();
692 }
693
694 // Load keyboard shortcuts for quicktools
695 // <keyboard><quicktools><key>
696 xmlKey = handle
697 .FirstChild("quicktools")
698 .FirstChild("key").ToElement();
699 while (xmlKey) {
700 const char* tool_id = xmlKey->Attribute("tool");
701 const char* tool_key = get_shortcut(xmlKey);
702 bool removed = bool_attr(xmlKey, "removed", false);
703
704 if (tool_id) {
705 tools::Tool* tool = App::instance()->toolBox()->getToolById(tool_id);
706 if (tool) {
707 KeyPtr key = this->quicktool(tool);
708 if (key && tool_key) {
709 LOG(VERBOSE, "KEYS: Shortcut for quicktool %s: %s\n", tool_id, tool_key);
710 Accelerator accel(tool_key);
711
712 if (!removed)
713 key->add(accel, source, *this);
714 else
715 key->disableAccel(accel, source);
716 }
717 }
718 }
719 xmlKey = xmlKey->NextSiblingElement();
720 }
721
722 // Load special keyboard shortcuts for sprite editor customization
723 // <keyboard><actions><key>
724 xmlKey = handle
725 .FirstChild("actions")
726 .FirstChild("key").ToElement();
727 while (xmlKey) {
728 const char* action_id = xmlKey->Attribute("action");
729 const char* action_key = get_shortcut(xmlKey);
730 bool removed = bool_attr(xmlKey, "removed", false);
731
732 if (action_id) {
733 KeyAction action = base::convert_to<KeyAction, std::string>(action_id);
734 if (action != KeyAction::None) {
735 // Read context
736 KeyContext keycontext = KeyContext::Any;
737 const char* keycontextstr = xmlKey->Attribute("context");
738 if (keycontextstr)
739 keycontext = base::convert_to<KeyContext>(std::string(keycontextstr));
740
741 KeyPtr key = this->action(action, keycontext);
742 if (key && action_key) {
743 LOG(VERBOSE, "KEYS: Shortcut for action %s/%s: %s\n", action_id,
744 (keycontextstr ? keycontextstr: "Any"), action_key);
745 Accelerator accel(action_key);
746
747 if (!removed)
748 key->add(accel, source, *this);
749 else
750 key->disableAccel(accel, source);
751 }
752 }
753 }
754 xmlKey = xmlKey->NextSiblingElement();
755 }
756
757 // Load special keyboard shortcuts for mouse wheel customization
758 // <keyboard><wheel><key>
759 xmlKey = handle
760 .FirstChild("wheel")
761 .FirstChild("key").ToElement();
762 while (xmlKey) {
763 const char* action_id = xmlKey->Attribute("action");
764 const char* action_key = get_shortcut(xmlKey);
765 bool removed = bool_attr(xmlKey, "removed", false);
766
767 if (action_id) {
768 WheelAction action = base::convert_to<WheelAction, std::string>(action_id);
769 if (action != WheelAction::None) {
770 KeyPtr key = this->wheelAction(action);
771 if (key && action_key) {
772 LOG(VERBOSE, "KEYS: Shortcut for wheel action %s: %s\n", action_id, action_key);
773 Accelerator accel(action_key);
774
775 if (!removed)
776 key->add(accel, source, *this);
777 else
778 key->disableAccel(accel, source);
779 }
780 }
781 }
782 xmlKey = xmlKey->NextSiblingElement();
783 }
784
785 // Load special keyboard shortcuts to simulate mouse wheel actions
786 // <keyboard><drag><key>
787 xmlKey = handle
788 .FirstChild("drag")
789 .FirstChild("key").ToElement();
790 while (xmlKey) {
791 const char* action_id = xmlKey->Attribute("action");
792 const char* action_key = get_shortcut(xmlKey);
793 bool removed = bool_attr(xmlKey, "removed", false);
794
795 if (action_id) {
796 WheelAction action = base::convert_to<WheelAction, std::string>(action_id);
797 if (action != WheelAction::None) {
798 KeyPtr key = this->dragAction(action);
799 if (key && action_key) {
800 if (auto vector_str = xmlKey->Attribute("vector")) {
801 double x, y = 0.0;
802 // Parse a string like "double,double"
803 x = std::strtod(vector_str, (char**)&vector_str);
804 if (vector_str && *vector_str == ',') {
805 ++vector_str;
806 y = std::strtod(vector_str, nullptr);
807 }
808 key->setDragVector(DragVector(x, y));
809 }
810
811 LOG(VERBOSE, "KEYS: Shortcut for drag action %s: %s\n", action_id, action_key);
812 Accelerator accel(action_key);
813
814 if (!removed)
815 key->add(accel, source, *this);
816 else
817 key->disableAccel(accel, source);
818 }
819 }
820 }
821 xmlKey = xmlKey->NextSiblingElement();
822 }
823}
824
825void KeyboardShortcuts::importFile(const std::string& filename, KeySource source)
826{
827 XmlDocumentRef doc = app::open_xml(filename);
828 TiXmlHandle handle(doc.get());
829 TiXmlElement* xmlKey = handle.FirstChild("keyboard").ToElement();
830
831 importFile(xmlKey, source);
832}
833
834void KeyboardShortcuts::exportFile(const std::string& filename)
835{
836 XmlDocumentRef doc(new TiXmlDocument());
837
838 TiXmlElement keyboard("keyboard");
839 TiXmlElement commands("commands");
840 TiXmlElement tools("tools");
841 TiXmlElement quicktools("quicktools");
842 TiXmlElement actions("actions");
843 TiXmlElement wheel("wheel");
844 TiXmlElement drag("drag");
845
846 keyboard.SetAttribute("version", XML_KEYBOARD_FILE_VERSION);
847
848 exportKeys(commands, KeyType::Command);
849 exportKeys(tools, KeyType::Tool);
850 exportKeys(quicktools, KeyType::Quicktool);
851 exportKeys(actions, KeyType::Action);
852 exportKeys(wheel, KeyType::WheelAction);
853 exportKeys(drag, KeyType::DragAction);
854
855 keyboard.InsertEndChild(commands);
856 keyboard.InsertEndChild(tools);
857 keyboard.InsertEndChild(quicktools);
858 keyboard.InsertEndChild(actions);
859 keyboard.InsertEndChild(wheel);
860 keyboard.InsertEndChild(drag);
861
862 TiXmlDeclaration declaration("1.0", "utf-8", "");
863 doc->InsertEndChild(declaration);
864 doc->InsertEndChild(keyboard);
865 save_xml(doc, filename);
866}
867
868void KeyboardShortcuts::exportKeys(TiXmlElement& parent, KeyType type)
869{
870 for (KeyPtr& key : m_keys) {
871 // Save only user defined accelerators.
872 if (key->type() != type)
873 continue;
874
875 for (const auto& kv : key->delsKeys())
876 if (kv.first == KeySource::UserDefined)
877 exportAccel(parent, key.get(), kv.second, true);
878
879 for (const auto& kv : key->addsKeys())
880 if (kv.first == KeySource::UserDefined)
881 exportAccel(parent, key.get(), kv.second, false);
882 }
883}
884
885void KeyboardShortcuts::exportAccel(TiXmlElement& parent, const Key* key, const ui::Accelerator& accel, bool removed)
886{
887 TiXmlElement elem("key");
888
889 switch (key->type()) {
890
891 case KeyType::Command: {
892 elem.SetAttribute("command", key->command()->id().c_str());
893
894 if (key->keycontext() != KeyContext::Any) {
895 elem.SetAttribute("context",
896 base::convert_to<std::string>(key->keycontext()).c_str());
897 }
898
899 for (const auto& param : key->params()) {
900 if (param.second.empty())
901 continue;
902
903 TiXmlElement paramElem("param");
904 paramElem.SetAttribute("name", param.first.c_str());
905 paramElem.SetAttribute("value", param.second.c_str());
906 elem.InsertEndChild(paramElem);
907 }
908 break;
909 }
910
911 case KeyType::Tool:
912 case KeyType::Quicktool:
913 elem.SetAttribute("tool", key->tool()->getId().c_str());
914 break;
915
916 case KeyType::Action:
917 elem.SetAttribute("action",
918 base::convert_to<std::string>(key->action()).c_str());
919 if (key->keycontext() != KeyContext::Any)
920 elem.SetAttribute("context",
921 base::convert_to<std::string>(key->keycontext()).c_str());
922 break;
923
924 case KeyType::WheelAction:
925 elem.SetAttribute("action",
926 base::convert_to<std::string>(key->wheelAction()));
927 break;
928
929 case KeyType::DragAction:
930 elem.SetAttribute("action",
931 base::convert_to<std::string>(key->wheelAction()));
932 elem.SetAttribute("vector",
933 fmt::format("{},{}",
934 key->dragVector().x,
935 key->dragVector().y));
936 break;
937 }
938
939 elem.SetAttribute("shortcut", accel.toString().c_str());
940
941 if (removed)
942 elem.SetAttribute("removed", "true");
943
944 parent.InsertEndChild(elem);
945}
946
947void KeyboardShortcuts::reset()
948{
949 for (KeyPtr& key : m_keys)
950 key->reset();
951}
952
953KeyPtr KeyboardShortcuts::command(const char* commandName, const Params& params, KeyContext keyContext)
954{
955 Command* command = Commands::instance()->byId(commandName);
956 if (!command)
957 return nullptr;
958
959 for (KeyPtr& key : m_keys) {
960 if (key->type() == KeyType::Command &&
961 key->keycontext() == keyContext &&
962 key->command() == command &&
963 key->params() == params) {
964 return key;
965 }
966 }
967
968 KeyPtr key = std::make_shared<Key>(command, params, keyContext);
969 m_keys.push_back(key);
970 return key;
971}
972
973KeyPtr KeyboardShortcuts::tool(tools::Tool* tool)
974{
975 for (KeyPtr& key : m_keys) {
976 if (key->type() == KeyType::Tool &&
977 key->tool() == tool) {
978 return key;
979 }
980 }
981
982 KeyPtr key = std::make_shared<Key>(KeyType::Tool, tool);
983 m_keys.push_back(key);
984 return key;
985}
986
987KeyPtr KeyboardShortcuts::quicktool(tools::Tool* tool)
988{
989 for (KeyPtr& key : m_keys) {
990 if (key->type() == KeyType::Quicktool &&
991 key->tool() == tool) {
992 return key;
993 }
994 }
995
996 KeyPtr key = std::make_shared<Key>(KeyType::Quicktool, tool);
997 m_keys.push_back(key);
998 return key;
999}
1000
1001KeyPtr KeyboardShortcuts::action(KeyAction action,
1002 KeyContext keyContext)
1003{
1004 for (KeyPtr& key : m_keys) {
1005 if (key->type() == KeyType::Action &&
1006 key->action() == action &&
1007 key->keycontext() == keyContext) {
1008 return key;
1009 }
1010 }
1011
1012 KeyPtr key = std::make_shared<Key>(action, keyContext);
1013 m_keys.push_back(key);
1014 return key;
1015}
1016
1017KeyPtr KeyboardShortcuts::wheelAction(WheelAction wheelAction)
1018{
1019 for (KeyPtr& key : m_keys) {
1020 if (key->type() == KeyType::WheelAction &&
1021 key->wheelAction() == wheelAction) {
1022 return key;
1023 }
1024 }
1025
1026 KeyPtr key = std::make_shared<Key>(wheelAction);
1027 m_keys.push_back(key);
1028 return key;
1029}
1030
1031KeyPtr KeyboardShortcuts::dragAction(WheelAction dragAction)
1032{
1033 for (KeyPtr& key : m_keys) {
1034 if (key->type() == KeyType::DragAction &&
1035 key->wheelAction() == dragAction) {
1036 return key;
1037 }
1038 }
1039
1040 KeyPtr key = Key::MakeDragAction(dragAction);
1041 m_keys.push_back(key);
1042 return key;
1043}
1044
1045void KeyboardShortcuts::disableAccel(const ui::Accelerator& accel,
1046 const KeySource source,
1047 const KeyContext keyContext,
1048 const Key* newKey)
1049{
1050 for (KeyPtr& key : m_keys) {
1051 if (key.get() != newKey &&
1052 key->keycontext() == keyContext &&
1053 key->hasAccel(accel) &&
1054 // Tools can contain the same keyboard shortcut
1055 (key->type() != KeyType::Tool ||
1056 newKey == nullptr ||
1057 newKey->type() != KeyType::Tool) &&
1058 // DragActions can share the same keyboard shortcut (e.g. to
1059 // change different values using different DragVectors)
1060 (key->type() != KeyType::DragAction ||
1061 newKey == nullptr ||
1062 newKey->type() != KeyType::DragAction)) {
1063 key->disableAccel(accel, source);
1064 }
1065 }
1066}
1067
1068KeyContext KeyboardShortcuts::getCurrentKeyContext()
1069{
1070 Doc* doc = UIContext::instance()->activeDocument();
1071 if (doc &&
1072 doc->isMaskVisible() &&
1073 // The active key context will be the selectedTool() (in the
1074 // toolbox) instead of the activeTool() (which depends on the
1075 // quick tool shortcuts).
1076 //
1077 // E.g. If we have the rectangular marquee tool selected
1078 // (selectedTool()) are going to press keys like alt+left or
1079 // alt+right to move the selection edge in the selection
1080 // context, the alt key switches the activeTool() to the
1081 // eyedropper, but we want to use alt+left and alt+right in the
1082 // original context (the selection tool).
1083 App::instance()->activeToolManager()
1084 ->selectedTool()->getInk(0)->isSelection())
1085 return KeyContext::SelectionTool;
1086 else
1087 return KeyContext::Normal;
1088}
1089
1090bool KeyboardShortcuts::getCommandFromKeyMessage(const Message* msg, Command** command, Params* params)
1091{
1092 for (KeyPtr& key : m_keys) {
1093 if (key->type() == KeyType::Command &&
1094 key->isPressed(msg, *this)) {
1095 if (command) *command = key->command();
1096 if (params) *params = key->params();
1097 return true;
1098 }
1099 }
1100 return false;
1101}
1102
1103tools::Tool* KeyboardShortcuts::getCurrentQuicktool(tools::Tool* currentTool)
1104{
1105 if (currentTool && currentTool->getInk(0)->isSelection()) {
1106 KeyPtr key = action(KeyAction::CopySelection, KeyContext::TranslatingSelection);
1107 if (key && key->isPressed())
1108 return NULL;
1109 }
1110
1111 tools::ToolBox* toolbox = App::instance()->toolBox();
1112
1113 // Iterate over all tools
1114 for (tools::Tool* tool : *toolbox) {
1115 KeyPtr key = quicktool(tool);
1116
1117 // Collect all tools with the pressed keyboard-shortcut
1118 if (key && key->isPressed()) {
1119 return tool;
1120 }
1121 }
1122
1123 return NULL;
1124}
1125
1126KeyAction KeyboardShortcuts::getCurrentActionModifiers(KeyContext context)
1127{
1128 KeyAction flags = KeyAction::None;
1129
1130 for (const KeyPtr& key : m_keys) {
1131 if (key->type() == KeyType::Action &&
1132 key->keycontext() == context &&
1133 key->isLooselyPressed()) {
1134 flags = static_cast<KeyAction>(int(flags) | int(key->action()));
1135 }
1136 }
1137
1138 return flags;
1139}
1140
1141WheelAction KeyboardShortcuts::getWheelActionFromMouseMessage(const KeyContext context,
1142 const ui::Message* msg)
1143{
1144 WheelAction wheelAction = WheelAction::None;
1145 const ui::Accelerator* bestAccel = nullptr;
1146 for (const KeyPtr& key : m_keys) {
1147 if (key->type() == KeyType::WheelAction &&
1148 key->keycontext() == context) {
1149 const ui::Accelerator* accel = key->isPressed(msg, *this);
1150 if ((accel) &&
1151 (!bestAccel || bestAccel->modifiers() < accel->modifiers())) {
1152 bestAccel = accel;
1153 wheelAction = key->wheelAction();
1154 }
1155 }
1156 }
1157 return wheelAction;
1158}
1159
1160Keys KeyboardShortcuts::getDragActionsFromKeyMessage(const KeyContext context,
1161 const ui::Message* msg)
1162{
1163 KeyPtr bestKey = nullptr;
1164 Keys keys;
1165 for (const KeyPtr& key : m_keys) {
1166 if (key->type() == KeyType::DragAction) {
1167 const ui::Accelerator* accel = key->isPressed(msg, *this);
1168 if (accel) {
1169 keys.push_back(key);
1170 }
1171 }
1172 }
1173 return keys;
1174}
1175
1176bool KeyboardShortcuts::hasMouseWheelCustomization() const
1177{
1178 for (const KeyPtr& key : m_keys) {
1179 if (key->type() == KeyType::WheelAction &&
1180 key->hasUserDefinedAccels())
1181 return true;
1182 }
1183 return false;
1184}
1185
1186void KeyboardShortcuts::clearMouseWheelKeys()
1187{
1188 for (auto it=m_keys.begin(); it!=m_keys.end(); ) {
1189 if ((*it)->type() == KeyType::WheelAction)
1190 it = m_keys.erase(it);
1191 else
1192 ++it;
1193 }
1194}
1195
1196void KeyboardShortcuts::addMissingMouseWheelKeys()
1197{
1198 for (int action=int(WheelAction::First);
1199 action<=int(WheelAction::Last); ++action) {
1200 // Wheel actions
1201 auto it = std::find_if(
1202 m_keys.begin(), m_keys.end(),
1203 [action](const KeyPtr& key) -> bool {
1204 return
1205 key->type() == KeyType::WheelAction &&
1206 key->wheelAction() == (WheelAction)action;
1207 });
1208 if (it == m_keys.end()) {
1209 KeyPtr key = std::make_shared<Key>((WheelAction)action);
1210 m_keys.push_back(key);
1211 }
1212
1213 // Drag actions
1214 it = std::find_if(
1215 m_keys.begin(), m_keys.end(),
1216 [action](const KeyPtr& key) -> bool {
1217 return
1218 key->type() == KeyType::DragAction &&
1219 key->wheelAction() == (WheelAction)action;
1220 });
1221 if (it == m_keys.end()) {
1222 KeyPtr key = Key::MakeDragAction((WheelAction)action);
1223 m_keys.push_back(key);
1224 }
1225 }
1226}
1227
1228void KeyboardShortcuts::setDefaultMouseWheelKeys(const bool zoomWithWheel)
1229{
1230 clearMouseWheelKeys();
1231
1232 KeyPtr key;
1233 key = std::make_shared<Key>(WheelAction::Zoom);
1234 key->add(Accelerator(zoomWithWheel ? kKeyNoneModifier:
1235 kKeyCtrlModifier, kKeyNil, 0),
1236 KeySource::Original, *this);
1237 m_keys.push_back(key);
1238
1239 if (!zoomWithWheel) {
1240 key = std::make_shared<Key>(WheelAction::VScroll);
1241 key->add(Accelerator(kKeyNoneModifier, kKeyNil, 0),
1242 KeySource::Original, *this);
1243 m_keys.push_back(key);
1244 }
1245
1246 key = std::make_shared<Key>(WheelAction::HScroll);
1247 key->add(Accelerator(kKeyShiftModifier, kKeyNil, 0),
1248 KeySource::Original, *this);
1249 m_keys.push_back(key);
1250
1251 key = std::make_shared<Key>(WheelAction::FgColor);
1252 key->add(Accelerator(kKeyAltModifier, kKeyNil, 0),
1253 KeySource::Original, *this);
1254 m_keys.push_back(key);
1255
1256 key = std::make_shared<Key>(WheelAction::BgColor);
1257 key->add(Accelerator((KeyModifiers)(kKeyAltModifier | kKeyShiftModifier), kKeyNil, 0),
1258 KeySource::Original, *this);
1259 m_keys.push_back(key);
1260
1261 if (zoomWithWheel) {
1262 key = std::make_shared<Key>(WheelAction::BrushSize);
1263 key->add(Accelerator(kKeyCtrlModifier, kKeyNil, 0),
1264 KeySource::Original, *this);
1265 m_keys.push_back(key);
1266
1267 key = std::make_shared<Key>(WheelAction::Frame);
1268 key->add(Accelerator((KeyModifiers)(kKeyCtrlModifier | kKeyShiftModifier), kKeyNil, 0),
1269 KeySource::Original, *this);
1270 m_keys.push_back(key);
1271 }
1272}
1273
1274void KeyboardShortcuts::addMissingKeysForCommands()
1275{
1276 std::set<std::string> commandsAlreadyAdded;
1277 for (const KeyPtr& key : m_keys) {
1278 if (key->type() != KeyType::Command)
1279 continue;
1280
1281 if (key->params().empty())
1282 commandsAlreadyAdded.insert(key->command()->id());
1283 }
1284
1285 std::vector<std::string> ids;
1286 Commands* commands = Commands::instance();
1287 commands->getAllIds(ids);
1288
1289 for (const std::string& id : ids) {
1290 Command* command = commands->byId(id.c_str());
1291
1292 // Don't add commands that need params (they will be added to
1293 // the list using the list of keyboard shortcuts from gui.xml).
1294 if (command->needsParams())
1295 continue;
1296
1297 auto it = commandsAlreadyAdded.find(command->id());
1298 if (it != commandsAlreadyAdded.end())
1299 continue;
1300
1301 // Create the new Key element in KeyboardShortcuts for this
1302 // command without params.
1303 this->command(command->id().c_str());
1304 }
1305}
1306
1307std::string key_tooltip(const char* str, const app::Key* key)
1308{
1309 std::string res;
1310 if (str)
1311 res += str;
1312 if (key && !key->accels().empty()) {
1313 res += " (";
1314 res += key->accels().front().toString();
1315 res += ")";
1316 }
1317 return res;
1318}
1319
1320std::string convertKeyContextToUserFriendlyString(KeyContext keyContext)
1321{
1322 switch (keyContext) {
1323 case KeyContext::Any:
1324 return std::string();
1325 case KeyContext::Normal:
1326 return I18N_KEY(key_context_normal);
1327 case KeyContext::SelectionTool:
1328 return I18N_KEY(key_context_selection);
1329 case KeyContext::TranslatingSelection:
1330 return I18N_KEY(key_context_translating_selection);
1331 case KeyContext::ScalingSelection:
1332 return I18N_KEY(key_context_scaling_selection);
1333 case KeyContext::RotatingSelection:
1334 return I18N_KEY(key_context_rotating_selection);
1335 case KeyContext::MoveTool:
1336 return I18N_KEY(key_context_move_tool);
1337 case KeyContext::FreehandTool:
1338 return I18N_KEY(key_context_freehand_tool);
1339 case KeyContext::ShapeTool:
1340 return I18N_KEY(key_context_shape_tool);
1341 }
1342 return std::string();
1343}
1344
1345} // namespace app
1346