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/app.h"
13#include "app/app_menus.h"
14#include "app/commands/command.h"
15#include "app/context.h"
16#include "app/file_selector.h"
17#include "app/i18n/strings.h"
18#include "app/match_words.h"
19#include "app/modules/gui.h"
20#include "app/resource_finder.h"
21#include "app/tools/tool.h"
22#include "app/tools/tool_box.h"
23#include "app/ui/app_menuitem.h"
24#include "app/ui/keyboard_shortcuts.h"
25#include "app/ui/search_entry.h"
26#include "app/ui/select_accelerator.h"
27#include "app/ui/separator_in_view.h"
28#include "app/ui/skin/skin_theme.h"
29#include "base/fs.h"
30#include "base/pi.h"
31#include "base/scoped_value.h"
32#include "base/split_string.h"
33#include "base/string.h"
34#include "fmt/format.h"
35#include "ui/alert.h"
36#include "ui/fit_bounds.h"
37#include "ui/graphics.h"
38#include "ui/listitem.h"
39#include "ui/message.h"
40#include "ui/paint_event.h"
41#include "ui/resize_event.h"
42#include "ui/separator.h"
43#include "ui/size_hint_event.h"
44#include "ui/splitter.h"
45#include "ui/system.h"
46
47#include "keyboard_shortcuts.xml.h"
48
49#include <algorithm>
50#include <map>
51#include <memory>
52
53#define KEYBOARD_FILENAME_EXTENSION "aseprite-keys"
54
55namespace app {
56
57using namespace skin;
58using namespace tools;
59using namespace ui;
60
61namespace {
62
63using MenuKeys = std::map<AppMenuItem*, KeyPtr>;
64
65class HeaderSplitter : public Splitter {
66public:
67 HeaderSplitter() : Splitter(Splitter::ByPixel, HORIZONTAL) {
68 }
69 void onPositionChange() override {
70 Splitter::onPositionChange();
71
72 Widget* p = parent();
73 while (p && p->type() != kViewWidget)
74 p = p->parent();
75 if (p)
76 p->layout();
77 }
78};
79
80class HeaderItem : public ListItem {
81public:
82 HeaderItem()
83 : m_actionLabel(Strings::keyboard_shortcuts_header_action())
84 , m_keyLabel(Strings::keyboard_shortcuts_header_key())
85 , m_contextLabel(Strings::keyboard_shortcuts_header_context()) {
86 setBorder(gfx::Border(0));
87
88 auto theme = SkinTheme::get(this);
89 m_actionLabel.setStyle(theme->styles.listHeaderLabel());
90 m_keyLabel.setStyle(theme->styles.listHeaderLabel());
91 m_contextLabel.setStyle(theme->styles.listHeaderLabel());
92
93 gfx::Size displaySize = display()->size();
94 m_splitter1.setPosition(displaySize.w*3/4 * 4/10);
95 m_splitter2.setPosition(displaySize.w*3/4 * 2/10);
96
97 addChild(&m_splitter1);
98 m_splitter1.addChild(&m_actionLabel);
99 m_splitter1.addChild(&m_splitter2);
100 m_splitter2.addChild(&m_keyLabel);
101 m_splitter2.addChild(&m_contextLabel);
102 }
103
104 int keyXPos() const {
105 return m_keyLabel.bounds().x - bounds().x;
106 }
107
108 int contextXPos() const {
109 return m_contextLabel.bounds().x - bounds().x;
110 }
111
112private:
113 HeaderSplitter m_splitter1, m_splitter2;
114 Label m_actionLabel;
115 Label m_keyLabel;
116 Label m_contextLabel;
117};
118
119class KeyItemBase : public ListItem {
120public:
121 KeyItemBase(const std::string& text)
122 : ListItem(text) {
123 }
124
125protected:
126 void onSizeHint(SizeHintEvent& ev) override {
127 gfx::Size size = textSize();
128 size.w = size.w + border().width();
129 size.h = size.h + border().height() + 6*guiscale();
130 ev.setSizeHint(size);
131 }
132
133};
134
135class KeyItem : public KeyItemBase {
136
137 // Used to avoid deleting the Add/Change/Del buttons on
138 // kMouseLeaveMessage when a foreground window is popup on a signal
139 // generated by those same buttons.
140 struct LockButtons {
141 KeyItem* keyItem;
142 LockButtons(KeyItem* keyItem) : keyItem(keyItem) {
143 keyItem->m_lockButtons = true;
144 };
145 ~LockButtons() {
146 keyItem->m_lockButtons = false;
147 };
148 };
149
150public:
151 KeyItem(KeyboardShortcuts& keys,
152 MenuKeys& menuKeys,
153 const std::string& text,
154 const KeyPtr& key,
155 AppMenuItem* menuitem,
156 const int level,
157 HeaderItem* headerItem)
158 : KeyItemBase(text)
159 , m_keys(keys)
160 , m_menuKeys(menuKeys)
161 , m_key(key)
162 , m_keyOrig(key ? new Key(*key): nullptr)
163 , m_menuitem(menuitem)
164 , m_level(level)
165 , m_hotAccel(-1)
166 , m_lockButtons(false)
167 , m_headerItem(headerItem) {
168 gfx::Border border = this->border();
169 border.top(0);
170 border.bottom(0);
171 setBorder(border);
172 }
173
174 KeyPtr key() { return m_key; }
175 AppMenuItem* menuitem() const { return m_menuitem; }
176
177 std::string searchableText() const {
178 if (m_menuitem) {
179 Widget* w = m_menuitem;
180
181 // If the menu has a submenu, this item cannot be triggered with a key
182 // TODO make this possible: we should be able to open a menu with a key
183 if (w->type() == kMenuItemWidget &&
184 static_cast<MenuItem*>(w)->getSubmenu())
185 return std::string();
186
187 std::string result;
188 while (w && w->type() == kMenuItemWidget) {
189 if (!result.empty())
190 result.insert(0, " > ");
191 result.insert(0, w->text());
192
193 w = w->parent();
194 if (w && w->type() == kMenuWidget) {
195 auto owner = static_cast<Menu*>(w)->getOwnerMenuItem();
196
197 // Add the text of the menu (useful for the Palette Menu)
198 if (!owner && !w->text().empty()) {
199 result.insert(0, " > ");
200 result.insert(0, w->text());
201 }
202
203 w = owner;
204 }
205 else {
206 w = nullptr;
207 }
208 }
209 return result;
210 }
211 else {
212 return text();
213 }
214 }
215
216private:
217
218 void onChangeAccel(int index) {
219 LockButtons lock(this);
220 Accelerator origAccel = m_key->accels()[index];
221 SelectAccelerator window(origAccel,
222 m_key->keycontext(),
223 m_keys);
224 window.openWindowInForeground();
225
226 if (window.isModified()) {
227 m_key->disableAccel(origAccel, KeySource::UserDefined);
228 if (!window.accel().isEmpty())
229 m_key->add(window.accel(), KeySource::UserDefined, m_keys);
230 }
231
232 this->window()->layout();
233 }
234
235 void onDeleteAccel(int index) {
236 LockButtons lock(this);
237 // We need to create a copy of the accelerator because
238 // Key::disableAccel() will modify the accels() collection itself.
239 ui::Accelerator accel = m_key->accels()[index];
240
241 if (ui::Alert::show(
242 fmt::format(
243 Strings::alerts_delete_shortcut(),
244 accel.toString())) != 1)
245 return;
246
247 m_key->disableAccel(accel, KeySource::UserDefined);
248 window()->layout();
249 }
250
251 void onAddAccel() {
252 LockButtons lock(this);
253 ui::Accelerator accel;
254 SelectAccelerator window(accel,
255 m_key ? m_key->keycontext(): KeyContext::Any,
256 m_keys);
257 window.openWindowInForeground();
258
259 if ((window.isModified()) ||
260 // We can assign a "None" accelerator to mouse wheel actions
261 (m_key && m_key->type() == KeyType::WheelAction && window.isOK())) {
262 if (!m_key) {
263 ASSERT(m_menuitem);
264 if (!m_menuitem)
265 return;
266
267 ASSERT(m_menuitem->getCommand());
268
269 m_key = m_keys.command(
270 m_menuitem->getCommandId().c_str(),
271 m_menuitem->getParams());
272
273 m_menuKeys[m_menuitem] = m_key;
274 }
275
276 m_key->add(window.accel(), KeySource::UserDefined, m_keys);
277 }
278
279 this->window()->layout();
280 }
281
282 void onSizeHint(SizeHintEvent& ev) override {
283 KeyItemBase::onSizeHint(ev);
284 gfx::Size size = ev.sizeHint();
285
286 if (m_key && m_key->keycontext() != KeyContext::Any) {
287 int w =
288 m_headerItem->contextXPos() +
289 Graphics::measureUITextLength(
290 convertKeyContextToUserFriendlyString(m_key->keycontext()), font());
291 size.w = std::max(size.w, w);
292 }
293
294 if (m_key && !m_key->accels().empty()) {
295 size_t combos = m_key->accels().size();
296 if (combos > 1)
297 size.h *= combos;
298 }
299
300 ev.setSizeHint(size);
301 }
302
303 void onPaint(PaintEvent& ev) override {
304 Graphics* g = ev.graphics();
305 auto theme = SkinTheme::get(this);
306 gfx::Rect bounds = clientBounds();
307 gfx::Color fg, bg;
308
309 if (isSelected()) {
310 fg = theme->colors.listitemSelectedText();
311 bg = theme->colors.listitemSelectedFace();
312 }
313 else {
314 fg = theme->colors.listitemNormalText();
315 bg = theme->colors.listitemNormalFace();
316 }
317
318 g->fillRect(bg, bounds);
319
320 int y = bounds.y + 2*guiscale();
321 const int th = textSize().h;
322 // Position of the second and third columns
323 const int keyXPos = bounds.x + m_headerItem->keyXPos();
324 const int contextXPos = bounds.x + m_headerItem->contextXPos();
325
326 bounds.shrink(border());
327 {
328 int x = bounds.x + m_level*16 * guiscale();
329 IntersectClip clip(g, gfx::Rect(x, y, keyXPos - x, th));
330 if (clip) {
331 g->drawUIText(text(), fg, bg, gfx::Point(x, y), 0);
332 }
333 }
334
335 if (m_key && !m_key->accels().empty()) {
336 if (m_key->keycontext() != KeyContext::Any) {
337 g->drawText(
338 convertKeyContextToUserFriendlyString(m_key->keycontext()), fg, bg,
339 gfx::Point(contextXPos, y));
340 }
341
342 const int dh = th + 4*guiscale();
343 IntersectClip clip(g, gfx::Rect(keyXPos, y,
344 contextXPos - keyXPos,
345 dh * m_key->accels().size()));
346 if (clip) {
347 int i = 0;
348 for (const Accelerator& accel : m_key->accels()) {
349 if (i != m_hotAccel || !m_changeButton) {
350 g->drawText(
351 getAccelText(accel), fg, bg,
352 gfx::Point(keyXPos, y));
353 }
354 y += dh;
355 ++i;
356 }
357 }
358 }
359 }
360
361 void onResize(ResizeEvent& ev) override {
362 KeyItemBase::onResize(ev);
363 destroyButtons();
364 }
365
366 bool onProcessMessage(Message* msg) override {
367 switch (msg->type()) {
368
369 case kMouseLeaveMessage: {
370 destroyButtons();
371 invalidate();
372 break;
373 }
374
375 case kMouseMoveMessage: {
376 if (!isEnabled())
377 break;
378
379 gfx::Rect bounds = this->bounds();
380 MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
381
382 const Accelerators* accels = (m_key ? &m_key->accels() : NULL);
383 int y = bounds.y;
384 int dh = textSize().h + 4*guiscale();
385 int maxi = (accels && accels->size() > 1 ? accels->size(): 1);
386
387 auto theme = SkinTheme::get(this);
388
389 for (int i=0; i<maxi; ++i, y += dh) {
390 int w = Graphics::measureUITextLength(
391 (accels && i < (int)accels->size() ? getAccelText((*accels)[i]).c_str(): ""),
392 font());
393 gfx::Rect itemBounds(bounds.x + m_headerItem->keyXPos(), y, w, dh);
394 itemBounds = itemBounds.enlarge(
395 gfx::Border(
396 4*guiscale(), 0,
397 6*guiscale(), 1*guiscale()));
398
399 if (accels &&
400 i < (int)accels->size() &&
401 mouseMsg->position().y >= itemBounds.y &&
402 mouseMsg->position().y < itemBounds.y+itemBounds.h) {
403 if (m_hotAccel != i) {
404 m_hotAccel = i;
405
406 m_changeConn = obs::connection();
407 m_changeButton.reset(new Button(""));
408 m_changeConn = m_changeButton->Click.connect([this, i]{ onChangeAccel(i); });
409 m_changeButton->setStyle(theme->styles.miniButton());
410 addChild(m_changeButton.get());
411
412 m_deleteConn = obs::connection();
413 m_deleteButton.reset(new Button(""));
414 m_deleteConn = m_deleteButton->Click.connect([this, i]{ onDeleteAccel(i); });
415 m_deleteButton->setStyle(theme->styles.miniButton());
416 addChild(m_deleteButton.get());
417
418 m_changeButton->setBgColor(gfx::ColorNone);
419 m_changeButton->setBounds(itemBounds);
420 m_changeButton->setText(getAccelText((*accels)[i]));
421
422 const char* label = "x";
423 m_deleteButton->setBgColor(gfx::ColorNone);
424 m_deleteButton->setBounds(
425 gfx::Rect(
426 itemBounds.x + itemBounds.w + 2*guiscale(),
427 itemBounds.y,
428 Graphics::measureUITextLength(
429 label, font()) + 4*guiscale(),
430 itemBounds.h));
431 m_deleteButton->setText(label);
432
433 invalidate();
434 }
435 }
436
437 if (i == 0 && !m_addButton &&
438 (!m_menuitem || m_menuitem->getCommand())) {
439 m_addConn = obs::connection();
440 m_addButton.reset(new Button(""));
441 m_addConn = m_addButton->Click.connect([this]{ onAddAccel(); });
442 m_addButton->setStyle(theme->styles.miniButton());
443 addChild(m_addButton.get());
444
445 itemBounds.w = 8*guiscale() + Graphics::measureUITextLength("Add", font());
446 itemBounds.x -= itemBounds.w + 2*guiscale();
447
448 m_addButton->setBgColor(gfx::ColorNone);
449 m_addButton->setBounds(itemBounds);
450 m_addButton->setText(Strings::keyboard_shortcuts_add());
451
452 invalidate();
453 }
454 }
455 break;
456 }
457 }
458 return KeyItemBase::onProcessMessage(msg);
459 }
460
461 void destroyButtons() {
462 m_changeConn = obs::connection();
463 m_deleteConn = obs::connection();
464 m_addConn = obs::connection();
465
466 if (!m_lockButtons) {
467 m_changeButton.reset();
468 m_deleteButton.reset();
469 m_addButton.reset();
470 }
471 // Just hide the buttons
472 else {
473 if (m_changeButton) m_changeButton->setVisible(false);
474 if (m_deleteButton) m_deleteButton->setVisible(false);
475 if (m_addButton) m_addButton->setVisible(false);
476 }
477
478 m_hotAccel = -1;
479 }
480
481 std::string getAccelText(const Accelerator& accel) const {
482 if (m_key && m_key->type() == KeyType::WheelAction &&
483 accel.isEmpty()) {
484 return Strings::keyboard_shortcuts_default_action();
485 }
486 else {
487 return accel.toString();
488 }
489 }
490
491 KeyboardShortcuts& m_keys;
492 MenuKeys& m_menuKeys;
493 KeyPtr m_key;
494 KeyPtr m_keyOrig;
495 AppMenuItem* m_menuitem;
496 int m_level;
497 ui::Accelerators m_newAccels;
498 std::shared_ptr<ui::Button> m_changeButton;
499 std::shared_ptr<ui::Button> m_deleteButton;
500 std::shared_ptr<ui::Button> m_addButton;
501 obs::scoped_connection m_changeConn;
502 obs::scoped_connection m_deleteConn;
503 obs::scoped_connection m_addConn;
504 int m_hotAccel;
505 bool m_lockButtons;
506 HeaderItem* m_headerItem;
507};
508
509class KeyboardShortcutsWindow : public app::gen::KeyboardShortcuts {
510 // TODO Merge with CanvasSizeWindow::Dir
511 enum class Dir { NW, N, NE, W, C, E, SW, S, SE };
512
513public:
514 KeyboardShortcutsWindow(app::KeyboardShortcuts& keys,
515 MenuKeys& menuKeys,
516 const std::string& searchText,
517 int& curSection)
518 : m_keys(keys)
519 , m_menuKeys(menuKeys)
520 , m_searchChange(false)
521 , m_wasDefault(false)
522 , m_curSection(curSection) {
523 setAutoRemap(false);
524
525 m_listBoxes.push_back(menus());
526 m_listBoxes.push_back(commands());
527 m_listBoxes.push_back(tools());
528 m_listBoxes.push_back(actions());
529 m_listBoxes.push_back(wheelActions());
530 m_listBoxes.push_back(dragActions());
531
532#ifdef __APPLE__ // Zoom sliding two fingers option only on macOS
533 slideZoom()->setVisible(true);
534#else
535 slideZoom()->setVisible(false);
536#endif
537
538 wheelBehavior()->setSelectedItem(
539 m_keys.hasMouseWheelCustomization() ? 1: 0);
540 if (isDefaultWheelBehavior()) {
541 m_keys.setDefaultMouseWheelKeys(wheelZoom()->isSelected());
542 m_wasDefault = true;
543 }
544 m_keys.addMissingMouseWheelKeys();
545 updateSlideZoomText();
546
547 onWheelBehaviorChange();
548
549 wheelBehavior()->ItemChange.connect([this]{ onWheelBehaviorChange(); });
550 wheelZoom()->Click.connect([this]{ onWheelZoomChange(); });
551
552 search()->Change.connect([this]{ onSearchChange(); });
553 section()->Change.connect([this]{ onSectionChange(); });
554 dragActions()->Change.connect([this]{ onDragActionsChange(); });
555 dragAngle()->ItemChange.connect([this]{ onDragVectorChange(); });
556 dragDistance()->Change.connect([this]{ onDragVectorChange(); });
557 importButton()->Click.connect([this]{ onImport(); });
558 exportButton()->Click.connect([this]{ onExport(); });
559 resetButton()->Click.connect([this]{ onReset(); });
560
561 fillAllLists();
562
563 if (!searchText.empty()) {
564 search()->setText(searchText);
565 onSearchChange();
566 }
567 }
568
569 ~KeyboardShortcutsWindow() {
570 deleteAllKeyItems();
571 }
572
573 bool isDefaultWheelBehavior() {
574 return (wheelBehavior()->selectedItem() == 0);
575 }
576
577private:
578
579 void deleteAllKeyItems() {
580 deleteList(searchList());
581 deleteList(menus());
582 deleteList(commands());
583 deleteList(tools());
584 deleteList(actions());
585 deleteList(wheelActions());
586 deleteList(dragActions());
587 }
588
589 void fillAllLists() {
590 deleteAllKeyItems();
591
592 // Fill each list box with the keyboard shortcuts...
593
594 fillMenusList(menus(), AppMenus::instance()->getRootMenu(), 0);
595
596 {
597 // Create a pseudo-item for the palette menu
598 KeyItemBase* listItem = new KeyItemBase(
599 Strings::palette_popup_menu_title());
600 menus()->addChild(listItem);
601 fillMenusList(menus(), AppMenus::instance()->getPalettePopupMenu(), 1);
602 }
603
604 fillToolsList(tools(), App::instance()->toolBox());
605 fillWheelActionsList();
606 fillDragActionsList();
607
608 for (const KeyPtr& key : m_keys) {
609 if (key->type() == KeyType::Tool ||
610 key->type() == KeyType::Quicktool ||
611 key->type() == KeyType::WheelAction ||
612 key->type() == KeyType::DragAction) {
613 continue;
614 }
615
616 std::string text = key->triggerString();
617 switch (key->keycontext()) {
618 case KeyContext::SelectionTool:
619 case KeyContext::TranslatingSelection:
620 case KeyContext::ScalingSelection:
621 case KeyContext::RotatingSelection:
622 case KeyContext::MoveTool:
623 case KeyContext::FreehandTool:
624 case KeyContext::ShapeTool:
625 text =
626 convertKeyContextToUserFriendlyString(key->keycontext())
627 + ": " + text;
628 break;
629 }
630 KeyItem* keyItem = new KeyItem(m_keys, m_menuKeys, text, key,
631 nullptr, 0, &m_headerItem);
632
633 ListBox* listBox = nullptr;
634 switch (key->type()) {
635 case KeyType::Command:
636 listBox = this->commands();
637 break;
638 case KeyType::Action:
639 listBox = this->actions();
640 break;
641 }
642
643 ASSERT(listBox);
644 if (listBox)
645 listBox->addChild(keyItem);
646 }
647
648 commands()->sortItems();
649 tools()->sortItems();
650 actions()->sortItems();
651
652 section()->selectIndex(m_curSection);
653 updateViews();
654 }
655
656 void deleteList(Widget* listbox) {
657 if (m_headerItem.parent() == listbox)
658 listbox->removeChild(&m_headerItem);
659
660 while (auto item = listbox->lastChild()) {
661 listbox->removeChild(item);
662 delete item;
663 }
664 }
665
666 void fillSearchList(const std::string& search) {
667 deleteList(searchList());
668
669 MatchWords match(search);
670
671 int sectionIdx = 0; // index 0 is menus
672 for (auto listBox : m_listBoxes) {
673 Separator* group = nullptr;
674
675 for (auto item : listBox->children()) {
676 if (KeyItem* keyItem = dynamic_cast<KeyItem*>(item)) {
677 std::string itemText = keyItem->searchableText();
678 if (!match(itemText))
679 continue;
680
681 if (!group) {
682 group = new SeparatorInView(
683 section()->children()[sectionIdx]->text(), HORIZONTAL);
684 searchList()->addChild(group);
685 }
686
687 KeyItem* copyItem =
688 new KeyItem(m_keys, m_menuKeys, itemText, keyItem->key(),
689 keyItem->menuitem(), 0, &m_headerItem);
690
691 if (!item->isEnabled())
692 copyItem->setEnabled(false);
693
694 searchList()->addChild(copyItem);
695 }
696 }
697
698 ++sectionIdx;
699 }
700 }
701
702 void onWheelBehaviorChange() {
703 const bool isDefault = isDefaultWheelBehavior();
704 wheelActions()->setEnabled(!isDefault);
705 wheelZoom()->setVisible(isDefault);
706
707 if (isDefault) {
708 m_keys.setDefaultMouseWheelKeys(wheelZoom()->isSelected());
709 m_wasDefault = true;
710 }
711 else if (m_wasDefault) {
712 m_wasDefault = false;
713 for (KeyPtr& key : m_keys) {
714 if (key->type() == KeyType::WheelAction)
715 key->copyOriginalToUser();
716 }
717 }
718 m_keys.addMissingMouseWheelKeys();
719 updateSlideZoomText();
720
721 fillWheelActionsList();
722 updateViews();
723 }
724
725 void updateSlideZoomText() {
726 slideZoom()->setText(
727 isDefaultWheelBehavior() ?
728 Strings::options_slide_zoom():
729 Strings::keyboard_shortcuts_slide_as_wheel());
730 }
731
732 void fillWheelActionsList() {
733 deleteList(wheelActions());
734 for (const KeyPtr& key : m_keys) {
735 if (key->type() == KeyType::WheelAction) {
736 KeyItem* keyItem = new KeyItem(
737 m_keys, m_menuKeys, key->triggerString(), key,
738 nullptr, 0, &m_headerItem);
739 wheelActions()->addChild(keyItem);
740 }
741 }
742 wheelActions()->sortItems();
743 }
744
745 void fillDragActionsList() {
746 deleteList(dragActions());
747 for (const KeyPtr& key : m_keys) {
748 if (key->type() == KeyType::DragAction) {
749 KeyItem* keyItem = new KeyItem(
750 m_keys, m_menuKeys, key->triggerString(), key,
751 nullptr, 0, &m_headerItem);
752 dragActions()->addChild(keyItem);
753 }
754 }
755 dragActions()->sortItems();
756 }
757
758 void onWheelZoomChange() {
759 const bool isDefault = isDefaultWheelBehavior();
760 if (isDefault)
761 onWheelBehaviorChange();
762 }
763
764 void onSearchChange() {
765 base::ScopedValue<bool> flag(m_searchChange, true, false);
766 std::string searchText = search()->text();
767
768 if (searchText.empty())
769 section()->selectIndex(m_curSection);
770 else {
771 fillSearchList(searchText);
772 section()->selectChild(nullptr);
773 }
774
775 updateViews();
776 }
777
778 void onSectionChange() {
779 if (m_searchChange)
780 return;
781
782 search()->setText("");
783 updateViews();
784 }
785
786 void onDragActionsChange() {
787 auto key = selectedDragActionKey();
788 if (!key)
789 return;
790
791 int angle = 180 * key->dragVector().angle() / PI;
792
793 ui::Widget* oldFocus = manager()->getFocus();
794 dragAngle()->setSelectedItem((int)angleToDir(angle));
795 if (oldFocus)
796 oldFocus->requestFocus();
797
798 dragDistance()->setValue(key->dragVector().magnitude());
799 }
800
801 void onDragVectorChange() {
802 auto key = selectedDragActionKey();
803 if (!key)
804 return;
805
806 auto v = key->dragVector();
807 double a = dirToAngle((Dir)dragAngle()->selectedItem()).angle();
808 double m = dragDistance()->getValue();
809 v.x = m * std::cos(a);
810 v.y = m * std::sin(a);
811 if (std::fabs(v.x) < 0.00001) v.x = 0.0;
812 if (std::fabs(v.y) < 0.00001) v.y = 0.0;
813 key->setDragVector(v);
814 }
815
816 void updateViews() {
817 int s = section()->getSelectedIndex();
818 if (s >= 0)
819 m_curSection = s;
820
821 searchView()->setVisible(s < 0);
822 menusView()->setVisible(s == 0);
823 commandsView()->setVisible(s == 1);
824 toolsView()->setVisible(s == 2);
825 actionsView()->setVisible(s == 3);
826 wheelSection()->setVisible(s == 4);
827 dragSection()->setVisible(s == 5);
828
829 if (m_headerItem.parent())
830 m_headerItem.parent()->removeChild(&m_headerItem);
831 if (s < 0)
832 searchList()->insertChild(0, &m_headerItem);
833 else
834 m_listBoxes[s]->insertChild(0, &m_headerItem);
835
836 listsPlaceholder()->layout();
837 }
838
839 void onImport() {
840 base::paths exts = { KEYBOARD_FILENAME_EXTENSION };
841 base::paths filename;
842 if (!app::show_file_selector(
843 Strings::keyboard_shortcuts_import_keyboard_sc(), "", exts,
844 FileSelectorType::Open, filename))
845 return;
846
847 ASSERT(!filename.empty());
848
849 m_keys.importFile(filename.front(), KeySource::UserDefined);
850
851 fillAllLists();
852 }
853
854 void onExport() {
855 base::paths exts = { KEYBOARD_FILENAME_EXTENSION };
856 base::paths filename;
857
858 if (!app::show_file_selector(
859 Strings::keyboard_shortcuts_export_keyboard_sc(), "", exts,
860 FileSelectorType::Save, filename))
861 return;
862
863 ASSERT(!filename.empty());
864
865 m_keys.exportFile(filename.front());
866 }
867
868 void onReset() {
869 if (ui::Alert::show(Strings::alerts_restore_all_shortcuts()) == 1) {
870 m_keys.reset();
871 if (!isDefaultWheelBehavior()) {
872 wheelBehavior()->setSelectedItem(0);
873 onWheelBehaviorChange();
874 }
875 listsPlaceholder()->layout();
876 }
877 }
878
879 void fillMenusList(ListBox* listbox, Menu* menu, int level) {
880 for (auto child : menu->children()) {
881 if (AppMenuItem* menuItem = dynamic_cast<AppMenuItem*>(child)) {
882 if (menuItem->isRecentFileItem())
883 continue;
884
885 KeyItem* keyItem = new KeyItem(
886 m_keys, m_menuKeys,
887 menuItem->text().c_str(),
888 m_menuKeys[menuItem],
889 menuItem, level,
890 &m_headerItem);
891
892 listbox->addChild(keyItem);
893
894 if (menuItem->hasSubmenu())
895 fillMenusList(listbox, menuItem->getSubmenu(), level+1);
896 }
897 }
898 }
899
900 void fillToolsList(ListBox* listbox, ToolBox* toolbox) {
901 for (Tool* tool : *toolbox) {
902 std::string text = tool->getText();
903
904 KeyPtr key = m_keys.tool(tool);
905 KeyItem* keyItem = new KeyItem(m_keys, m_menuKeys, text, key,
906 nullptr, 0, &m_headerItem);
907 listbox->addChild(keyItem);
908
909 text += " (quick)";
910 key = m_keys.quicktool(tool);
911 keyItem = new KeyItem(m_keys, m_menuKeys, text, key,
912 nullptr, 0, &m_headerItem);
913 listbox->addChild(keyItem);
914 }
915 }
916
917 bool onProcessMessage(ui::Message* msg) override {
918 switch (msg->type()) {
919 case kOpenMessage:
920 load_window_pos(this, "KeyboardShortcuts");
921 invalidate();
922 break;
923 case kCloseMessage:
924 save_window_pos(this, "KeyboardShortcuts");
925 break;
926 }
927 return app::gen::KeyboardShortcuts::onProcessMessage(msg);
928 }
929
930 KeyPtr selectedDragActionKey() {
931 auto item = dragActions()->getSelectedChild();
932 if (KeyItem* keyItem = dynamic_cast<KeyItem*>(item)) {
933 KeyPtr key = keyItem->key();
934 if (key && key->type() == KeyType::DragAction)
935 return key;
936 }
937 return nullptr;
938 }
939
940 Dir angleToDir(int angle) {
941 if (angle >= -1*45/2 && angle < 1*45/2) return Dir::E;
942 if (angle >= 1*45/2 && angle < 3*45/2) return Dir::NE;
943 if (angle >= 3*45/2 && angle < 5*45/2) return Dir::N;
944 if (angle >= 5*45/2 && angle < 7*45/2) return Dir::NW;
945 if ((angle >= 7*45/2 && angle <= 180) ||
946 (angle >= -180 && angle <= -7*45/2)) return Dir::W;
947 if (angle > -7*45/2 && angle <= -5*45/2) return Dir::SW;
948 if (angle > -5*45/2 && angle <= -3*45/2) return Dir::S;
949 if (angle > -3*45/2 && angle <= -1*45/2) return Dir::SE;
950 return Dir::C;
951 }
952
953 DragVector dirToAngle(Dir dir) {
954 switch (dir) {
955 case Dir::NW: return DragVector(-1, +1);
956 case Dir::N: return DragVector( 0, +1);
957 case Dir::NE: return DragVector(+1, +1);
958 case Dir::W: return DragVector(-1, 0);
959 case Dir::C: return DragVector( 0, 0);
960 case Dir::E: return DragVector(+1, 0);
961 case Dir::SW: return DragVector(-1, -1);
962 case Dir::S: return DragVector( 0, -1);
963 case Dir::SE: return DragVector(+1, -1);
964 }
965 return DragVector();
966 }
967
968 app::KeyboardShortcuts& m_keys;
969 MenuKeys& m_menuKeys;
970 std::vector<ListBox*> m_listBoxes;
971 bool m_searchChange;
972 bool m_wasDefault;
973 HeaderItem m_headerItem;
974 int& m_curSection;
975};
976
977} // anonymous namespace
978
979class KeyboardShortcutsCommand : public Command {
980public:
981 KeyboardShortcutsCommand();
982
983protected:
984 void onLoadParams(const Params& params) override;
985 void onExecute(Context* context) override;
986
987private:
988 void fillMenusKeys(app::KeyboardShortcuts& keys,
989 MenuKeys& menuKeys, Menu* menu);
990
991 std::string m_search;
992};
993
994KeyboardShortcutsCommand::KeyboardShortcutsCommand()
995 : Command(CommandId::KeyboardShortcuts(), CmdUIOnlyFlag)
996{
997}
998
999void KeyboardShortcutsCommand::onLoadParams(const Params& params)
1000{
1001 m_search = params.get("search");
1002}
1003
1004void KeyboardShortcutsCommand::onExecute(Context* context)
1005{
1006 static int curSection = 0;
1007
1008 app::KeyboardShortcuts* globalKeys = app::KeyboardShortcuts::instance();
1009 app::KeyboardShortcuts keys;
1010 keys.setKeys(*globalKeys, true);
1011 keys.addMissingKeysForCommands();
1012
1013 MenuKeys menuKeys;
1014 fillMenusKeys(keys, menuKeys, AppMenus::instance()->getRootMenu());
1015 fillMenusKeys(keys, menuKeys, AppMenus::instance()->getPalettePopupMenu());
1016
1017 // Here we copy the m_search field because
1018 // KeyboardShortcutsWindow::fillAllLists() modifies this same
1019 // KeyboardShortcutsCommand instance (so m_search will be "")
1020 // TODO Seeing this, we need a complete new way to handle UI commands execution
1021 std::string neededSearchCopy = m_search;
1022 KeyboardShortcutsWindow window(keys, menuKeys, neededSearchCopy, curSection);
1023
1024 ui::Display* mainDisplay = Manager::getDefault()->display();
1025 ui::fit_bounds(mainDisplay, &window,
1026 gfx::Rect(mainDisplay->size()),
1027 [](const gfx::Rect& workarea,
1028 gfx::Rect& bounds,
1029 std::function<gfx::Rect(Widget*)> getWidgetBounds) {
1030 gfx::Point center = bounds.center();
1031 bounds.setSize(workarea.size()*3/4);
1032 bounds.setOrigin(center - gfx::Point(bounds.size()/2));
1033 });
1034
1035 window.loadLayout();
1036
1037 window.setVisible(true);
1038 window.openWindowInForeground();
1039
1040 if (window.closer() == window.ok()) {
1041 globalKeys->setKeys(keys, false);
1042 for (const auto& p : menuKeys)
1043 p.first->setKey(p.second);
1044
1045 // Save preferences in widgets that are bound to options automatically
1046 {
1047 Message msg(kSavePreferencesMessage);
1048 msg.setPropagateToChildren(true);
1049 window.sendMessage(&msg);
1050 }
1051
1052 // Save keyboard shortcuts in configuration file
1053 {
1054 ResourceFinder rf;
1055 rf.includeUserDir("user." KEYBOARD_FILENAME_EXTENSION);
1056 std::string fn = rf.getFirstOrCreateDefault();
1057 globalKeys->exportFile(fn);
1058 }
1059 }
1060
1061 AppMenus::instance()->syncNativeMenuItemKeyShortcuts();
1062}
1063
1064void KeyboardShortcutsCommand::fillMenusKeys(app::KeyboardShortcuts& keys,
1065 MenuKeys& menuKeys,
1066 Menu* menu)
1067{
1068 for (auto child : menu->children()) {
1069 if (AppMenuItem* menuItem = dynamic_cast<AppMenuItem*>(child)) {
1070 if (menuItem->isRecentFileItem())
1071 continue;
1072
1073 if (menuItem->getCommand()) {
1074 menuKeys[menuItem] =
1075 keys.command(menuItem->getCommandId().c_str(),
1076 menuItem->getParams());
1077 }
1078
1079 if (menuItem->hasSubmenu())
1080 fillMenusKeys(keys, menuKeys, menuItem->getSubmenu());
1081 }
1082 }
1083}
1084
1085Command* CommandFactory::createKeyboardShortcutsCommand()
1086{
1087 return new KeyboardShortcutsCommand;
1088}
1089
1090} // namespace app
1091