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 | |
55 | namespace app { |
56 | |
57 | using namespace skin; |
58 | using namespace tools; |
59 | using namespace ui; |
60 | |
61 | namespace { |
62 | |
63 | using = std::map<AppMenuItem*, KeyPtr>; |
64 | |
65 | class : public Splitter { |
66 | public: |
67 | () : Splitter(Splitter::ByPixel, HORIZONTAL) { |
68 | } |
69 | void () 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 | |
80 | class : public ListItem { |
81 | public: |
82 | () |
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 () const { |
105 | return m_keyLabel.bounds().x - bounds().x; |
106 | } |
107 | |
108 | int () const { |
109 | return m_contextLabel.bounds().x - bounds().x; |
110 | } |
111 | |
112 | private: |
113 | HeaderSplitter , ; |
114 | Label ; |
115 | Label ; |
116 | Label ; |
117 | }; |
118 | |
119 | class KeyItemBase : public ListItem { |
120 | public: |
121 | KeyItemBase(const std::string& text) |
122 | : ListItem(text) { |
123 | } |
124 | |
125 | protected: |
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 | |
135 | class 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 | |
150 | public: |
151 | (KeyboardShortcuts& keys, |
152 | MenuKeys& , |
153 | const std::string& text, |
154 | const KeyPtr& key, |
155 | AppMenuItem* , |
156 | const int level, |
157 | 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* () 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 | |
216 | private: |
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& ; |
493 | KeyPtr m_key; |
494 | KeyPtr m_keyOrig; |
495 | AppMenuItem* ; |
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* ; |
507 | }; |
508 | |
509 | class 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 | |
513 | public: |
514 | (app::KeyboardShortcuts& keys, |
515 | 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 | |
577 | private: |
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 (ListBox* listbox, Menu* , int level) { |
880 | for (auto child : menu->children()) { |
881 | if (AppMenuItem* = 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& ; |
970 | std::vector<ListBox*> m_listBoxes; |
971 | bool m_searchChange; |
972 | bool m_wasDefault; |
973 | HeaderItem ; |
974 | int& m_curSection; |
975 | }; |
976 | |
977 | } // anonymous namespace |
978 | |
979 | class KeyboardShortcutsCommand : public Command { |
980 | public: |
981 | KeyboardShortcutsCommand(); |
982 | |
983 | protected: |
984 | void onLoadParams(const Params& params) override; |
985 | void onExecute(Context* context) override; |
986 | |
987 | private: |
988 | void fillMenusKeys(app::KeyboardShortcuts& keys, |
989 | MenuKeys& , Menu* ); |
990 | |
991 | std::string m_search; |
992 | }; |
993 | |
994 | KeyboardShortcutsCommand::KeyboardShortcutsCommand() |
995 | : Command(CommandId::KeyboardShortcuts(), CmdUIOnlyFlag) |
996 | { |
997 | } |
998 | |
999 | void KeyboardShortcutsCommand::onLoadParams(const Params& params) |
1000 | { |
1001 | m_search = params.get("search" ); |
1002 | } |
1003 | |
1004 | void 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 ; |
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 | |
1064 | void KeyboardShortcutsCommand::fillMenusKeys(app::KeyboardShortcuts& keys, |
1065 | MenuKeys& , |
1066 | Menu* ) |
1067 | { |
1068 | for (auto child : menu->children()) { |
1069 | if (AppMenuItem* = 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 | |
1085 | Command* CommandFactory::createKeyboardShortcutsCommand() |
1086 | { |
1087 | return new KeyboardShortcutsCommand; |
1088 | } |
1089 | |
1090 | } // namespace app |
1091 | |