1 // Aseprite
2 // Copyright (C) 2001-2018 David Capello
3 //
4 // This program is distributed under the terms of
5 // the End-User License Agreement for Aseprite.
6
7 #ifdef HAVE_CONFIG_H
8 #include "config.h"
9 #endif
10
11 #include "app/app_menus.h"
12
13 #include "app/app.h"
14 #include "app/commands/command.h"
15 #include "app/commands/commands.h"
16 #include "app/commands/params.h"
17 #include "app/console.h"
18 #include "app/gui_xml.h"
19 #include "app/i18n/strings.h"
20 #include "app/recent_files.h"
21 #include "app/resource_finder.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/main_window.h"
26 #include "app/ui_context.h"
27 #include "app/util/filetoks.h"
28 #include "base/bind.h"
29 #include "base/fs.h"
30 #include "base/string.h"
31 #include "she/menus.h"
32 #include "she/system.h"
33 #include "ui/ui.h"
34
35 #include "tinyxml.h"
36
37 #include <cctype>
38 #include <cstdio>
39 #include <cstring>
40
41 namespace app {
42
43 using namespace ui;
44
45 namespace {
46
47 // TODO Move this to she layer
48 const int kUnicodeEsc = 27;
49 const int kUnicodeEnter = '\r'; // 10
50 const int kUnicodeInsert = 0xF727; // NSInsertFunctionKey
51 const int kUnicodeDel = 0xF728; // NSDeleteFunctionKey
52 const int kUnicodeHome = 0xF729; // NSHomeFunctionKey
53 const int kUnicodeEnd = 0xF72B; // NSEndFunctionKey
54 const int kUnicodePageUp = 0xF72C; // NSPageUpFunctionKey
55 const int kUnicodePageDown = 0xF72D; // NSPageDownFunctionKey
56 const int kUnicodeLeft = 0xF702; // NSLeftArrowFunctionKey
57 const int kUnicodeRight = 0xF703; // NSRightArrowFunctionKey
58 const int kUnicodeUp = 0xF700; // NSUpArrowFunctionKey
59 const int kUnicodeDown = 0xF701; // NSDownArrowFunctionKey
60
destroy_instance(AppMenus * instance)61 void destroy_instance(AppMenus* instance)
62 {
63 delete instance;
64 }
65
is_text_entry_shortcut(const she::Shortcut & shortcut)66 bool is_text_entry_shortcut(const she::Shortcut& shortcut)
67 {
68 const she::KeyModifiers mod = shortcut.modifiers();
69 const int chr = shortcut.unicode();
70 const int lchr = std::tolower(chr);
71
72 bool result =
73 ((mod == she::KeyModifiers::kKeyNoneModifier ||
74 mod == she::KeyModifiers::kKeyShiftModifier) &&
75 chr >= 32 && chr < 0xF000)
76 ||
77 ((mod == she::KeyModifiers::kKeyCmdModifier ||
78 mod == she::KeyModifiers::kKeyCtrlModifier) &&
79 (lchr == 'a' || lchr == 'c' || lchr == 'v' || lchr == 'x'))
80 ||
81 (chr == kUnicodeInsert ||
82 chr == kUnicodeDel ||
83 chr == kUnicodeHome ||
84 chr == kUnicodeEnd ||
85 chr == kUnicodeLeft ||
86 chr == kUnicodeRight ||
87 chr == kUnicodeEsc ||
88 chr == kUnicodeEnter);
89
90 return result;
91 }
92
can_call_global_shortcut(const AppMenuItem::Native * native)93 bool can_call_global_shortcut(const AppMenuItem::Native* native)
94 {
95 ASSERT(native);
96
97 ui::Manager* manager = ui::Manager::getDefault();
98 ASSERT(manager);
99 ui::Widget* focus = manager->getFocus();
100 return
101 // The mouse is not capture
102 (manager->getCapture() == nullptr) &&
103 // The foreground window must be the main window to avoid calling
104 // a global command inside a modal dialog.
105 (manager->getForegroundWindow() == App::instance()->mainWindow()) &&
106 // If we are in a menubox window (e.g. we've pressed
107 // Alt+mnemonic), we should disable the native shortcuts
108 // temporarily so we can use mnemonics without modifiers
109 // (e.g. Alt+S opens the Sprite menu, then 'S' key should execute
110 // "Sprite Size" command in that menu, instead of Stroke command
111 // which is in 'Edit > Stroke'). This is necessary in macOS, when
112 // the native menu + Aseprite pixel-art menus are enabled.
113 (dynamic_cast<MenuBoxWindow*>(manager->getTopWindow()) == nullptr) &&
114 // The focused widget cannot be an entry, because entry fields
115 // prefer text input, so we cannot call shortcuts without
116 // modifiers (e.g. F or T keystrokes) to trigger a global command
117 // in a text field.
118 (focus == nullptr ||
119 focus->type() != ui::kEntryWidget ||
120 !is_text_entry_shortcut(native->shortcut)) &&
121 (native->keyContext == KeyContext::Any ||
122 native->keyContext == KeyboardShortcuts::instance()->getCurrentKeyContext());
123 }
124
125 // TODO this should be on "she" library (or we should use
126 // she::Shortcut instead of ui::Accelerators)
from_scancode_to_unicode(KeyScancode scancode)127 int from_scancode_to_unicode(KeyScancode scancode)
128 {
129 static int map[] = {
130 0, // kKeyNil
131 'a', // kKeyA
132 'b', // kKeyB
133 'c', // kKeyC
134 'd', // kKeyD
135 'e', // kKeyE
136 'f', // kKeyF
137 'g', // kKeyG
138 'h', // kKeyH
139 'i', // kKeyI
140 'j', // kKeyJ
141 'k', // kKeyK
142 'l', // kKeyL
143 'm', // kKeyM
144 'n', // kKeyN
145 'o', // kKeyO
146 'p', // kKeyP
147 'q', // kKeyQ
148 'r', // kKeyR
149 's', // kKeyS
150 't', // kKeyT
151 'u', // kKeyU
152 'v', // kKeyV
153 'w', // kKeyW
154 'x', // kKeyX
155 'y', // kKeyY
156 'z', // kKeyZ
157 '0', // kKey0
158 '1', // kKey1
159 '2', // kKey2
160 '3', // kKey3
161 '4', // kKey4
162 '5', // kKey5
163 '6', // kKey6
164 '7', // kKey7
165 '8', // kKey8
166 '9', // kKey9
167 0, // kKey0Pad
168 0, // kKey1Pad
169 0, // kKey2Pad
170 0, // kKey3Pad
171 0, // kKey4Pad
172 0, // kKey5Pad
173 0, // kKey6Pad
174 0, // kKey7Pad
175 0, // kKey8Pad
176 0, // kKey9Pad
177 0xF704, // kKeyF1 (NSF1FunctionKey)
178 0xF705, // kKeyF2
179 0xF706, // kKeyF3
180 0xF707, // kKeyF4
181 0xF708, // kKeyF5
182 0xF709, // kKeyF6
183 0xF70A, // kKeyF7
184 0xF70B, // kKeyF8
185 0xF70C, // kKeyF9
186 0xF70D, // kKeyF10
187 0xF70E, // kKeyF11
188 0xF70F, // kKeyF12
189 kUnicodeEsc, // kKeyEsc
190 '~', // kKeyTilde
191 '-', // kKeyMinus
192 '=', // kKeyEquals
193 8, // kKeyBackspace
194 9, // kKeyTab
195 '[', // kKeyOpenbrace
196 ']', // kKeyClosebrace
197 kUnicodeEnter, // kKeyEnter
198 ':', // kKeyColon
199 '\'', // kKeyQuote
200 '\\', // kKeyBackslash
201 0, // kKeyBackslash2
202 ',', // kKeyComma
203 '.', // kKeyStop
204 '/', // kKeySlash
205 ' ', // kKeySpace
206 kUnicodeInsert, // kKeyInsert (NSInsertFunctionKey)
207 kUnicodeDel, // kKeyDel (NSDeleteFunctionKey)
208 kUnicodeHome, // kKeyHome (NSHomeFunctionKey)
209 kUnicodeEnd, // kKeyEnd (NSEndFunctionKey)
210 kUnicodePageUp, // kKeyPageUp (NSPageUpFunctionKey)
211 kUnicodePageDown, // kKeyPageDown (NSPageDownFunctionKey)
212 kUnicodeLeft, // kKeyLeft (NSLeftArrowFunctionKey)
213 kUnicodeRight, // kKeyRight (NSRightArrowFunctionKey)
214 kUnicodeUp, // kKeyUp (NSUpArrowFunctionKey)
215 kUnicodeDown, // kKeyDown (NSDownArrowFunctionKey)
216 '/', // kKeySlashPad
217 '*', // kKeyAsterisk
218 0, // kKeyMinusPad
219 0, // kKeyPlusPad
220 0, // kKeyDelPad
221 0, // kKeyEnterPad
222 0, // kKeyPrtscr
223 0, // kKeyPause
224 0, // kKeyAbntC1
225 0, // kKeyYen
226 0, // kKeyKana
227 0, // kKeyConvert
228 0, // kKeyNoconvert
229 0, // kKeyAt
230 0, // kKeyCircumflex
231 0, // kKeyColon2
232 0, // kKeyKanji
233 0, // kKeyEqualsPad
234 '`', // kKeyBackquote
235 0, // kKeySemicolon
236 0, // kKeyUnknown1
237 0, // kKeyUnknown2
238 0, // kKeyUnknown3
239 0, // kKeyUnknown4
240 0, // kKeyUnknown5
241 0, // kKeyUnknown6
242 0, // kKeyUnknown7
243 0, // kKeyUnknown8
244 0, // kKeyLShift
245 0, // kKeyRShift
246 0, // kKeyLControl
247 0, // kKeyRControl
248 0, // kKeyAlt
249 0, // kKeyAltGr
250 0, // kKeyLWin
251 0, // kKeyRWin
252 0, // kKeyMenu
253 0, // kKeyCommand
254 0, // kKeyScrLock
255 0, // kKeyNumLock
256 0, // kKeyCapsLock
257 };
258 if (scancode >= 0 && scancode < sizeof(map) / sizeof(map[0]))
259 return map[scancode];
260 else
261 return 0;
262 }
263
get_native_shortcut_for_command(const char * commandId,const Params & params=Params ())264 AppMenuItem::Native get_native_shortcut_for_command(
265 const char* commandId,
266 const Params& params = Params())
267 {
268 AppMenuItem::Native native;
269 KeyPtr key = KeyboardShortcuts::instance()->command(commandId, params);
270 if (key) {
271 native.shortcut = get_os_shortcut_from_key(key.get());
272 native.keyContext = key->keycontext();
273 }
274 return native;
275 }
276
277 } // anonymous namespace
278
get_os_shortcut_from_key(const Key * key)279 she::Shortcut get_os_shortcut_from_key(const Key* key)
280 {
281 if (key && !key->accels().empty()) {
282 const ui::Accelerator& accel = key->accels().front();
283 return she::Shortcut(
284 (accel.unicodeChar() ? accel.unicodeChar():
285 from_scancode_to_unicode(accel.scancode())),
286 accel.modifiers());
287 }
288 else
289 return she::Shortcut();
290 }
291
292 // static
instance()293 AppMenus* AppMenus::instance()
294 {
295 static AppMenus* instance = NULL;
296 if (!instance) {
297 instance = new AppMenus;
298 App::instance()->Exit.connect(base::Bind<void>(&destroy_instance, instance));
299 }
300 return instance;
301 }
302
AppMenus()303 AppMenus::AppMenus()
304 : m_recentListMenuitem(nullptr)
305 , m_osMenu(nullptr)
306 {
307 m_recentFilesConn =
308 App::instance()->recentFiles()->Changed.connect(
309 base::Bind(&AppMenus::rebuildRecentList, this));
310 }
311
~AppMenus()312 AppMenus::~AppMenus()
313 {
314 if (m_osMenu)
315 m_osMenu->dispose();
316 }
317
reload()318 void AppMenus::reload()
319 {
320 XmlDocumentRef doc(GuiXml::instance()->doc());
321 TiXmlHandle handle(doc.get());
322 const char* path = GuiXml::instance()->filename();
323
324 ////////////////////////////////////////
325 // Load menus
326
327 LOG("MENU: Loading menus from %s\n", path);
328
329 m_rootMenu.reset(loadMenuById(handle, "main_menu"));
330
331 #if _DEBUG
332 // Add a warning element because the user is not using the last well-known gui.xml file.
333 if (GuiXml::instance()->version() != VERSION)
334 m_rootMenu->insertChild(0, createInvalidVersionMenuitem());
335 #endif
336
337 LOG("MENU: Main menu loaded.\n");
338
339 m_tabPopupMenu.reset(loadMenuById(handle, "tab_popup_menu"));
340 m_documentTabPopupMenu.reset(loadMenuById(handle, "document_tab_popup_menu"));
341 m_layerPopupMenu.reset(loadMenuById(handle, "layer_popup_menu"));
342 m_framePopupMenu.reset(loadMenuById(handle, "frame_popup_menu"));
343 m_celPopupMenu.reset(loadMenuById(handle, "cel_popup_menu"));
344 m_celMovementPopupMenu.reset(loadMenuById(handle, "cel_movement_popup_menu"));
345 m_frameTagPopupMenu.reset(loadMenuById(handle, "frame_tag_popup_menu"));
346 m_slicePopupMenu.reset(loadMenuById(handle, "slice_popup_menu"));
347 m_palettePopupMenu.reset(loadMenuById(handle, "palette_popup_menu"));
348 m_inkPopupMenu.reset(loadMenuById(handle, "ink_popup_menu"));
349
350 ////////////////////////////////////////
351 // Load keyboard shortcuts for commands
352
353 LOG("MENU: Loading commands keyboard shortcuts from %s\n", path);
354
355 TiXmlElement* xmlKey = handle
356 .FirstChild("gui")
357 .FirstChild("keyboard").ToElement();
358
359 KeyboardShortcuts::instance()->clear();
360 KeyboardShortcuts::instance()->importFile(xmlKey, KeySource::Original);
361
362 // Load user settings
363 {
364 ResourceFinder rf;
365 rf.includeUserDir("user.aseprite-keys");
366 std::string fn = rf.getFirstOrCreateDefault();
367 if (base::is_file(fn))
368 KeyboardShortcuts::instance()->importFile(fn, KeySource::UserDefined);
369 }
370
371 // Create native menus after the default + user defined keyboard
372 // shortcuts are loaded correctly.
373 createNativeMenus();
374 }
375
initTheme()376 void AppMenus::initTheme()
377 {
378 updateMenusList();
379 for (Menu* menu : m_menus)
380 if (menu)
381 menu->initTheme();
382 }
383
rebuildRecentList()384 void AppMenus::rebuildRecentList() // workaround for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=213773
385 {
386 AppMenuItem* list_menuitem = dynamic_cast<AppMenuItem*>(m_recentListMenuitem);
387 MenuItem* menuitem;
388
389 // Update the recent file list menu item
390 if (list_menuitem) {
391 if (list_menuitem->hasSubmenuOpened())
392 return;
393
394 Command* cmd_open_file =
395 Commands::instance()->byId(CommandId::OpenFile());
396
397 Menu* submenu = list_menuitem->getSubmenu();
398 if (submenu) {
399 list_menuitem->setSubmenu(NULL);
400 submenu->deferDelete();
401 }
402
403 // Build the menu of recent files
404 submenu = new Menu();
405 list_menuitem->setSubmenu(submenu);
406
407 auto it = App::instance()->recentFiles()->files_begin();
408 auto end = App::instance()->recentFiles()->files_end();
409 if (it != end) {
410 Params params;
411
412 for (; it != end; ++it) {
413 const char* filename = it->c_str();
414 params.set("filename", filename);
415
416 menuitem = new AppMenuItem(
417 base::get_file_name(filename).c_str(),
418 cmd_open_file,
419 params);
420 submenu->addChild(menuitem);
421 }
422 }
423 else {
424 menuitem = new AppMenuItem("Nothing", NULL, Params());
425 menuitem->setEnabled(false);
426 submenu->addChild(menuitem);
427 }
428
429 // Sync native menus
430 if (list_menuitem->native() &&
431 list_menuitem->native()->menuItem) {
432 she::Menus* menus = she::instance()->menus();
433 she::Menu* osMenu = (menus ? menus->createMenu(): nullptr);
434 if (osMenu) {
435 createNativeSubmenus(osMenu, submenu);
436 list_menuitem->native()->menuItem->setSubmenu(osMenu);
437 }
438 }
439 }
440 }
441
loadMenuById(TiXmlHandle & handle,const char * id)442 Menu* AppMenus::loadMenuById(TiXmlHandle& handle, const char* id)
443 {
444 ASSERT(id != NULL);
445
446 // <gui><menus><menu>
447 TiXmlElement* xmlMenu = handle
448 .FirstChild("gui")
449 .FirstChild("menus")
450 .FirstChild("menu").ToElement();
451 while (xmlMenu) {
452 const char* menuId = xmlMenu->Attribute("id");
453
454 if (menuId && strcmp(menuId, id) == 0) {
455 m_xmlTranslator.setStringIdPrefix(menuId);
456 return convertXmlelemToMenu(xmlMenu);
457 }
458
459 xmlMenu = xmlMenu->NextSiblingElement();
460 }
461
462 throw base::Exception("Error loading menu '%s'\nReinstall the application.", id);
463 }
464
convertXmlelemToMenu(TiXmlElement * elem)465 Menu* AppMenus::convertXmlelemToMenu(TiXmlElement* elem)
466 {
467 Menu* menu = new Menu();
468
469 TiXmlElement* child = elem->FirstChildElement();
470 while (child) {
471 Widget* menuitem = convertXmlelemToMenuitem(child);
472 if (menuitem)
473 menu->addChild(menuitem);
474 else
475 throw base::Exception("Error converting the element \"%s\" to a menu-item.\n",
476 static_cast<const char*>(child->Value()));
477
478 child = child->NextSiblingElement();
479 }
480
481 return menu;
482 }
483
convertXmlelemToMenuitem(TiXmlElement * elem)484 Widget* AppMenus::convertXmlelemToMenuitem(TiXmlElement* elem)
485 {
486 // is it a <separator>?
487 if (strcmp(elem->Value(), "separator") == 0)
488 return new MenuSeparator;
489
490 const char* command_id = elem->Attribute("command");
491 Command* command =
492 command_id ? Commands::instance()->byId(command_id):
493 nullptr;
494
495 // load params
496 Params params;
497 if (command) {
498 TiXmlElement* xmlParam = elem->FirstChildElement("param");
499 while (xmlParam) {
500 const char* param_name = xmlParam->Attribute("name");
501 const char* param_value = xmlParam->Attribute("value");
502
503 if (param_name && param_value)
504 params.set(param_name, param_value);
505
506 xmlParam = xmlParam->NextSiblingElement();
507 }
508 }
509
510 // Create the item
511 AppMenuItem* menuitem = new AppMenuItem(m_xmlTranslator(elem, "text"),
512 command, params);
513 if (!menuitem)
514 return nullptr;
515
516 menuitem->processMnemonicFromText();
517
518 // Has it a ID?
519 const char* id = elem->Attribute("id");
520 if (id) {
521 // Recent list menu
522 if (std::strcmp(id, "recent_list") == 0) {
523 m_recentListMenuitem = menuitem;
524 }
525 else if (std::strcmp(id, "help_menu") == 0) {
526 m_helpMenuitem = menuitem;
527 }
528 }
529
530 // Has it a sub-menu (<menu>)?
531 if (strcmp(elem->Value(), "menu") == 0) {
532 // Create the sub-menu
533 Menu* subMenu = convertXmlelemToMenu(elem);
534 if (!subMenu)
535 throw base::Exception("Error reading the sub-menu\n");
536
537 menuitem->setSubmenu(subMenu);
538 }
539
540 return menuitem;
541 }
542
createInvalidVersionMenuitem()543 Widget* AppMenus::createInvalidVersionMenuitem()
544 {
545 AppMenuItem* menuitem = new AppMenuItem("WARNING!");
546 Menu* subMenu = new Menu();
547 subMenu->addChild(new AppMenuItem(PACKAGE " is using a customized gui.xml (maybe from your HOME directory)."));
548 subMenu->addChild(new AppMenuItem("You should update your customized gui.xml file to the new version to get"));
549 subMenu->addChild(new AppMenuItem("the latest commands available."));
550 subMenu->addChild(new MenuSeparator);
551 subMenu->addChild(new AppMenuItem("You can bypass this validation adding the correct version"));
552 subMenu->addChild(new AppMenuItem("number in <gui version=\"" VERSION "\"> element."));
553 menuitem->setSubmenu(subMenu);
554 return menuitem;
555 }
556
applyShortcutToMenuitemsWithCommand(Command * command,const Params & params,const KeyPtr & key)557 void AppMenus::applyShortcutToMenuitemsWithCommand(Command* command,
558 const Params& params,
559 const KeyPtr& key)
560 {
561 updateMenusList();
562 for (Menu* menu : m_menus)
563 if (menu)
564 applyShortcutToMenuitemsWithCommand(menu, command, params, key);
565 }
566
applyShortcutToMenuitemsWithCommand(Menu * menu,Command * command,const Params & params,const KeyPtr & key)567 void AppMenus::applyShortcutToMenuitemsWithCommand(Menu* menu,
568 Command* command,
569 const Params& params,
570 const KeyPtr& key)
571 {
572 for (auto child : menu->children()) {
573 if (child->type() == kMenuItemWidget) {
574 AppMenuItem* menuitem = dynamic_cast<AppMenuItem*>(child);
575 if (!menuitem)
576 continue;
577
578 Command* mi_command = menuitem->getCommand();
579 const Params& mi_params = menuitem->getParams();
580
581 if ((mi_command) &&
582 (base::utf8_icmp(mi_command->id(), command->id()) == 0) &&
583 (mi_params == params)) {
584 // Set the keyboard shortcut to be shown in this menu-item
585 menuitem->setKey(key);
586 }
587
588 if (Menu* submenu = menuitem->getSubmenu())
589 applyShortcutToMenuitemsWithCommand(submenu, command, params, key);
590 }
591 }
592 }
593
syncNativeMenuItemKeyShortcuts()594 void AppMenus::syncNativeMenuItemKeyShortcuts()
595 {
596 syncNativeMenuItemKeyShortcuts(m_rootMenu.get());
597 }
598
syncNativeMenuItemKeyShortcuts(Menu * menu)599 void AppMenus::syncNativeMenuItemKeyShortcuts(Menu* menu)
600 {
601 for (auto child : menu->children()) {
602 if (child->type() == kMenuItemWidget) {
603 if (AppMenuItem* menuitem = dynamic_cast<AppMenuItem*>(child))
604 menuitem->syncNativeMenuItemKeyShortcut();
605
606 if (Menu* submenu = static_cast<MenuItem*>(child)->getSubmenu())
607 syncNativeMenuItemKeyShortcuts(submenu);
608 }
609 }
610 }
611
612 // TODO redesign the list of popup menus, it might be an
613 // autogenerated widget from 'gen'
updateMenusList()614 void AppMenus::updateMenusList()
615 {
616 m_menus.clear();
617 m_menus.push_back(m_rootMenu);
618 m_menus.push_back(m_tabPopupMenu);
619 m_menus.push_back(m_documentTabPopupMenu);
620 m_menus.push_back(m_layerPopupMenu);
621 m_menus.push_back(m_framePopupMenu);
622 m_menus.push_back(m_celPopupMenu);
623 m_menus.push_back(m_celMovementPopupMenu);
624 m_menus.push_back(m_frameTagPopupMenu);
625 m_menus.push_back(m_slicePopupMenu);
626 m_menus.push_back(m_palettePopupMenu);
627 m_menus.push_back(m_inkPopupMenu);
628 }
629
createNativeMenus()630 void AppMenus::createNativeMenus()
631 {
632 she::Menus* menus = she::instance()->menus();
633 if (!menus) // This platform doesn't support native menu items
634 return;
635
636 if (m_osMenu)
637 m_osMenu->dispose();
638 m_osMenu = menus->createMenu();
639
640 #ifdef __APPLE__ // Create default macOS app menus (App ... Window)
641 {
642 she::MenuItemInfo about("About " PACKAGE);
643 auto native = get_native_shortcut_for_command(CommandId::About());
644 about.shortcut = native.shortcut;
645 about.execute = [native]{
646 if (can_call_global_shortcut(&native)) {
647 Command* cmd = Commands::instance()->byId(CommandId::About());
648 UIContext::instance()->executeCommand(cmd);
649 }
650 };
651
652 she::MenuItemInfo preferences("Preferences...");
653 native = get_native_shortcut_for_command(CommandId::Options());
654 preferences.shortcut = native.shortcut;
655 preferences.execute = [native]{
656 if (can_call_global_shortcut(&native)) {
657 Command* cmd = Commands::instance()->byId(CommandId::Options());
658 UIContext::instance()->executeCommand(cmd);
659 }
660 };
661
662 she::MenuItemInfo hide("Hide " PACKAGE, she::MenuItemInfo::Hide);
663 hide.shortcut = she::Shortcut('h', she::kKeyCmdModifier);
664
665 she::MenuItemInfo quit("Quit " PACKAGE, she::MenuItemInfo::Quit);
666 quit.shortcut = she::Shortcut('q', she::kKeyCmdModifier);
667
668 she::Menu* appMenu = menus->createMenu();
669 appMenu->addItem(menus->createMenuItem(about));
670 appMenu->addItem(menus->createMenuItem(she::MenuItemInfo(she::MenuItemInfo::Separator)));
671 appMenu->addItem(menus->createMenuItem(preferences));
672 appMenu->addItem(menus->createMenuItem(she::MenuItemInfo(she::MenuItemInfo::Separator)));
673 appMenu->addItem(menus->createMenuItem(hide));
674 appMenu->addItem(menus->createMenuItem(she::MenuItemInfo("Hide Others", she::MenuItemInfo::HideOthers)));
675 appMenu->addItem(menus->createMenuItem(she::MenuItemInfo("Show All", she::MenuItemInfo::ShowAll)));
676 appMenu->addItem(menus->createMenuItem(she::MenuItemInfo(she::MenuItemInfo::Separator)));
677 appMenu->addItem(menus->createMenuItem(quit));
678
679 she::MenuItem* appItem = menus->createMenuItem(she::MenuItemInfo("App"));
680 appItem->setSubmenu(appMenu);
681 m_osMenu->addItem(appItem);
682 }
683 #endif
684
685 createNativeSubmenus(m_osMenu, m_rootMenu);
686
687 #ifdef __APPLE__
688 {
689 // Search the index where help menu is located (so the Window menu
690 // can take its place/index position)
691 int i = 0, helpIndex = int(m_rootMenu->children().size());
692 for (const auto child : m_rootMenu->children()) {
693 if (child == m_helpMenuitem) {
694 helpIndex = i;
695 break;
696 }
697 ++i;
698 }
699
700 she::MenuItemInfo minimize("Minimize", she::MenuItemInfo::Minimize);
701 minimize.shortcut = she::Shortcut('m', she::kKeyCmdModifier);
702
703 she::Menu* windowMenu = menus->createMenu();
704 windowMenu->addItem(menus->createMenuItem(minimize));
705 windowMenu->addItem(menus->createMenuItem(she::MenuItemInfo("Zoom", she::MenuItemInfo::Zoom)));
706
707 she::MenuItem* windowItem = menus->createMenuItem(she::MenuItemInfo("Window"));
708 windowItem->setSubmenu(windowMenu);
709
710 // We use helpIndex+1 because the first index in m_osMenu is the
711 // App menu.
712 m_osMenu->insertItem(helpIndex+1, windowItem);
713 }
714 #endif
715
716 menus->setAppMenu(m_osMenu);
717 }
718
createNativeSubmenus(she::Menu * osMenu,const ui::Menu * uiMenu)719 void AppMenus::createNativeSubmenus(she::Menu* osMenu, const ui::Menu* uiMenu)
720 {
721 she::Menus* menus = she::instance()->menus();
722
723 for (const auto& child : uiMenu->children()) {
724 she::MenuItemInfo info;
725 AppMenuItem* appMenuItem = dynamic_cast<AppMenuItem*>(child);
726 AppMenuItem::Native native;
727
728 if (child->type() == kSeparatorWidget) {
729 info.type = she::MenuItemInfo::Separator;
730 }
731 else if (child->type() == kMenuItemWidget) {
732 if (appMenuItem &&
733 appMenuItem->getCommand()) {
734 native = get_native_shortcut_for_command(
735 appMenuItem->getCommand()->id().c_str(),
736 appMenuItem->getParams());
737 }
738
739 info.type = she::MenuItemInfo::Normal;
740 info.text = child->text();
741 info.shortcut = native.shortcut;
742 info.execute = [appMenuItem]{
743 if (can_call_global_shortcut(appMenuItem->native()))
744 appMenuItem->executeClick();
745 };
746 info.validate = [appMenuItem](she::MenuItem* osItem) {
747 if (can_call_global_shortcut(appMenuItem->native())) {
748 appMenuItem->validateItem();
749 osItem->setEnabled(appMenuItem->isEnabled());
750 osItem->setChecked(appMenuItem->isSelected());
751 }
752 else {
753 // Disable item when there are a modal window
754 osItem->setEnabled(false);
755 }
756 };
757 }
758 else {
759 ASSERT(false); // Unsupported menu item type
760 continue;
761 }
762
763 she::MenuItem* osItem = menus->createMenuItem(info);
764 if (osItem) {
765 osMenu->addItem(osItem);
766 if (appMenuItem) {
767 native.menuItem = osItem;
768 appMenuItem->setNative(native);
769 }
770
771 if (child->type() == ui::kMenuItemWidget &&
772 ((ui::MenuItem*)child)->hasSubmenu()) {
773 she::Menu* osSubmenu = menus->createMenu();
774 createNativeSubmenus(osSubmenu, ((ui::MenuItem*)child)->getSubmenu());
775 osItem->setSubmenu(osSubmenu);
776 }
777 }
778 }
779 }
780
781 } // namespace app
782