1// Aseprite
2// Copyright (C) 2019-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_menus.h"
13
14#include "app/app.h"
15#include "app/commands/command.h"
16#include "app/commands/commands.h"
17#include "app/commands/params.h"
18#include "app/console.h"
19#include "app/extensions.h"
20#include "app/gui_xml.h"
21#include "app/i18n/strings.h"
22#include "app/recent_files.h"
23#include "app/resource_finder.h"
24#include "app/tools/tool_box.h"
25#include "app/ui/app_menuitem.h"
26#include "app/ui/keyboard_shortcuts.h"
27#include "app/ui/main_window.h"
28#include "app/ui_context.h"
29#include "app/util/filetoks.h"
30#include "base/fs.h"
31#include "base/string.h"
32#include "fmt/format.h"
33#include "os/menus.h"
34#include "os/system.h"
35#include "ui/ui.h"
36#include "ver/info.h"
37
38#include "tinyxml.h"
39
40#include <cctype>
41#include <cstring>
42#include <string>
43#include <algorithm>
44#include <cstdlib>
45
46#define MENUS_TRACE(...) // TRACEARGS
47
48namespace app {
49
50using namespace ui;
51
52namespace {
53
54// TODO Move this to "os" layer
55const int kUnicodeEsc = 27;
56const int kUnicodeEnter = '\r'; // 10
57const int kUnicodeInsert = 0xF727; // NSInsertFunctionKey
58const int kUnicodeDel = 0xF728; // NSDeleteFunctionKey
59const int kUnicodeHome = 0xF729; // NSHomeFunctionKey
60const int kUnicodeEnd = 0xF72B; // NSEndFunctionKey
61const int kUnicodePageUp = 0xF72C; // NSPageUpFunctionKey
62const int kUnicodePageDown = 0xF72D; // NSPageDownFunctionKey
63const int kUnicodeLeft = 0xF702; // NSLeftArrowFunctionKey
64const int kUnicodeRight = 0xF703; // NSRightArrowFunctionKey
65const int kUnicodeUp = 0xF700; // NSUpArrowFunctionKey
66const int kUnicodeDown = 0xF701; // NSDownArrowFunctionKey
67
68const char* kFileRecentListGroup = "file_recent_list";
69
70void destroy_instance(AppMenus* instance)
71{
72 delete instance;
73}
74
75bool is_text_entry_shortcut(const os::Shortcut& shortcut)
76{
77 const os::KeyModifiers mod = shortcut.modifiers();
78 const int chr = shortcut.unicode();
79 const int lchr = std::tolower(chr);
80
81 bool result =
82 ((mod == os::KeyModifiers::kKeyNoneModifier ||
83 mod == os::KeyModifiers::kKeyShiftModifier) &&
84 chr >= 32 && chr < 0xF000)
85 ||
86 ((mod == os::KeyModifiers::kKeyCmdModifier ||
87 mod == os::KeyModifiers::kKeyCtrlModifier) &&
88 (lchr == 'a' || lchr == 'c' || lchr == 'v' || lchr == 'x'))
89 ||
90 (chr == kUnicodeInsert ||
91 chr == kUnicodeDel ||
92 chr == kUnicodeHome ||
93 chr == kUnicodeEnd ||
94 chr == kUnicodeLeft ||
95 chr == kUnicodeRight ||
96 chr == kUnicodeEsc ||
97 chr == kUnicodeEnter);
98
99 return result;
100}
101
102bool can_call_global_shortcut(const AppMenuItem::Native* native)
103{
104 ASSERT(native);
105
106 ui::Manager* manager = ui::Manager::getDefault();
107 ASSERT(manager);
108 ui::Widget* focus = manager->getFocus();
109 return
110 // The mouse is not capture
111 (manager->getCapture() == nullptr) &&
112 // The foreground window must be the main window to avoid calling
113 // a global command inside a modal dialog.
114 (manager->getForegroundWindow() == App::instance()->mainWindow()) &&
115 // If we are in a menubox window (e.g. we've pressed
116 // Alt+mnemonic), we should disable the native shortcuts
117 // temporarily so we can use mnemonics without modifiers
118 // (e.g. Alt+S opens the Sprite menu, then 'S' key should execute
119 // "Sprite Size" command in that menu, instead of Stroke command
120 // which is in 'Edit > Stroke'). This is necessary in macOS, when
121 // the native menu + Aseprite pixel-art menus are enabled.
122 (dynamic_cast<MenuBoxWindow*>(manager->getTopWindow()) == nullptr) &&
123 // The focused widget cannot be an entry, because entry fields
124 // prefer text input, so we cannot call shortcuts without
125 // modifiers (e.g. F or T keystrokes) to trigger a global command
126 // in a text field.
127 (focus == nullptr ||
128 focus->type() != ui::kEntryWidget ||
129 !is_text_entry_shortcut(native->shortcut)) &&
130 (native->keyContext == KeyContext::Any ||
131 native->keyContext == KeyboardShortcuts::instance()->getCurrentKeyContext());
132}
133
134// TODO this should be on "she" library (or we should use
135// os::Shortcut instead of ui::Accelerators)
136int from_scancode_to_unicode(KeyScancode scancode)
137{
138 static int map[] = {
139 0, // kKeyNil
140 'a', // kKeyA
141 'b', // kKeyB
142 'c', // kKeyC
143 'd', // kKeyD
144 'e', // kKeyE
145 'f', // kKeyF
146 'g', // kKeyG
147 'h', // kKeyH
148 'i', // kKeyI
149 'j', // kKeyJ
150 'k', // kKeyK
151 'l', // kKeyL
152 'm', // kKeyM
153 'n', // kKeyN
154 'o', // kKeyO
155 'p', // kKeyP
156 'q', // kKeyQ
157 'r', // kKeyR
158 's', // kKeyS
159 't', // kKeyT
160 'u', // kKeyU
161 'v', // kKeyV
162 'w', // kKeyW
163 'x', // kKeyX
164 'y', // kKeyY
165 'z', // kKeyZ
166 '0', // kKey0
167 '1', // kKey1
168 '2', // kKey2
169 '3', // kKey3
170 '4', // kKey4
171 '5', // kKey5
172 '6', // kKey6
173 '7', // kKey7
174 '8', // kKey8
175 '9', // kKey9
176 0, // kKey0Pad
177 0, // kKey1Pad
178 0, // kKey2Pad
179 0, // kKey3Pad
180 0, // kKey4Pad
181 0, // kKey5Pad
182 0, // kKey6Pad
183 0, // kKey7Pad
184 0, // kKey8Pad
185 0, // kKey9Pad
186 0xF704, // kKeyF1 (NSF1FunctionKey)
187 0xF705, // kKeyF2
188 0xF706, // kKeyF3
189 0xF707, // kKeyF4
190 0xF708, // kKeyF5
191 0xF709, // kKeyF6
192 0xF70A, // kKeyF7
193 0xF70B, // kKeyF8
194 0xF70C, // kKeyF9
195 0xF70D, // kKeyF10
196 0xF70E, // kKeyF11
197 0xF70F, // kKeyF12
198 kUnicodeEsc, // kKeyEsc
199 '~', // kKeyTilde
200 '-', // kKeyMinus
201 '=', // kKeyEquals
202 8, // kKeyBackspace
203 9, // kKeyTab
204 '[', // kKeyOpenbrace
205 ']', // kKeyClosebrace
206 kUnicodeEnter, // kKeyEnter
207 ':', // kKeyColon
208 '\'', // kKeyQuote
209 '\\', // kKeyBackslash
210 0, // kKeyBackslash2
211 ',', // kKeyComma
212 '.', // kKeyStop
213 '/', // kKeySlash
214 ' ', // kKeySpace
215 kUnicodeInsert, // kKeyInsert (NSInsertFunctionKey)
216 kUnicodeDel, // kKeyDel (NSDeleteFunctionKey)
217 kUnicodeHome, // kKeyHome (NSHomeFunctionKey)
218 kUnicodeEnd, // kKeyEnd (NSEndFunctionKey)
219 kUnicodePageUp, // kKeyPageUp (NSPageUpFunctionKey)
220 kUnicodePageDown, // kKeyPageDown (NSPageDownFunctionKey)
221 kUnicodeLeft, // kKeyLeft (NSLeftArrowFunctionKey)
222 kUnicodeRight, // kKeyRight (NSRightArrowFunctionKey)
223 kUnicodeUp, // kKeyUp (NSUpArrowFunctionKey)
224 kUnicodeDown, // kKeyDown (NSDownArrowFunctionKey)
225 '/', // kKeySlashPad
226 '*', // kKeyAsterisk
227 0, // kKeyMinusPad
228 0, // kKeyPlusPad
229 0, // kKeyDelPad
230 0, // kKeyEnterPad
231 0, // kKeyPrtscr
232 0, // kKeyPause
233 0, // kKeyAbntC1
234 0, // kKeyYen
235 0, // kKeyKana
236 0, // kKeyConvert
237 0, // kKeyNoconvert
238 0, // kKeyAt
239 0, // kKeyCircumflex
240 0, // kKeyColon2
241 0, // kKeyKanji
242 0, // kKeyEqualsPad
243 '`', // kKeyBackquote
244 0, // kKeySemicolon
245 0, // kKeyUnknown1
246 0, // kKeyUnknown2
247 0, // kKeyUnknown3
248 0, // kKeyUnknown4
249 0, // kKeyUnknown5
250 0, // kKeyUnknown6
251 0, // kKeyUnknown7
252 0, // kKeyUnknown8
253 0, // kKeyLShift
254 0, // kKeyRShift
255 0, // kKeyLControl
256 0, // kKeyRControl
257 0, // kKeyAlt
258 0, // kKeyAltGr
259 0, // kKeyLWin
260 0, // kKeyRWin
261 0, // kKeyMenu
262 0, // kKeyCommand
263 0, // kKeyScrLock
264 0, // kKeyNumLock
265 0, // kKeyCapsLock
266 };
267 if (scancode >= 0 && scancode < sizeof(map) / sizeof(map[0]))
268 return map[scancode];
269 else
270 return 0;
271}
272
273AppMenuItem::Native get_native_shortcut_for_command(
274 const char* commandId,
275 const Params& params = Params())
276{
277 AppMenuItem::Native native;
278 KeyPtr key = KeyboardShortcuts::instance()->command(commandId, params);
279 if (key) {
280 native.shortcut = get_os_shortcut_from_key(key.get());
281 native.keyContext = key->keycontext();
282 }
283 return native;
284}
285
286} // anonymous namespace
287
288os::Shortcut get_os_shortcut_from_key(const Key* key)
289{
290 if (key && !key->accels().empty()) {
291 const ui::Accelerator& accel = key->accels().front();
292
293#ifdef __APPLE__
294 // Shortcuts with spacebar as modifier do not work well in macOS
295 // (they will be called when the space bar is unpressed too).
296 if ((accel.modifiers() & ui::kKeySpaceModifier) == ui::kKeySpaceModifier)
297 return os::Shortcut();
298#endif
299
300 return os::Shortcut(
301 (accel.unicodeChar() ? accel.unicodeChar():
302 from_scancode_to_unicode(accel.scancode())),
303 accel.modifiers());
304 }
305 else
306 return os::Shortcut();
307}
308
309// static
310AppMenus* AppMenus::instance()
311{
312 static AppMenus* instance = NULL;
313 if (!instance) {
314 instance = new AppMenus;
315 App::instance()->Exit.connect([]{ destroy_instance(instance); });
316 }
317 return instance;
318}
319
320AppMenus::AppMenus()
321 : m_recentFilesPlaceholder(nullptr)
322 , m_osMenu(nullptr)
323{
324 m_recentFilesConn =
325 App::instance()->recentFiles()->Changed.connect(
326 [this]{ rebuildRecentList(); });
327}
328
329void AppMenus::reload()
330{
331 MENUS_TRACE("MENUS: AppMenus::reload()");
332
333 XmlDocumentRef doc(GuiXml::instance()->doc());
334 TiXmlHandle handle(doc.get());
335 const char* path = GuiXml::instance()->filename();
336
337 ////////////////////////////////////////
338 // Remove all menu items added to groups from recent files and
339 // scripts so we can re-add them later in the new menus.
340
341 for (auto& it : m_groups) {
342 GroupInfo& group = it.second;
343 MENUS_TRACE("MENUS: - groups", it.first, "with", group.items.size(), "item(s)");
344 group.end = nullptr; // This value will be restored later
345 for (auto& item : group.items)
346 item->parent()->removeChild(item);
347 }
348
349 ////////////////////////////////////////
350 // Load menus
351
352 LOG("MENU: Loading menus from %s\n", path);
353
354 m_rootMenu.reset(loadMenuById(handle, "main_menu"));
355
356 LOG("MENU: Main menu loaded.\n");
357
358 m_tabPopupMenu.reset(loadMenuById(handle, "tab_popup_menu"));
359 m_documentTabPopupMenu.reset(loadMenuById(handle, "document_tab_popup_menu"));
360 m_layerPopupMenu.reset(loadMenuById(handle, "layer_popup_menu"));
361 m_framePopupMenu.reset(loadMenuById(handle, "frame_popup_menu"));
362 m_celPopupMenu.reset(loadMenuById(handle, "cel_popup_menu"));
363 m_celMovementPopupMenu.reset(loadMenuById(handle, "cel_movement_popup_menu"));
364 m_tagPopupMenu.reset(loadMenuById(handle, "tag_popup_menu"));
365 m_slicePopupMenu.reset(loadMenuById(handle, "slice_popup_menu"));
366 m_palettePopupMenu.reset(loadMenuById(handle, "palette_popup_menu"));
367 m_inkPopupMenu.reset(loadMenuById(handle, "ink_popup_menu"));
368
369 // Add one menu item to run each script from the user scripts/ folder
370 {
371 MenuItem* scriptsMenu = dynamic_cast<MenuItem*>(
372 m_rootMenu->findItemById("scripts_menu"));
373#ifdef ENABLE_SCRIPTING
374 // Load scripts
375 ResourceFinder rf;
376 rf.includeUserDir("scripts/.");
377 std::string scriptsDir = rf.getFirstOrCreateDefault();
378 scriptsDir = base::get_file_path(scriptsDir);
379 if (base::is_directory(scriptsDir)) {
380 loadScriptsSubmenu(scriptsMenu->getSubmenu(), scriptsDir, true);
381 }
382#else
383 // Scripting is not available
384 if (scriptsMenu) {
385 delete scriptsMenu;
386 delete m_rootMenu->findItemById("scripts_menu_separator");
387
388 // Remove scripts group
389 auto it = m_groups.find("file_scripts");
390 if (it != m_groups.end())
391 m_groups.erase(it);
392 }
393#endif
394 }
395
396 // Remove the "Enter license" menu item when DRM is not enabled.
397#ifndef ENABLE_DRM
398 if (auto helpMenuItem = m_rootMenu->findItemById("help_menu")) {
399 if (Menu* helpMenu = dynamic_cast<MenuItem*>(helpMenuItem)->getSubmenu()) {
400 delete helpMenu->findChild("enter_license_separator");
401 delete helpMenu->findChild("enter_license");
402 }
403
404 auto it = m_groups.find("help_enter_license");
405 if (it != m_groups.end())
406 m_groups.erase(it);
407 }
408#endif
409
410 ////////////////////////////////////////
411 // Re-add menu items in groups (recent files & scripts)
412
413 for (auto& it : m_groups) {
414 GroupInfo& group = it.second;
415 if (group.end) {
416 MENUS_TRACE("MENUS: - re-adding group ", it.first, "with", group.items.size(), "item(s)");
417
418 auto menu = group.end->parent();
419 int insertIndex = menu->getChildIndex(group.end);
420 for (auto& item : group.items) {
421 menu->insertChild(++insertIndex, item);
422 group.end = item;
423 }
424 }
425 // Delete items that don't have a group now
426 else {
427 MENUS_TRACE("MENUS: - deleting group ", it.first, "with", group.items.size(), "item(s)");
428 for (auto& item : group.items)
429 item->deferDelete();
430 group.items.clear();
431 }
432 }
433
434 ////////////////////////////////////////
435 // Load keyboard shortcuts for commands
436
437 LOG("MENU: Loading commands keyboard shortcuts from %s\n", path);
438
439 TiXmlElement* xmlKey = handle
440 .FirstChild("gui")
441 .FirstChild("keyboard").ToElement();
442
443 // From a fresh start, load the default keys
444 KeyboardShortcuts::instance()->clear();
445 KeyboardShortcuts::instance()->importFile(xmlKey, KeySource::Original);
446
447 // Load extension-defined keys
448 for (const Extension* ext : App::instance()->extensions()) {
449 if (ext->isEnabled() &&
450 ext->hasKeys()) {
451 for (const auto& kv : ext->keys()) {
452 KeyboardShortcuts::instance()->importFile(
453 kv.second, KeySource::ExtensionDefined);
454 }
455 }
456 }
457
458 // Load user-defined keys
459 {
460 ResourceFinder rf;
461 rf.includeUserDir("user.aseprite-keys");
462 std::string fn = rf.getFirstOrCreateDefault();
463 if (base::is_file(fn))
464 KeyboardShortcuts::instance()->importFile(fn, KeySource::UserDefined);
465 }
466
467 // Create native menus after the default + user defined keyboard
468 // shortcuts are loaded correctly.
469 createNativeMenus();
470}
471
472#ifdef ENABLE_SCRIPTING
473void AppMenus::loadScriptsSubmenu(ui::Menu* menu,
474 const std::string& dir,
475 const bool rootLevel)
476{
477 auto files = base::list_files(dir);
478 std::sort(files.begin(), files.end(),
479 [](const std::string& a, const std::string& b) {
480 return base::compare_filenames(a, b) < 0;
481 });
482 int insertPos = 0;
483 for (auto fn : files) {
484 std::string fullFn = base::join_path(dir, fn);
485 AppMenuItem* menuitem = nullptr;
486
487 if (fn[0] == '.') // Ignore all files and directories that start with a dot
488 continue;
489
490 if (base::is_file(fullFn)) {
491 if (base::string_to_lower(base::get_file_extension(fn)) == "lua") {
492 Params params;
493 params.set("filename", fullFn.c_str());
494 menuitem = new AppMenuItem(
495 base::get_file_title(fn).c_str(),
496 CommandId::RunScript(),
497 params);
498 }
499 }
500 else if (base::is_directory(fullFn)) {
501 Menu* submenu = new Menu();
502 loadScriptsSubmenu(submenu, fullFn, false);
503
504 menuitem = new AppMenuItem(
505 base::get_file_title(fn).c_str());
506 menuitem->setSubmenu(submenu);
507 }
508 if (menuitem) {
509 menu->insertChild(insertPos++, menuitem);
510 }
511 }
512 if (rootLevel && insertPos > 0)
513 menu->insertChild(insertPos, new MenuSeparator());
514}
515#endif
516
517void AppMenus::initTheme()
518{
519 updateMenusList();
520 for (Menu* menu : m_menus)
521 if (menu)
522 menu->initTheme();
523}
524
525bool AppMenus::rebuildRecentList()
526{
527 MENUS_TRACE("MENUS: AppMenus::rebuildRecentList m_recentFilesPlaceholder=", m_recentFilesPlaceholder);
528
529 if (!m_recentFilesPlaceholder)
530 return true;
531
532 Menu* menu = dynamic_cast<Menu*>(m_recentFilesPlaceholder->parent());
533 if (!menu)
534 return false;
535
536 AppMenuItem* owner = dynamic_cast<AppMenuItem*>(menu->getOwnerMenuItem());
537 if (!owner || owner->hasSubmenuOpened())
538 return false;
539
540 // Remove active items
541 for (auto item : m_recentMenuItems)
542 removeMenuItemFromGroup(item);
543 m_recentMenuItems.clear();
544
545 auto recent = App::instance()->recentFiles();
546 base::paths files;
547 files.insert(files.end(),
548 recent->pinnedFiles().begin(),
549 recent->pinnedFiles().end());
550 files.insert(files.end(),
551 recent->recentFiles().begin(),
552 recent->recentFiles().end());
553 if (!files.empty()) {
554 Params params;
555 for (const auto& fn : files) {
556 params.set("filename", fn.c_str());
557
558 std::unique_ptr<AppMenuItem> menuitem(
559 new AppMenuItem(base::get_file_name(fn).c_str(),
560 CommandId::OpenFile(),
561 params));
562 menuitem->setIsRecentFileItem(true);
563
564 m_recentMenuItems.push_back(menuitem.get());
565 addMenuItemIntoGroup(kFileRecentListGroup, std::move(menuitem));
566 }
567 }
568 else {
569 std::unique_ptr<AppMenuItem> menuitem(
570 new AppMenuItem(
571 Strings::main_menu_file_no_recent_file()));
572 menuitem->setIsRecentFileItem(true);
573 menuitem->setEnabled(false);
574
575 m_recentMenuItems.push_back(menuitem.get());
576 addMenuItemIntoGroup(kFileRecentListGroup, std::move(menuitem));
577 }
578
579 // Sync native menus
580 if (owner->native() &&
581 owner->native()->menuItem) {
582 auto menus = os::instance()->menus();
583 os::MenuRef osMenu = (menus ? menus->makeMenu(): nullptr);
584 if (osMenu) {
585 createNativeSubmenus(osMenu.get(), menu);
586 owner->native()->menuItem->setSubmenu(osMenu);
587 }
588 }
589
590 return true;
591}
592
593void AppMenus::addMenuItemIntoGroup(const std::string& groupId,
594 std::unique_ptr<MenuItem>&& menuItem)
595{
596 GroupInfo& group = m_groups[groupId];
597 Widget* menu = group.end->parent();
598 ASSERT(menu);
599 int insertIndex = menu->getChildIndex(group.end);
600 menu->insertChild(insertIndex+1, menuItem.get());
601
602 group.end = menuItem.get();
603 group.items.push_back(menuItem.get());
604
605 menuItem.release();
606}
607
608template<typename Pred>
609void AppMenus::removeMenuItemFromGroup(Pred pred)
610{
611 for (auto& it : m_groups) {
612 GroupInfo& group = it.second;
613 for (auto it=group.items.begin(); it != group.items.end(); ) {
614 auto& item = *it;
615 if (pred(item)) {
616 if (item == group.end)
617 group.end = group.end->previousSibling();
618
619 item->parent()->removeChild(item);
620 if (auto appItem = dynamic_cast<AppMenuItem*>(item)) {
621 if (appItem)
622 appItem->disposeNative();
623 }
624 item->deferDelete();
625
626 it = group.items.erase(it);
627 }
628 else {
629 ++it;
630 }
631 }
632 }
633}
634
635void AppMenus::removeMenuItemFromGroup(Command* cmd)
636{
637 removeMenuItemFromGroup(
638 [cmd](Widget* item){
639 auto appMenuItem = dynamic_cast<AppMenuItem*>(item);
640 return (appMenuItem && appMenuItem->getCommand() == cmd);
641 });
642}
643
644void AppMenus::removeMenuItemFromGroup(Widget* menuItem)
645{
646 removeMenuItemFromGroup(
647 [menuItem](Widget* item){
648 return (item == menuItem);
649 });
650}
651
652Menu* AppMenus::loadMenuById(TiXmlHandle& handle, const char* id)
653{
654 ASSERT(id != NULL);
655
656 // <gui><menus><menu>
657 TiXmlElement* xmlMenu = handle
658 .FirstChild("gui")
659 .FirstChild("menus")
660 .FirstChild("menu").ToElement();
661 while (xmlMenu) {
662 const char* menuId = xmlMenu->Attribute("id");
663
664 if (menuId && strcmp(menuId, id) == 0) {
665 m_xmlTranslator.setStringIdPrefix(menuId);
666 return convertXmlelemToMenu(xmlMenu);
667 }
668
669 xmlMenu = xmlMenu->NextSiblingElement();
670 }
671
672 throw base::Exception("Error loading menu '%s'\nReinstall the application.", id);
673}
674
675Menu* AppMenus::convertXmlelemToMenu(TiXmlElement* elem)
676{
677 Menu* menu = new Menu();
678 menu->setText(m_xmlTranslator(elem, "text"));
679
680 TiXmlElement* child = elem->FirstChildElement();
681 while (child) {
682 Widget* menuitem = convertXmlelemToMenuitem(child);
683 if (menuitem)
684 menu->addChild(menuitem);
685 else
686 throw base::Exception("Error converting the element \"%s\" to a menu-item.\n",
687 static_cast<const char*>(child->Value()));
688
689 child = child->NextSiblingElement();
690 }
691
692 return menu;
693}
694
695Widget* AppMenus::convertXmlelemToMenuitem(TiXmlElement* elem)
696{
697 const char* id = elem->Attribute("id");
698 const char* group = elem->Attribute("group");
699
700 // is it a <separator>?
701 if (strcmp(elem->Value(), "separator") == 0) {
702 auto item = new MenuSeparator;
703 if (id) {
704 item->setId(id);
705
706 // Recent list menu
707 if (std::strcmp(id, "recent_files_placeholder") == 0) {
708 m_recentFilesPlaceholder = item;
709 }
710 }
711 if (group)
712 m_groups[group].end = item;
713 return item;
714 }
715
716 const char* command_id = elem->Attribute("command");
717 Command* command =
718 command_id ? Commands::instance()->byId(command_id):
719 nullptr;
720
721 // load params
722 Params params;
723 if (command) {
724 TiXmlElement* xmlParam = elem->FirstChildElement("param");
725 while (xmlParam) {
726 const char* param_name = xmlParam->Attribute("name");
727 const char* param_value = xmlParam->Attribute("value");
728
729 if (param_name && param_value)
730 params.set(param_name, param_value);
731
732 xmlParam = xmlParam->NextSiblingElement();
733 }
734 }
735
736 // Create the item
737 AppMenuItem* menuitem = new AppMenuItem(m_xmlTranslator(elem, "text"),
738 (command ? command->id(): ""),
739 params);
740 if (!menuitem)
741 return nullptr;
742
743 if (id) menuitem->setId(id);
744 menuitem->processMnemonicFromText();
745 if (group)
746 m_groups[group].end = menuitem;
747
748 // Has it a ID?
749 if (id) {
750 if (std::strcmp(id, "help_menu") == 0) {
751 m_helpMenuitem = menuitem;
752 }
753 }
754
755 // Has it a sub-menu (<menu>)?
756 if (strcmp(elem->Value(), "menu") == 0) {
757 // Create the sub-menu
758 Menu* subMenu = convertXmlelemToMenu(elem);
759 if (!subMenu)
760 throw base::Exception("Error reading the sub-menu\n");
761
762 menuitem->setSubmenu(subMenu);
763 }
764
765 return menuitem;
766}
767
768void AppMenus::applyShortcutToMenuitemsWithCommand(Command* command,
769 const Params& params,
770 const KeyPtr& key)
771{
772 updateMenusList();
773 for (Menu* menu : m_menus)
774 if (menu)
775 applyShortcutToMenuitemsWithCommand(menu, command, params, key);
776}
777
778void AppMenus::applyShortcutToMenuitemsWithCommand(Menu* menu,
779 Command* command,
780 const Params& params,
781 const KeyPtr& key)
782{
783 for (auto child : menu->children()) {
784 if (child->type() == kMenuItemWidget) {
785 AppMenuItem* menuitem = dynamic_cast<AppMenuItem*>(child);
786 if (!menuitem)
787 continue;
788
789 const std::string& mi_commandId = menuitem->getCommandId();
790 const Params& mi_params = menuitem->getParams();
791
792 if ((base::utf8_icmp(mi_commandId, command->id()) == 0) &&
793 (mi_params == params)) {
794 // Set the keyboard shortcut to be shown in this menu-item
795 menuitem->setKey(key);
796 }
797
798 if (Menu* submenu = menuitem->getSubmenu())
799 applyShortcutToMenuitemsWithCommand(submenu, command, params, key);
800 }
801 }
802}
803
804void AppMenus::syncNativeMenuItemKeyShortcuts()
805{
806 syncNativeMenuItemKeyShortcuts(m_rootMenu.get());
807}
808
809void AppMenus::syncNativeMenuItemKeyShortcuts(Menu* menu)
810{
811 for (auto child : menu->children()) {
812 if (child->type() == kMenuItemWidget) {
813 if (AppMenuItem* menuitem = dynamic_cast<AppMenuItem*>(child))
814 menuitem->syncNativeMenuItemKeyShortcut();
815
816 if (Menu* submenu = static_cast<MenuItem*>(child)->getSubmenu())
817 syncNativeMenuItemKeyShortcuts(submenu);
818 }
819 }
820}
821
822// TODO redesign the list of popup menus, it might be an
823// autogenerated widget from 'gen'
824void AppMenus::updateMenusList()
825{
826 m_menus.clear();
827 m_menus.push_back(m_rootMenu.get());
828 m_menus.push_back(m_tabPopupMenu.get());
829 m_menus.push_back(m_documentTabPopupMenu.get());
830 m_menus.push_back(m_layerPopupMenu.get());
831 m_menus.push_back(m_framePopupMenu.get());
832 m_menus.push_back(m_celPopupMenu.get());
833 m_menus.push_back(m_celMovementPopupMenu.get());
834 m_menus.push_back(m_tagPopupMenu.get());
835 m_menus.push_back(m_slicePopupMenu.get());
836 m_menus.push_back(m_palettePopupMenu.get());
837 m_menus.push_back(m_inkPopupMenu.get());
838}
839
840void AppMenus::createNativeMenus()
841{
842 os::Menus* menus = os::instance()->menus();
843 if (!menus) // This platform doesn't support native menu items
844 return;
845
846 // Save a reference to the old menu to avoid destroying it.
847 os::MenuRef oldOSMenu = m_osMenu;
848 m_osMenu = menus->makeMenu();
849
850#ifdef __APPLE__ // Create default macOS app menus (App ... Window)
851 {
852 os::MenuItemInfo about(fmt::format("About {}", get_app_name()));
853 auto native = get_native_shortcut_for_command(CommandId::About());
854 about.shortcut = native.shortcut;
855 about.execute = [native]{
856 if (can_call_global_shortcut(&native)) {
857 Command* cmd = Commands::instance()->byId(CommandId::About());
858 UIContext::instance()->executeCommandFromMenuOrShortcut(cmd);
859 }
860 };
861 about.validate = [native](os::MenuItem* item){
862 item->setEnabled(can_call_global_shortcut(&native));
863 };
864
865 os::MenuItemInfo preferences("Preferences...");
866 native = get_native_shortcut_for_command(CommandId::Options());
867 preferences.shortcut = native.shortcut;
868 preferences.execute = [native]{
869 if (can_call_global_shortcut(&native)) {
870 Command* cmd = Commands::instance()->byId(CommandId::Options());
871 UIContext::instance()->executeCommandFromMenuOrShortcut(cmd);
872 }
873 };
874 preferences.validate = [native](os::MenuItem* item){
875 item->setEnabled(can_call_global_shortcut(&native));
876 };
877
878 os::MenuItemInfo hide(fmt::format("Hide {}", get_app_name()), os::MenuItemInfo::Hide);
879 hide.shortcut = os::Shortcut('h', os::kKeyCmdModifier);
880
881 os::MenuItemInfo quit(fmt::format("Quit {}", get_app_name()), os::MenuItemInfo::Quit);
882 quit.shortcut = os::Shortcut('q', os::kKeyCmdModifier);
883
884 os::MenuRef appMenu = menus->makeMenu();
885 appMenu->addItem(menus->makeMenuItem(about));
886 appMenu->addItem(menus->makeMenuItem(os::MenuItemInfo(os::MenuItemInfo::Separator)));
887 appMenu->addItem(menus->makeMenuItem(preferences));
888 appMenu->addItem(menus->makeMenuItem(os::MenuItemInfo(os::MenuItemInfo::Separator)));
889 appMenu->addItem(menus->makeMenuItem(hide));
890 appMenu->addItem(menus->makeMenuItem(os::MenuItemInfo("Hide Others", os::MenuItemInfo::HideOthers)));
891 appMenu->addItem(menus->makeMenuItem(os::MenuItemInfo("Show All", os::MenuItemInfo::ShowAll)));
892 appMenu->addItem(menus->makeMenuItem(os::MenuItemInfo(os::MenuItemInfo::Separator)));
893 appMenu->addItem(menus->makeMenuItem(quit));
894
895 os::MenuItemRef appItem = menus->makeMenuItem(os::MenuItemInfo("App"));
896 appItem->setSubmenu(appMenu);
897 m_osMenu->addItem(appItem);
898 }
899#endif
900
901 createNativeSubmenus(m_osMenu.get(), m_rootMenu.get());
902
903#ifdef __APPLE__
904 {
905 // Search the index where help menu is located (so the Window menu
906 // can take its place/index position)
907 int i = 0, helpIndex = int(m_rootMenu->children().size());
908 for (const auto child : m_rootMenu->children()) {
909 if (child == m_helpMenuitem) {
910 helpIndex = i;
911 break;
912 }
913 ++i;
914 }
915
916 os::MenuItemInfo minimize("Minimize", os::MenuItemInfo::Minimize);
917 minimize.shortcut = os::Shortcut('m', os::kKeyCmdModifier);
918
919 os::MenuRef windowMenu = menus->makeMenu();
920 windowMenu->addItem(menus->makeMenuItem(minimize));
921 windowMenu->addItem(menus->makeMenuItem(os::MenuItemInfo("Zoom", os::MenuItemInfo::Zoom)));
922
923 os::MenuItemRef windowItem = menus->makeMenuItem(os::MenuItemInfo("Window"));
924 windowItem->setSubmenu(windowMenu);
925
926 // We use helpIndex+1 because the first index in m_osMenu is the
927 // App menu.
928 m_osMenu->insertItem(helpIndex+1, windowItem);
929 }
930#endif
931
932 menus->setAppMenu(m_osMenu);
933 if (oldOSMenu)
934 oldOSMenu.reset();
935}
936
937void AppMenus::createNativeSubmenus(os::Menu* osMenu,
938 const ui::Menu* uiMenu)
939{
940 os::Menus* menus = os::instance()->menus();
941
942 for (const auto& child : uiMenu->children()) {
943 os::MenuItemInfo info;
944 AppMenuItem* appMenuItem = dynamic_cast<AppMenuItem*>(child);
945 AppMenuItem::Native native;
946
947 if (child->type() == kSeparatorWidget) {
948 info.type = os::MenuItemInfo::Separator;
949 }
950 else if (child->type() == kMenuItemWidget) {
951 if (appMenuItem &&
952 appMenuItem->getCommand()) {
953 native = get_native_shortcut_for_command(
954 appMenuItem->getCommandId().c_str(),
955 appMenuItem->getParams());
956 }
957
958 info.type = os::MenuItemInfo::Normal;
959 info.text = child->text();
960 info.shortcut = native.shortcut;
961 info.execute = [appMenuItem]{
962 if (can_call_global_shortcut(appMenuItem->native()))
963 appMenuItem->executeClick();
964 };
965 info.validate = [appMenuItem](os::MenuItem* osItem) {
966 if (can_call_global_shortcut(appMenuItem->native())) {
967 appMenuItem->validateItem();
968 osItem->setEnabled(appMenuItem->isEnabled());
969 osItem->setChecked(appMenuItem->isSelected());
970 }
971 else {
972 // Disable item when there are a modal window
973 osItem->setEnabled(false);
974 }
975 };
976 }
977 else {
978 ASSERT(false); // Unsupported menu item type
979 continue;
980 }
981
982 os::MenuItemRef osItem = menus->makeMenuItem(info);
983 if (osItem) {
984 osMenu->addItem(osItem);
985 if (appMenuItem) {
986 native.menuItem = osItem;
987 appMenuItem->setNative(native);
988 }
989
990 if (child->type() == ui::kMenuItemWidget &&
991 ((ui::MenuItem*)child)->hasSubmenu()) {
992 os::MenuRef osSubmenu = menus->makeMenu();
993 createNativeSubmenus(osSubmenu.get(), ((ui::MenuItem*)child)->getSubmenu());
994 osItem->setSubmenu(osSubmenu);
995 }
996 }
997 }
998}
999
1000} // namespace app
1001