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