1// Aseprite UI Library
2// Copyright (C) 2018-2022 Igara Studio S.A.
3// Copyright (C) 2001-2018 David Capello
4//
5// This file is released under the terms of the MIT license.
6// Read LICENSE.txt for more information.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "ui/menu.h"
13
14#include "gfx/size.h"
15#include "os/font.h"
16#include "ui/display.h"
17#include "ui/intern.h"
18#include "ui/ui.h"
19
20#include <algorithm>
21#include <cctype>
22#include <memory>
23
24static const int kTimeoutToOpenSubmenu = 250;
25
26namespace ui {
27
28using namespace gfx;
29
30//////////////////////////////////////////////////////////////////////
31// Internal messages: to move between menus
32
33RegisterMessage kOpenMenuItemMessage;
34RegisterMessage kCloseMenuItemMessage;
35RegisterMessage kClosePopupMessage;
36RegisterMessage kExecuteMenuItemMessage;
37
38class OpenMenuItemMessage : public Message {
39public:
40 OpenMenuItemMessage(bool select_first) :
41 Message(kOpenMenuItemMessage),
42 m_select_first(select_first) {
43 }
44
45 // If this value is true, it means that after opening the menu, we
46 // have to select the first item (i.e. highlighting it).
47 bool select_first() const { return m_select_first; }
48
49private:
50 bool m_select_first;
51};
52
53class CloseMenuItemMessage : public Message {
54public:
55 CloseMenuItemMessage(bool last_of_close_chain) :
56 Message(kCloseMenuItemMessage),
57 m_last_of_close_chain(last_of_close_chain) {
58 }
59
60 // This fields is used to indicate the end of a sequence of
61 // kOpenMenuItemMessage and kCloseMenuItemMessage messages. If it is true
62 // the message is the last one of the chain, which means that no
63 // more kOpenMenuItemMessage or kCloseMenuItemMessage messages are in the queue.
64 bool last_of_close_chain() const { return m_last_of_close_chain; }
65
66private:
67 bool m_last_of_close_chain;
68};
69
70// Data for the main jmenubar or the first popuped-jmenubox
71struct MenuBaseData {
72 // True when the menu-items must be opened with the cursor movement
73 bool was_clicked;
74
75 // True when there's kOpen/CloseMenuItemMessage messages in queue, to
76 // avoid start processing another menuitem-request when we're
77 // already working in one
78 bool is_processing;
79
80 // True when the kMouseDownMessage is being filtered
81 bool is_filtering;
82
83 bool close_all;
84
85 MenuBaseData() {
86 was_clicked = false;
87 is_filtering = false;
88 is_processing = false;
89 close_all = false;
90 }
91
92};
93
94static MenuBox* get_base_menubox(Widget* widget);
95static MenuBaseData* get_base(Widget* widget);
96
97static MenuItem* check_for_letter(Menu* menu, const KeyMessage* keymsg);
98
99static MenuItem* find_nextitem(Menu* menu, MenuItem* menuitem);
100static MenuItem* find_previtem(Menu* menu, MenuItem* menuitem);
101
102static void choose_side(gfx::Rect& bounds,
103 const gfx::Rect& workarea,
104 const gfx::Rect& parentBounds)
105{
106 int scale = guiscale();
107 if (get_multiple_displays())
108 scale = Manager::getDefault()->display()->scale();
109
110 int x_left = parentBounds.x - bounds.w + 1*scale;
111 int x_right = parentBounds.x2() - 1*scale;
112 int x, y = bounds.y;
113 Rect r1(0, 0, bounds.w, bounds.h);
114 Rect r2(0, 0, bounds.w, bounds.h);
115
116 r1.x = x_left = std::clamp(x_left, workarea.x, std::max(workarea.x, workarea.x2()-bounds.w));
117 r2.x = x_right = std::clamp(x_right, workarea.x, std::max(workarea.x, workarea.x2()-bounds.w));
118 r1.y = r2.y = y = std::clamp(y, workarea.y, std::max(workarea.y, workarea.y2()-bounds.h));
119
120 // Calculate both intersections
121 const gfx::Rect s1 = r1.createIntersection(parentBounds);
122 const gfx::Rect s2 = r2.createIntersection(parentBounds);
123
124 if (s2.isEmpty())
125 x = x_right; // Use the right because there aren't intersection with it
126 else if (s1.isEmpty())
127 x = x_left; // Use the left because there are not intersection
128 else if (s2.w*s2.h <= s1.w*s1.h)
129 x = x_right; // Use the right because there are less intersection area
130 else
131 x = x_left; // Use the left because there are less intersection area
132
133 bounds.x = x;
134 bounds.y = y;
135}
136
137static void add_scrollbars_if_needed(MenuBoxWindow* window,
138 const gfx::Rect& workarea,
139 gfx::Rect& bounds)
140{
141 gfx::Rect rc = bounds;
142
143 if (rc.x < workarea.x) {
144 rc.w -= (workarea.x - rc.x);
145 rc.x = workarea.x;
146 }
147 if (rc.x2() > workarea.x2()) {
148 rc.w = workarea.x2() - rc.x;
149 }
150
151 bool vscrollbarsAdded = false;
152 if (rc.y < workarea.y) {
153 rc.h -= (workarea.y - rc.y);
154 rc.y = workarea.y;
155 vscrollbarsAdded = true;
156 }
157 if (rc.y2() > workarea.y2()) {
158 rc.h = workarea.y2() - rc.y;
159 vscrollbarsAdded = true;
160 }
161
162 if (rc == bounds)
163 return;
164
165 MenuBox* menubox = window->menubox();
166 View* view = new View;
167 view->InitTheme.connect([view]{ view->noBorderNoChildSpacing(); });
168 view->initTheme();
169
170 if (vscrollbarsAdded) {
171 int barWidth = view->verticalBar()->getBarWidth();;
172 if (get_multiple_displays())
173 barWidth *= window->display()->scale();
174
175 rc.w += 2*barWidth;
176 if (rc.x2() > workarea.x2()) {
177 rc.x = workarea.x2() - rc.w;
178 if (rc.x < workarea.x) {
179 rc.x = workarea.x;
180 rc.w = workarea.w;
181 }
182 }
183 }
184
185 // New bounds
186 bounds = rc;
187
188 window->removeChild(menubox);
189 view->attachToView(menubox);
190 window->addChild(view);
191}
192
193//////////////////////////////////////////////////////////////////////
194// Menu
195
196Menu::Menu()
197 : Widget(kMenuWidget)
198 , m_menuitem(nullptr)
199{
200 enableFlags(IGNORE_MOUSE);
201 initTheme();
202}
203
204Menu::~Menu()
205{
206 if (m_menuitem) {
207 if (m_menuitem->getSubmenu() == this) {
208 m_menuitem->setSubmenu(nullptr);
209 }
210 else {
211 ASSERT(m_menuitem->getSubmenu() == nullptr);
212 }
213 }
214}
215
216void Menu::onOpenPopup()
217{
218 OpenPopup();
219}
220
221//////////////////////////////////////////////////////////////////////
222// MenuBox
223
224MenuBox::MenuBox(WidgetType type)
225 : Widget(type)
226 , m_base(nullptr)
227{
228 this->setFocusStop(true);
229 initTheme();
230}
231
232MenuBox::~MenuBox()
233{
234 stopFilteringMouseDown();
235}
236
237//////////////////////////////////////////////////////////////////////
238// MenuBar
239
240bool MenuBar::m_expandOnMouseover = false;
241
242MenuBar::MenuBar(ProcessTopLevelShortcuts processShortcuts)
243 : MenuBox(kMenuBarWidget)
244 , m_processTopLevelShortcuts(processShortcuts == ProcessTopLevelShortcuts::kYes)
245{
246 createBase();
247}
248
249// static
250bool MenuBar::expandOnMouseover()
251{
252 return m_expandOnMouseover;
253}
254
255// static
256void MenuBar::setExpandOnMouseover(bool state)
257{
258 m_expandOnMouseover = state;
259}
260
261//////////////////////////////////////////////////////////////////////
262// MenuItem
263
264MenuItem::MenuItem(const std::string& text)
265 : Widget(kMenuItemWidget)
266{
267 m_highlighted = false;
268 m_submenu = nullptr;
269 m_submenu_menubox = nullptr;
270
271 setText(text);
272 initTheme();
273}
274
275MenuItem::~MenuItem()
276{
277 delete m_submenu;
278}
279
280Menu* MenuBox::getMenu()
281{
282 if (children().empty())
283 return nullptr;
284 else
285 return static_cast<Menu*>(children().front());
286}
287
288MenuBaseData* MenuBox::createBase()
289{
290 m_base.reset(new MenuBaseData);
291 return m_base.get();
292}
293
294Menu* MenuItem::getSubmenu()
295{
296 return m_submenu;
297}
298
299void MenuBox::setMenu(Menu* menu)
300{
301 if (Menu* oldMenu = getMenu())
302 removeChild(oldMenu);
303
304 if (menu) {
305 ASSERT_VALID_WIDGET(menu);
306 addChild(menu);
307 }
308}
309
310void MenuItem::setSubmenu(Menu* menu)
311{
312 if (m_submenu)
313 m_submenu->setOwnerMenuItem(nullptr);
314
315 m_submenu = menu;
316
317 if (m_submenu) {
318 ASSERT_VALID_WIDGET(m_submenu);
319 m_submenu->setOwnerMenuItem(this);
320 }
321}
322
323void MenuItem::openSubmenu()
324{
325 if (auto menu = static_cast<Menu*>(parent()))
326 menu->highlightItem(this, true, true, true);
327}
328
329bool MenuItem::isHighlighted() const
330{
331 return m_highlighted;
332}
333
334void MenuItem::setHighlighted(bool state)
335{
336 m_highlighted = state;
337}
338
339bool MenuItem::hasSubmenu() const
340{
341 return (m_submenu && !m_submenu->children().empty());
342}
343
344void Menu::showPopup(const gfx::Point& pos,
345 Display* parentDisplay)
346{
347 // Generally, when we call showPopup() the menu shouldn't contain a
348 // parent menu-box, because we're filtering kMouseDownMessage to
349 // close the popup automatically when we click outside the menubox.
350 // Anyway there is one specific case were a clicked widget might
351 // call showPopup() when it's clicked the first time, and a second
352 // click could generate a kDoubleClickMessage which is then
353 // converted to kMouseDownMessage to finally call showPopup() again.
354 // In this case, the menu is already in a menubox.
355 if (parent()) {
356 static_cast<MenuBox*>(parent())->cancelMenuLoop();
357 return;
358 }
359
360 // New window and new menu-box
361 MenuBoxWindow window;
362 window.Open.connect([this]{ this->onOpenPopup(); });
363
364 MenuBox* menubox = window.menubox();
365 MenuBaseData* base = menubox->createBase();
366 base->was_clicked = true;
367
368 // Set children
369 menubox->setMenu(this);
370 menubox->startFilteringMouseDown();
371
372 window.remapWindow();
373 fit_bounds(parentDisplay,
374 &window,
375 gfx::Rect(pos, window.size()),
376 [&window, pos](const gfx::Rect& workarea,
377 gfx::Rect& bounds,
378 std::function<gfx::Rect(Widget*)> getWidgetBounds) {
379 choose_side(bounds, workarea, gfx::Rect(bounds.x-1, bounds.y, 1, 1));
380 add_scrollbars_if_needed(&window, workarea, bounds);
381 });
382
383 // Set the focus to the new menubox
384 Manager* manager = Manager::getDefault();
385 manager->setFocus(menubox);
386
387 // Open the window
388 window.openWindowInForeground();
389
390 // Free the keyboard focus if it's in the menu popup, in other case
391 // it means that the user set the focus to other specific widget
392 // before we closed the popup.
393 Widget* focus = manager->getFocus();
394 if (focus && focus->window() == &window)
395 focus->releaseFocus();
396
397 menubox->stopFilteringMouseDown();
398}
399
400Widget* Menu::findItemById(const char* id) const
401{
402 Widget* result = findChild(id);
403 if (result)
404 return result;
405 for (auto child : children()) {
406 if (child->type() == kMenuItemWidget) {
407 if (Menu* submenu = static_cast<MenuItem*>(child)->getSubmenu()) {
408 result = submenu->findItemById(id);
409 if (result)
410 return result;
411 }
412 }
413 }
414 return nullptr;
415}
416
417void Menu::onPaint(PaintEvent& ev)
418{
419 theme()->paintMenu(ev);
420}
421
422void Menu::onResize(ResizeEvent& ev)
423{
424 setBoundsQuietly(ev.bounds());
425
426 Rect cpos = childrenBounds();
427 bool isBar = (parent()->type() == kMenuBarWidget);
428
429 for (auto child : children()) {
430 Size reqSize = child->sizeHint();
431
432 if (isBar)
433 cpos.w = reqSize.w;
434 else
435 cpos.h = reqSize.h;
436
437 child->setBounds(cpos);
438
439 if (isBar)
440 cpos.x += cpos.w;
441 else
442 cpos.y += cpos.h;
443 }
444}
445
446void Menu::onSizeHint(SizeHintEvent& ev)
447{
448 Size size(0, 0);
449 Size reqSize;
450
451 for (auto it=children().begin(),
452 end=children().end();
453 it!=end; ) {
454 auto next = it;
455 ++next;
456
457 reqSize = (*it)->sizeHint();
458
459 if (parent() &&
460 parent()->type() == kMenuBarWidget) {
461 size.w += reqSize.w + ((next != end) ? childSpacing(): 0);
462 size.h = std::max(size.h, reqSize.h);
463 }
464 else {
465 size.w = std::max(size.w, reqSize.w);
466 size.h += reqSize.h + ((next != end) ? childSpacing(): 0);
467 }
468
469 it = next;
470 }
471
472 size.w += border().width();
473 size.h += border().height();
474
475 ev.setSizeHint(size);
476}
477
478bool MenuBox::onProcessMessage(Message* msg)
479{
480 Menu* menu = MenuBox::getMenu();
481
482 switch (msg->type()) {
483
484 case kMouseMoveMessage: {
485 MenuBaseData* base = get_base(this);
486 ASSERT(base);
487 if (!base)
488 break;
489
490 if (!base->was_clicked)
491 break;
492
493 [[fallthrough]];
494 }
495
496 case kMouseDownMessage:
497 case kDoubleClickMessage:
498 if (menu && msg->display()) {
499 ASSERT(menu->parent() == this);
500
501 MenuBaseData* base = get_base(this);
502 ASSERT(base);
503 if (!base)
504 break;
505
506 if (base->is_processing)
507 break;
508
509 const gfx::Point mousePos = static_cast<MouseMessage*>(msg)->position();
510 const gfx::Point screenPos = msg->display()->nativeWindow()->pointToScreen(mousePos);
511
512 // Get the widget below the mouse cursor
513 auto mgr = manager();
514 if (!mgr)
515 break;
516
517 Widget* picked = mgr->pickFromScreenPos(screenPos);
518
519 // Here we catch the filtered messages (menu-bar or the
520 // popuped menu-box) to detect if the user press outside of
521 // the widget
522 if (msg->type() == kMouseDownMessage && m_base != nullptr) {
523 // If one of these conditions are accomplished we have to
524 // close all menus (back to menu-bar or close the popuped
525 // menubox), this is the place where we control if...
526 if (picked == nullptr || // If the button was clicked nowhere
527 picked == this || // If the button was clicked in this menubox
528 // The picked widget isn't from the same tree of menus
529 (get_base_menubox(picked) != this ||
530 (this->type() == kMenuBarWidget &&
531 picked->type() == kMenuWidget))) {
532 // The user click outside all the menu-box/menu-items, close all
533 menu->closeAll();
534
535 // Change to "return false" if you want to send the click
536 // to the window after closing all menus.
537 return true;
538 }
539 }
540
541 if (picked) {
542 if ((picked->type() == kMenuItemWidget) &&
543 !(picked->hasFlags(DISABLED))) {
544 MenuItem* pickedItem = static_cast<MenuItem*>(picked);
545
546 // If the picked menu-item is not highlighted...
547 if (!pickedItem->isHighlighted()) {
548 // In menu-bar always open the submenu, in other popup-menus
549 // open the submenu only if the user does click
550 bool open_submenu =
551 (this->type() == kMenuBarWidget) ||
552 (msg->type() == kMouseDownMessage);
553
554 menu->highlightItem(pickedItem, false, open_submenu, false);
555 }
556 // If the user pressed in a highlighted menu-item (maybe
557 // the user was waiting for the timer to open the
558 // submenu...)
559 else if (msg->type() == kMouseDownMessage &&
560 pickedItem->hasSubmenu()) {
561 pickedItem->stopTimer();
562
563 // If the submenu is closed, open it
564 if (!pickedItem->hasSubmenuOpened())
565 pickedItem->openSubmenu(false);
566 else if (pickedItem->inBar()) {
567 pickedItem->getSubmenu()->closeAll();
568
569 // Set this flag to false so the submenu is not open
570 // again on kMouseMoveMessage.
571 base->was_clicked = false;
572 }
573 }
574 }
575 else if (!base->was_clicked) {
576 menu->unhighlightItem();
577 }
578 }
579 }
580 break;
581
582 case kMouseLeaveMessage:
583 if (menu) {
584 MenuBaseData* base = get_base(this);
585 ASSERT(base);
586 if (!base)
587 break;
588
589 if (base->is_processing)
590 break;
591
592 MenuItem* highlight = menu->getHighlightedItem();
593 if (highlight && !highlight->hasSubmenuOpened())
594 menu->unhighlightItem();
595 }
596 break;
597
598 case kMouseUpMessage:
599 if (menu) {
600 MenuBaseData* base = get_base(this);
601 ASSERT(base);
602 if (!base)
603 break;
604
605 if (base->is_processing)
606 break;
607
608 // The item is highlighted and not opened (and the timer to open the submenu is stopped)
609 MenuItem* highlight = menu->getHighlightedItem();
610 if (highlight &&
611 !highlight->hasSubmenuOpened() &&
612 highlight->m_submenu_timer == nullptr) {
613 menu->closeAll();
614 highlight->executeClick();
615 }
616 }
617 break;
618
619 case kKeyDownMessage:
620 if (menu) {
621 MenuItem* selected;
622
623 MenuBaseData* base = get_base(this);
624 ASSERT(base);
625 if (!base)
626 break;
627
628 if (base->is_processing)
629 break;
630
631 base->was_clicked = false;
632
633 // Check for ALT+some underlined letter
634 if (((this->type() == kMenuBoxWidget) && (msg->modifiers() == kKeyNoneModifier || // <-- Inside menu-boxes we can use letters without Alt modifier pressed
635 msg->modifiers() == kKeyAltModifier)) ||
636 ((this->type() == kMenuBarWidget) && (msg->modifiers() == kKeyAltModifier) &&
637 static_cast<MenuBar*>(this)->processTopLevelShortcuts())) {
638 auto keymsg = static_cast<KeyMessage*>(msg);
639 selected = check_for_letter(menu, keymsg);
640 if (selected) {
641 menu->highlightItem(selected, true, true, true);
642 return true;
643 }
644 }
645
646 // Highlight movement with keyboard
647 if (this->hasFocus()) {
648 MenuItem* highlight = menu->getHighlightedItem();
649 MenuItem* child_with_submenu_opened = nullptr;
650 bool used = false;
651
652 // Search a child with highlight or the submenu opened
653 for (auto child : menu->children()) {
654 if (child->type() != kMenuItemWidget)
655 continue;
656
657 if (static_cast<MenuItem*>(child)->hasSubmenuOpened())
658 child_with_submenu_opened = static_cast<MenuItem*>(child);
659 }
660
661 if (!highlight && child_with_submenu_opened)
662 highlight = child_with_submenu_opened;
663
664 switch (static_cast<KeyMessage*>(msg)->scancode()) {
665
666 case kKeyEsc:
667 // In menu-bar
668 if (this->type() == kMenuBarWidget) {
669 if (highlight) {
670 cancelMenuLoop();
671 used = true;
672 }
673 }
674 // In menu-boxes
675 else {
676 if (child_with_submenu_opened) {
677 child_with_submenu_opened->closeSubmenu(true);
678 used = true;
679 }
680 // Go to parent
681 else if (menu->m_menuitem) {
682 // Just retrogress one parent-level
683 menu->m_menuitem->closeSubmenu(true);
684 used = true;
685 }
686 }
687 break;
688
689 case kKeyUp:
690 // In menu-bar
691 if (this->type() == kMenuBarWidget) {
692 if (child_with_submenu_opened)
693 child_with_submenu_opened->closeSubmenu(true);
694 }
695 // In menu-boxes
696 else {
697 // Go to previous
698 highlight = find_previtem(menu, highlight);
699 menu->highlightItem(highlight, false, false, false);
700 }
701 used = true;
702 break;
703
704 case kKeyDown:
705 // In menu-bar
706 if (this->type() == kMenuBarWidget) {
707 // Select the active menu
708 menu->highlightItem(highlight, true, true, true);
709 }
710 // In menu-boxes
711 else {
712 // Go to next
713 highlight = find_nextitem(menu, highlight);
714 menu->highlightItem(highlight, false, false, false);
715 }
716 used = true;
717 break;
718
719 case kKeyLeft:
720 // In menu-bar
721 if (this->type() == kMenuBarWidget) {
722 // Go to previous
723 highlight = find_previtem(menu, highlight);
724 menu->highlightItem(highlight, false, false, false);
725 }
726 // In menu-boxes
727 else {
728 // Go to parent
729 if (menu->m_menuitem) {
730 Widget* parent = menu->m_menuitem->parent()->parent();
731
732 // Go to the previous item in the parent
733
734 // If the parent is the menu-bar
735 if (parent->type() == kMenuBarWidget) {
736 menu = static_cast<MenuBar*>(parent)->getMenu();
737 MenuItem* menuitem = find_previtem(menu, menu->getHighlightedItem());
738
739 // Go to previous item in the parent
740 menu->highlightItem(menuitem, false, true, true);
741 }
742 // If the parent isn't the menu-bar
743 else {
744 // Just retrogress one parent-level
745 menu->m_menuitem->closeSubmenu(true);
746 }
747 }
748 }
749 used = true;
750 break;
751
752 case kKeyRight:
753 // In menu-bar
754 if (this->type() == kMenuBarWidget) {
755 // Go to next
756 highlight = find_nextitem(menu, highlight);
757 menu->highlightItem(highlight, false, false, false);
758 }
759 // In menu-boxes
760 else {
761 // Enter in sub-menu
762 if (highlight && highlight->hasSubmenu()) {
763 menu->highlightItem(highlight, true, true, true);
764 }
765 // Go to parent
766 else if (menu->m_menuitem) {
767 // Get the root menu
768 MenuBox* root = get_base_menubox(this);
769 ASSERT(root);
770 if (!root)
771 break;
772 menu = root->getMenu();
773
774 // Go to the next item in the root
775 MenuItem* menuitem = find_nextitem(menu, menu->getHighlightedItem());
776
777 // Open the sub-menu
778 menu->highlightItem(menuitem, false, true, true);
779 }
780 }
781 used = true;
782 break;
783
784 case kKeyEnter:
785 case kKeyEnterPad:
786 if (highlight)
787 menu->highlightItem(highlight, true, true, true);
788 used = true;
789 break;
790 }
791
792 // Return true if we've already consumed the key.
793 if (used) {
794 return true;
795 }
796 // If the user presses the ALT key we close everything.
797 else if (static_cast<KeyMessage*>(msg)->scancode() == kKeyAlt) {
798 cancelMenuLoop();
799 }
800 }
801 }
802 break;
803
804 case kMouseWheelMessage: {
805 View* view = View::getView(this);
806 if (view) {
807 auto mouseMsg = static_cast<MouseMessage*>(msg);
808 gfx::Point scroll = view->viewScroll();
809
810 if (mouseMsg->preciseWheel())
811 scroll += mouseMsg->wheelDelta();
812 else
813 scroll += mouseMsg->wheelDelta() * textHeight()*3;
814
815 view->setViewScroll(scroll);
816 }
817 break;
818 }
819
820 default:
821 if (msg->type() == kClosePopupMessage) {
822 window()->closeWindow(nullptr);
823 }
824 break;
825
826 }
827
828 return Widget::onProcessMessage(msg);
829}
830
831void MenuBox::onResize(ResizeEvent& ev)
832{
833 setBoundsQuietly(ev.bounds());
834
835 if (Menu* menu = getMenu())
836 menu->setBounds(childrenBounds());
837}
838
839void MenuBox::onSizeHint(SizeHintEvent& ev)
840{
841 Size size(0, 0);
842
843 if (Menu* menu = getMenu())
844 size = menu->sizeHint();
845
846 size.w += border().width();
847 size.h += border().height();
848
849 ev.setSizeHint(size);
850}
851
852bool MenuItem::onProcessMessage(Message* msg)
853{
854 switch (msg->type()) {
855
856 case kMouseEnterMessage:
857 // TODO theme specific!!
858 invalidate();
859
860 // When a menu item receives the mouse, start a timer to open the submenu...
861 if (isEnabled() && hasSubmenu()) {
862 // Start the timer to open the submenu...
863 if (!inBar() || MenuBar::expandOnMouseover())
864 startTimer();
865 }
866 break;
867
868 case kMouseLeaveMessage:
869 // Unhighlight this item if its submenu isn't opened
870 if (isHighlighted() &&
871 !m_submenu_menubox &&
872 parent() &&
873 parent()->type() == kMenuWidget) {
874 static_cast<Menu*>(parent())->unhighlightItem();
875 }
876
877 // TODO theme specific!!
878 invalidate();
879
880 // Stop timer to open the popup
881 if (m_submenu_timer)
882 m_submenu_timer.reset();
883 break;
884
885 default:
886 if (msg->type() == kOpenMessage) {
887 validateItem();
888 }
889 else if (msg->type() == kOpenMenuItemMessage) {
890 validateItem();
891
892 MenuBaseData* base = get_base(this);
893 ASSERT(base);
894 if (!base)
895 break;
896
897 bool select_first = static_cast<OpenMenuItemMessage*>(msg)->select_first();
898
899 ASSERT(base->is_processing);
900 ASSERT(hasSubmenu());
901
902 // New window that will be automatically deleted
903 auto window = new MenuBoxWindow(this);
904 window->Close.connect([window]{
905 window->deferDelete();
906 });
907
908 MenuBox* menubox = window->menubox();
909 m_submenu_menubox = menubox;
910 menubox->setMenu(m_submenu);
911
912 window->remapWindow();
913 fit_bounds(
914 display(), window, window->bounds(),
915 [this, window](const gfx::Rect& workarea,
916 gfx::Rect& bounds,
917 std::function<gfx::Rect(Widget*)> getWidgetBounds){
918 const gfx::Rect itemBounds = getWidgetBounds(this);
919 if (inBar()) {
920 bounds.x = std::clamp(itemBounds.x, workarea.x, std::max(workarea.x, workarea.x2()-bounds.w));
921 bounds.y = std::max(workarea.y, itemBounds.y2());
922 }
923 else {
924 int scale = guiscale();
925 if (get_multiple_displays())
926 scale = display()->scale();
927
928 const gfx::Rect parentBounds = getWidgetBounds(this->window());
929 bounds.y = itemBounds.y-3*scale;
930 choose_side(bounds, workarea, parentBounds);
931 }
932
933 add_scrollbars_if_needed(window, workarea, bounds);
934 });
935
936 // Setup the highlight of the new menubox
937 if (select_first) {
938 // Select the first child
939 MenuItem* first_child = nullptr;
940
941 for (auto child : m_submenu->children()) {
942 if (child->type() != kMenuItemWidget)
943 continue;
944
945 if (child->isEnabled()) {
946 first_child = static_cast<MenuItem*>(child);
947 break;
948 }
949 }
950
951 if (first_child)
952 m_submenu->highlightItem(first_child, false, false, false);
953 else
954 m_submenu->unhighlightItem();
955 }
956 else
957 m_submenu->unhighlightItem();
958
959 // Run in background
960 window->openWindow();
961
962 base->is_processing = false;
963
964 return true;
965 }
966 else if (msg->type() == kCloseMenuItemMessage) {
967 bool last_of_close_chain = static_cast<CloseMenuItemMessage*>(msg)->last_of_close_chain();
968 MenuBaseData* base = get_base(this);
969 ASSERT(base);
970 if (!base)
971 break;
972
973 ASSERT(base->is_processing);
974
975 ASSERT(m_submenu_menubox);
976 Window* window = m_submenu_menubox->window();
977 ASSERT(window && window->type() == kWindowWidget);
978 m_submenu_menubox = nullptr;
979
980 // Destroy the window
981 window->closeWindow(nullptr);
982
983 // Set the focus to this menu-box of this menu-item
984 if (base->close_all)
985 manager()->freeFocus();
986 else
987 manager()->setFocus(this->parent()->parent());
988
989 // Do not call "delete window" here, because it
990 // (MenuBoxWindow) will be deferDelete() on
991 // kCloseMessage.
992
993 if (last_of_close_chain) {
994 base->close_all = false;
995 base->is_processing = false;
996 }
997
998 // Stop timer to open the popup
999 stopTimer();
1000 return true;
1001 }
1002 else if (msg->type() == kExecuteMenuItemMessage) {
1003 onClick();
1004 return true;
1005 }
1006 break;
1007
1008 case kTimerMessage:
1009 if (static_cast<TimerMessage*>(msg)->timer() == m_submenu_timer.get()) {
1010 MenuBaseData* base = get_base(this);
1011 ASSERT(base);
1012 if (!base)
1013 break;
1014
1015 ASSERT(hasSubmenu());
1016
1017 // Stop timer to open the popup
1018 stopTimer();
1019
1020 // If the submenu is closed, and we are not processing messages, open it
1021 if (m_submenu_menubox == nullptr && !base->is_processing)
1022 openSubmenu(false);
1023 }
1024 break;
1025
1026 }
1027
1028 return Widget::onProcessMessage(msg);
1029}
1030
1031void MenuItem::onInitTheme(InitThemeEvent& ev)
1032{
1033 if (m_submenu)
1034 m_submenu->initTheme();
1035 if (m_submenu_menubox)
1036 m_submenu_menubox->initTheme();
1037 Widget::onInitTheme(ev);
1038}
1039
1040void MenuItem::onPaint(PaintEvent& ev)
1041{
1042 theme()->paintMenuItem(ev);
1043}
1044
1045void MenuItem::onClick()
1046{
1047 // Fire new Click() signal.
1048 Click();
1049}
1050
1051void MenuItem::onValidate()
1052{
1053 // Here the user can customize the automatic validation of the menu
1054 // item before it's shown.
1055}
1056
1057void MenuItem::onSizeHint(SizeHintEvent& ev)
1058{
1059 Size size(0, 0);
1060
1061 if (hasText()) {
1062 size.w =
1063 + textWidth()
1064 + (inBar() ? childSpacing()/4: childSpacing())
1065 + border().width();
1066
1067 size.h =
1068 + textHeight()
1069 + border().height();
1070 }
1071
1072 ev.setSizeHint(size);
1073}
1074
1075// Climbs the hierarchy of menus to get the most-top menubox.
1076static MenuBox* get_base_menubox(Widget* widget)
1077{
1078 while (widget) {
1079 ASSERT_VALID_WIDGET(widget);
1080
1081 // We are in a menubox
1082 if (widget->type() == kMenuBoxWidget ||
1083 widget->type() == kMenuBarWidget) {
1084 if (static_cast<MenuBox*>(widget)->getBase()) {
1085 return static_cast<MenuBox*>(widget);
1086 }
1087 else {
1088 Menu* menu = static_cast<MenuBox*>(widget)->getMenu();
1089
1090 ASSERT(menu != nullptr);
1091 ASSERT(menu->getOwnerMenuItem() != nullptr);
1092
1093 // We have received a crash report where the "menu" variable
1094 // can be nullptr in the kMouseDownMessage message processing
1095 // from MenuBox::onProcessMessage().
1096 if (menu == nullptr)
1097 return nullptr;
1098
1099 widget = menu->getOwnerMenuItem();
1100 }
1101 }
1102 // This is useful for menuboxes inside a viewport (so we can scroll a viewport clicking scrollbars)
1103 else if (widget->type() == kViewScrollbarWidget &&
1104 widget->parent() &&
1105 widget->parent()->type() == kViewWidget &&
1106 static_cast<View*>(widget->parent())->attachedWidget() &&
1107 static_cast<View*>(widget->parent())->attachedWidget()->type() == kMenuBoxWidget) {
1108 widget = static_cast<View*>(widget->parent())->attachedWidget();
1109 }
1110 else {
1111 widget = widget->parent();
1112 }
1113 }
1114 return nullptr;
1115}
1116
1117static MenuBaseData* get_base(Widget* widget)
1118{
1119 MenuBox* menubox = get_base_menubox(widget);
1120 ASSERT(menubox);
1121 if (menubox)
1122 return menubox->getBase();
1123 else
1124 return nullptr;
1125}
1126
1127MenuItem* Menu::getHighlightedItem()
1128{
1129 for (auto child : children()) {
1130 if (child->type() != kMenuItemWidget)
1131 continue;
1132
1133 MenuItem* menuitem = static_cast<MenuItem*>(child);
1134 if (menuitem->isHighlighted())
1135 return menuitem;
1136 }
1137 return nullptr;
1138}
1139
1140void Menu::highlightItem(MenuItem* menuitem, bool click, bool open_submenu, bool select_first_child)
1141{
1142 // Find the menuitem with the highlight
1143 for (auto child : children()) {
1144 if (child->type() != kMenuItemWidget)
1145 continue;
1146
1147 if (child != menuitem) {
1148 // Is it?
1149 if (static_cast<MenuItem*>(child)->isHighlighted()) {
1150 static_cast<MenuItem*>(child)->setHighlighted(false);
1151 child->invalidate();
1152 }
1153 }
1154 }
1155
1156 if (menuitem) {
1157 if (!menuitem->isHighlighted()) {
1158 menuitem->setHighlighted(true);
1159 menuitem->invalidate();
1160
1161 // Scroll
1162 View* view = View::getView(menuitem->parent()->parent());
1163 if (view) {
1164 gfx::Rect itemBounds = menuitem->bounds();
1165 itemBounds.y -= menuitem->parent()->origin().y;
1166
1167 gfx::Point scroll = view->viewScroll();
1168 gfx::Size visSize = view->visibleSize();
1169
1170 if (itemBounds.y < scroll.y)
1171 scroll.y = itemBounds.y;
1172 else if (itemBounds.y2() > scroll.y+visSize.h)
1173 scroll.y = itemBounds.y2()-visSize.h;
1174
1175 view->setViewScroll(scroll);
1176 }
1177 }
1178
1179 // Highlight parents
1180 if (getOwnerMenuItem() != nullptr) {
1181 static_cast<Menu*>(getOwnerMenuItem()->parent())
1182 ->highlightItem(getOwnerMenuItem(), false, false, false);
1183 }
1184
1185 // Open submenu of the menitem
1186 if (menuitem->hasSubmenu()) {
1187 if (open_submenu) {
1188 // If the submenu is closed, open it
1189 if (!menuitem->hasSubmenuOpened())
1190 menuitem->openSubmenu(select_first_child);
1191
1192 // The mouse was clicked
1193 MenuBaseData* base = get_base(menuitem);
1194 ASSERT(base);
1195 if (base)
1196 base->was_clicked = true;
1197 }
1198 }
1199 // Execute menuitem action
1200 else if (click) {
1201 closeAll();
1202 menuitem->executeClick();
1203 }
1204 }
1205}
1206
1207void Menu::unhighlightItem()
1208{
1209 highlightItem(nullptr, false, false, false);
1210}
1211
1212bool MenuItem::inBar() const
1213{
1214 return
1215 (parent() &&
1216 parent()->parent() &&
1217 parent()->parent()->type() == kMenuBarWidget);
1218}
1219
1220void MenuItem::openSubmenu(bool select_first)
1221{
1222 Widget* menu;
1223 Message* msg;
1224
1225 ASSERT(hasSubmenu());
1226
1227 menu = this->parent();
1228
1229 // The menu item is already opened?
1230 ASSERT(m_submenu_menubox == nullptr);
1231
1232 ASSERT_VALID_WIDGET(menu);
1233
1234 // Close all siblings of 'menuitem'
1235 if (menu->parent()) {
1236 for (auto child : menu->children()) {
1237 if (child->type() != kMenuItemWidget)
1238 continue;
1239
1240 MenuItem* childMenuItem = static_cast<MenuItem*>(child);
1241 if (childMenuItem != this && childMenuItem->hasSubmenuOpened()) {
1242 childMenuItem->closeSubmenu(false);
1243 }
1244 }
1245 }
1246
1247 msg = new OpenMenuItemMessage(select_first);
1248 msg->setRecipient(this);
1249 Manager::getDefault()->enqueueMessage(msg);
1250
1251 // Get the 'base'
1252 MenuBaseData* base = get_base(this);
1253 ASSERT(base);
1254 if (!base)
1255 return;
1256 ASSERT(base->is_processing == false);
1257
1258 // Reset flags
1259 base->close_all = false;
1260 base->is_processing = true;
1261
1262 // We need to add a filter of the kMouseDownMessage to intercept
1263 // clicks outside the menu (and close all the hierarchy in that
1264 // case); the widget to intercept messages is the base menu-bar or
1265 // popuped menu-box
1266 MenuBox* base_menubox = get_base_menubox(this);
1267 if (base_menubox)
1268 base_menubox->startFilteringMouseDown();
1269}
1270
1271void MenuItem::closeSubmenu(bool last_of_close_chain)
1272{
1273 Widget* menu;
1274 Message* msg;
1275 MenuBaseData* base;
1276
1277 ASSERT(m_submenu_menubox != nullptr);
1278
1279 // First: recursively close the children
1280 menu = m_submenu_menubox->getMenu();
1281 ASSERT(menu != nullptr);
1282
1283 for (auto child : menu->children()) {
1284 if (child->type() != kMenuItemWidget)
1285 continue;
1286
1287 if (static_cast<MenuItem*>(child)->hasSubmenuOpened())
1288 static_cast<MenuItem*>(child)->closeSubmenu(false);
1289 }
1290
1291 // Second: now we can close the 'menuitem'
1292 msg = new CloseMenuItemMessage(last_of_close_chain);
1293 msg->setRecipient(this);
1294 Manager::getDefault()->enqueueMessage(msg);
1295
1296 // If this is the last message of the chain, here we have the
1297 // responsibility to set is_processing flag to true.
1298 if (last_of_close_chain) {
1299 // Get the 'base'
1300 base = get_base(this);
1301 ASSERT(base);
1302 if (base) {
1303 ASSERT(base->is_processing == false);
1304
1305 // Start processing
1306 base->is_processing = true;
1307 }
1308 }
1309}
1310
1311void MenuItem::startTimer()
1312{
1313 if (m_submenu_timer == nullptr)
1314 m_submenu_timer.reset(new Timer(kTimeoutToOpenSubmenu, this));
1315
1316 m_submenu_timer->start();
1317}
1318
1319void MenuItem::stopTimer()
1320{
1321 // Stop timer to open the popup
1322 if (m_submenu_timer)
1323 m_submenu_timer.reset();
1324}
1325
1326void Menu::closeAll()
1327{
1328 Menu* menu = this;
1329 MenuItem* menuitem = nullptr;
1330 while (menu->m_menuitem) {
1331 menuitem = menu->m_menuitem;
1332 menu = static_cast<Menu*>(menuitem->parent());
1333 }
1334
1335 MenuBox* base_menubox = get_base_menubox(menu->parent());
1336 ASSERT(base_menubox);
1337 if (!base_menubox)
1338 return;
1339
1340 MenuBaseData* base = base_menubox->getBase();
1341 ASSERT(base);
1342 if (!base)
1343 return;
1344
1345 base->close_all = true;
1346 base->was_clicked = false;
1347 base_menubox->stopFilteringMouseDown();
1348
1349 menu->unhighlightItem();
1350
1351 if (menuitem != nullptr) {
1352 if (menuitem->hasSubmenuOpened())
1353 menuitem->closeSubmenu(true);
1354 }
1355 else {
1356 for (auto child : menu->children()) {
1357 if (child->type() != kMenuItemWidget)
1358 continue;
1359
1360 menuitem = static_cast<MenuItem*>(child);
1361 if (menuitem->hasSubmenuOpened())
1362 menuitem->closeSubmenu(true);
1363 }
1364 }
1365
1366 // For popuped menus
1367 if (base_menubox->type() == kMenuBoxWidget)
1368 base_menubox->closePopup();
1369}
1370
1371void MenuBox::closePopup()
1372{
1373 Message* msg = new Message(kClosePopupMessage);
1374 msg->setRecipient(this);
1375 Manager::getDefault()->enqueueMessage(msg);
1376}
1377
1378void MenuBox::startFilteringMouseDown()
1379{
1380 if (m_base && !m_base->is_filtering) {
1381 m_base->is_filtering = true;
1382 Manager::getDefault()->addMessageFilter(kMouseDownMessage, this);
1383 Manager::getDefault()->addMessageFilter(kDoubleClickMessage, this);
1384 }
1385}
1386
1387void MenuBox::stopFilteringMouseDown()
1388{
1389 if (m_base && m_base->is_filtering) {
1390 m_base->is_filtering = false;
1391 Manager::getDefault()->removeMessageFilter(kMouseDownMessage, this);
1392 Manager::getDefault()->removeMessageFilter(kDoubleClickMessage, this);
1393 }
1394}
1395
1396void MenuBox::cancelMenuLoop()
1397{
1398 Menu* menu = getMenu();
1399 if (!menu)
1400 return;
1401
1402 MenuBaseData* base = get_base(this);
1403 ASSERT(base);
1404 if (!base)
1405 return;
1406
1407 // Do not close the popup menus if we're already processing
1408 // open/close popup messages.
1409 if (base->is_processing)
1410 return;
1411
1412 menu->closeAll();
1413
1414 // Lost focus
1415 Manager::getDefault()->freeFocus();
1416}
1417
1418void MenuItem::executeClick()
1419{
1420 // Send the message
1421 Message* msg = new Message(kExecuteMenuItemMessage);
1422 msg->setRecipient(this);
1423 Manager::getDefault()->enqueueMessage(msg);
1424}
1425
1426void MenuItem::validateItem()
1427{
1428 onValidate();
1429}
1430
1431static MenuItem* check_for_letter(Menu* menu, const KeyMessage* keymsg)
1432{
1433 for (auto child : menu->children()) {
1434 if (child->type() != kMenuItemWidget)
1435 continue;
1436
1437 MenuItem* menuitem = static_cast<MenuItem*>(child);
1438 if (menuitem->isMnemonicPressed(keymsg))
1439 return menuitem;
1440 }
1441 return nullptr;
1442}
1443
1444// Finds the next item of `menuitem', if `menuitem' is nullptr searchs
1445// from the first item in `menu'
1446static MenuItem* find_nextitem(Menu* menu, MenuItem* menuitem)
1447{
1448 WidgetsList::const_iterator begin = menu->children().begin();
1449 WidgetsList::const_iterator it, end = menu->children().end();
1450
1451 if (menuitem) {
1452 it = std::find(begin, end, menuitem);
1453 if (it != end)
1454 ++it;
1455 }
1456 else
1457 it = begin;
1458
1459 for (; it != end; ++it) {
1460 Widget* nextitem = *it;
1461 if ((nextitem->type() == kMenuItemWidget) && nextitem->isEnabled())
1462 return static_cast<MenuItem*>(nextitem);
1463 }
1464
1465 if (menuitem)
1466 return find_nextitem(menu, nullptr);
1467 else
1468 return nullptr;
1469}
1470
1471static MenuItem* find_previtem(Menu* menu, MenuItem* menuitem)
1472{
1473 WidgetsList::const_reverse_iterator begin = menu->children().rbegin();
1474 WidgetsList::const_reverse_iterator it, end = menu->children().rend();
1475
1476 if (menuitem) {
1477 it = std::find(begin, end, menuitem);
1478 if (it != end)
1479 ++it;
1480 }
1481 else
1482 it = begin;
1483
1484 for (; it != end; ++it) {
1485 Widget* nextitem = *it;
1486 if ((nextitem->type() == kMenuItemWidget) && nextitem->isEnabled())
1487 return static_cast<MenuItem*>(nextitem);
1488 }
1489
1490 if (menuitem)
1491 return find_previtem(menu, nullptr);
1492 else
1493 return nullptr;
1494}
1495
1496//////////////////////////////////////////////////////////////////////
1497// MenuBoxWindow
1498
1499MenuBoxWindow::MenuBoxWindow(MenuItem* menuitem)
1500 : Window(WithoutTitleBar, "")
1501 , m_menuitem(menuitem)
1502{
1503 setMoveable(false); // Can't move the window
1504 setSizeable(false); // Can't resize the window
1505
1506 m_menubox.setFocusMagnet(true);
1507
1508 addChild(&m_menubox);
1509}
1510
1511MenuBoxWindow::~MenuBoxWindow()
1512{
1513 // The menu of the menubox should already be nullptr because it
1514 // was reset in kCloseMessage.
1515 ASSERT(m_menubox.getMenu() == nullptr);
1516 m_menubox.setMenu(nullptr);
1517
1518 // This can fail in case that add_scrollbars_if_needed() replaced
1519 // the MenuBox widget with a View, and now the MenuBox is inside the
1520 // viewport.
1521 if (hasChild(&m_menubox)) {
1522 removeChild(&m_menubox);
1523 }
1524 else {
1525 ASSERT(firstChild() != nullptr &&
1526 firstChild()->type() == kViewWidget);
1527 }
1528}
1529
1530bool MenuBoxWindow::onProcessMessage(Message* msg)
1531{
1532 switch (msg->type()) {
1533
1534 case kCloseMessage:
1535 if (m_menuitem) {
1536 MenuBaseData* base = get_base(m_menuitem);
1537
1538 // If this window was closed using the OS close button
1539 // (e.g. on Linux we can Super key+right click to show the
1540 // popup menu and close the window)
1541 if (base && !base->is_processing) {
1542 if (m_menuitem->hasSubmenuOpened())
1543 m_menuitem->closeSubmenu(true);
1544 }
1545 }
1546
1547 // Fetch the "menu" to avoid destroy it with 'delete'.
1548 m_menubox.setMenu(nullptr);
1549 break;
1550
1551 }
1552 return Window::onProcessMessage(msg);
1553}
1554
1555} // namespace ui
1556