1 // Written in the D programming language.
2 
3 /**
4 This module contains menu widgets implementation.
5 
6 MenuItem - menu item properties container - to hold hierarchy of menu.
7 MainMenu - main menu widget
8 PopupMenu - popup menu widget
9 
10 Synopsis:
11 
12 ----
13 import dlangui.widgets.popup;
14 
15 ----
16 
17 Copyright: Vadim Lopatin, 2014
18 License:   Boost License 1.0
19 Authors:   Vadim Lopatin, coolreader.org@gmail.com
20 */
21 module dlangui.widgets.menu;
22 
23 import dlangui.core.events;
24 import dlangui.widgets.controls;
25 import dlangui.widgets.layouts;
26 import dlangui.widgets.lists;
27 import dlangui.widgets.popup;
28 
29 // define DebugMenus for menu debug messages
30 //debug = DebugMenus;
31 
32 /// menu item type
33 enum MenuItemType {
34     /// normal menu item
35     Normal,
36     /// menu item - checkbox
37     Check,
38     /// menu item - radio button
39     Radio,
40     /// menu separator (horizontal line)
41     Separator,
42     /// submenu - contains child items
43     Submenu
44 }
45 
46 /// interface to handle menu item click
47 interface MenuItemClickHandler {
48     bool onMenuItemClick(MenuItem item);
49 }
50 
51 /// interface to handle menu item action
52 interface MenuItemActionHandler {
53     bool onMenuItemAction(const Action action);
54 }
55 
56 
57 /// menu item properties
58 class MenuItem {
59     protected bool _checked;
60     protected bool _enabled;
61     protected MenuItemType _type = MenuItemType.Normal;
62     protected Action _action;
63     protected MenuItem[] _subitems;
64     protected MenuItem _parent;
65     /// handle menu item click (parameter is MenuItem)
66     Signal!MenuItemClickHandler menuItemClick;
67     /// handle menu item click action (parameter is Action)
68     Signal!MenuItemActionHandler menuItemAction;
69     /// item action id, 0 if no action
70     @property int id() const { return _action is null ? 0 : _action.id; }
71     /// returns count of submenu items
72     @property int subitemCount() {
73         return cast(int)_subitems.length;
74     }
75     /// returns subitem index for item, -1 if item is not direct subitem of this
76     @property int subitemIndex(MenuItem item) {
77         for (int i = 0; i < _subitems.length; i++)
78             if (_subitems[i] is item)
79                 return i;
80         return -1;
81     }
82     /// returns submenu item by index
83     MenuItem subitem(int index) {
84         return _subitems[index];
85     }
86 
87     /// map key to action
88     Action findKeyAction(uint keyCode, uint flags) {
89         if (_action) {
90             if (_action.checkAccelerator(keyCode, flags))
91                 return _action;
92         }
93         for (int i = 0; i < subitemCount; i++) {
94             Action a = subitem(i).findKeyAction(keyCode, flags);
95             if (a)
96                 return a;
97         }
98         return null;
99     }
100 
101     @property MenuItemType type() const {
102         if (id == SEPARATOR_ACTION_ID)
103             return MenuItemType.Separator;
104         if (_subitems.length > 0) // if there are children, force type to Submenu
105             return MenuItemType.Submenu;
106         return _type; 
107     }
108 
109     /// set new MenuItemType
110     @property MenuItem type(MenuItemType type) {
111         _type = type;
112         return this; 
113     }
114 
115     /// get check for checkbox or radio button item
116     @property bool checked() {
117         //if (_checked) {
118         //    Log.d("Menu item is checked");
119         //    return true;
120         //}
121         return _checked;
122     }
123     /// check radio button with specified index, uncheck other radio buttons in group (group consists of sequence of radio button items; other item type - end of group)
124     protected void checkRadioButton(int index) {
125         // find bounds of group
126         int start = index;
127         int end = index;
128         for (; start > 0 && _subitems[start - 1].type == MenuItemType.Radio; start--) {
129             // do nothing
130         }
131         for (; end < _subitems.length - 1 && _subitems[end + 1].type == MenuItemType.Radio; end++) {
132             // do nothing
133         }
134         // check item with specified index, uncheck others
135         for (int i = start; i <= end; i++)
136             _subitems[i]._checked = (i == index);
137     }
138     /// set check for checkbox or radio button item
139     @property MenuItem checked(bool flg) {
140         if (_checked == flg)
141             return this;
142         if (_action)
143             _action.checked = flg;
144         _checked = flg;
145         if (flg && _parent && type == MenuItemType.Radio) {
146             int index = _parent.subitemIndex(this);
147             if (index >= 0) {
148                 _parent.checkRadioButton(index);
149             }
150         }
151         return this;
152     }
153     
154     /// get hotkey character from label (e.g. 'F' for item labeled "&File"), 0 if no hotkey
155     dchar getHotkey() {
156         static import std.uni;
157         dstring s = label;
158         dchar ch = 0;
159         for (int i = 0; i < s.length - 1; i++) {
160             if (s[i] == '&') {
161                 ch = s[i + 1];
162                 break;
163             }
164         }
165         return std.uni.toUpper(ch);
166     }
167 
168     /// find subitem by hotkey character, returns subitem index, -1 if not found
169     int findSubitemByHotkey(dchar ch) {
170         static import std.uni;
171         if (!ch)
172             return -1;
173         ch = std.uni.toUpper(ch);
174         for (int i = 0; i < _subitems.length; i++) {
175             if (_subitems[i].getHotkey() == ch)
176                 return i;
177         }
178         return -1;
179     }
180 
181     /// find subitem by hotkey character, returns subitem index, -1 if not found
182     MenuItem findSubitemByHotkeyRecursive(dchar ch) {
183         static import std.uni;
184         if (!ch)
185             return null;
186         ch = std.uni.toUpper(ch);
187         for (int i = 0; i < _subitems.length; i++) {
188             if (_subitems[i].getHotkey() == ch)
189                 return _subitems[i];
190         }
191         for (int i = 0; i < _subitems.length; i++) {
192             MenuItem res = _subitems[i].findSubitemByHotkeyRecursive(ch);
193             if (res)
194                 return res;
195         }
196         return null;
197     }
198 
199     /// Add separator item
200     MenuItem addSeparator() {
201         return add(new Action(SEPARATOR_ACTION_ID));
202     }
203 
204     /// adds submenu item
205     MenuItem add(MenuItem subitem) {
206         _subitems ~= subitem;
207         subitem._parent = this;
208         return this;
209     }
210     /// adds submenu item(s) from one or more actions (will return item for last action)
211     MenuItem add(Action[] subitemActions...) {
212         MenuItem res = null;
213         foreach(subitemAction; subitemActions) {
214             res = add(new MenuItem(subitemAction));
215         }
216         return res;
217     }
218     /// adds submenu item(s) from one or more actions (will return item for last action)
219     MenuItem add(const Action[] subitemActions...) {
220         MenuItem res = null;
221         foreach(subitemAction; subitemActions) {
222             res = add(new MenuItem(subitemAction));
223         }
224         return res;
225     }
226     /// returns text description for first accelerator of action; null if no accelerators
227     @property dstring acceleratorText() {
228         if (!_action)
229             return null;
230         return _action.acceleratorText;
231     }
232     /// returns true if item is submenu (contains subitems)
233     @property bool isSubmenu() {
234         return _subitems.length > 0;
235     }
236     /// returns item label
237     @property UIString label() {
238         return _action !is null ? _action.labelValue : UIString("", null);
239     }
240     /// returns item action
241     @property const(Action) action() const { return _action; }
242     /// sets item action
243     @property MenuItem action(Action a) { _action = a; return this; }
244 
245     /// menu item Enabled flag
246     @property bool enabled() { return _enabled && type != MenuItemType.Separator; }
247     /// menu item Enabled flag
248     @property MenuItem enabled(bool enabled) {
249         _enabled = enabled;
250         return this;
251     }
252 
253     /// handle menu item click
254     Signal!(void, MenuItem) onMenuItem;
255     /// prepare for opening of submenu, return true if opening is allowed
256     Signal!(bool, MenuItem) openingSubmenu;
257 
258     /// call to update state for action (if action is assigned for widget)
259     void updateActionState(Widget w) {
260         //import dlangui.widgets.editors;
261         if (_action) {
262             //if (_action.id == EditorActions.Copy) {
263             //    Log.d("Requesting Copy action. Old state: ", _action.state);
264             //}
265             bool actionStateProcessed = w.updateActionState(_action, true, false);
266             _enabled = _action.state.enabled;
267             if (actionStateProcessed)
268                 _checked = _action.state.checked;
269         }
270         for (int i = 0; i < _subitems.length; i++) {
271             _subitems[i].updateActionState(w);
272         }
273     }
274 
275     this() {
276         _enabled = true;
277     }
278     this(Action action) {
279         _action = action;
280         _enabled = true;
281     }
282     this(const Action action) {
283         _action = action.clone;
284         _enabled = true;
285     }
286     ~this() {
287         // TODO
288     }
289 }
290 
291 /// widget to draw menu item
292 class MenuItemWidget : WidgetGroupDefaultDrawing {
293     protected bool _mainMenu;
294     protected MenuItem _item;
295     protected ImageWidget _icon;
296     protected TextWidget _accel;
297     protected TextWidget _label;
298     protected int _labelWidth;
299     protected int _iconWidth;
300     protected int _accelWidth;
301     protected int _height;
302     @property MenuItem item() { return _item; }
303     void setSubitemSizes(int maxLabelWidth, int maxHeight, int maxIconWidth, int maxAccelWidth) {
304         _labelWidth = maxLabelWidth;
305         _height = maxHeight;
306         _iconWidth = maxIconWidth;
307         _accelWidth = maxAccelWidth;
308     }
309     void measureSubitems(ref int maxLabelWidth, ref int maxHeight, ref int maxIconWidth, ref int maxAccelWidth) {
310         _label.measure(SIZE_UNSPECIFIED, SIZE_UNSPECIFIED);
311         if (maxLabelWidth < _label.measuredWidth)
312             maxLabelWidth = _label.measuredWidth;
313         if (maxHeight < _label.measuredHeight)
314             maxHeight = _label.measuredHeight;
315         if (_icon) {
316             _icon.measure(SIZE_UNSPECIFIED, SIZE_UNSPECIFIED);
317             if (maxIconWidth < _icon.measuredWidth)
318                 maxIconWidth = _icon.measuredWidth;
319             if (maxHeight < _icon.measuredHeight)
320                 maxHeight = _icon.measuredHeight;
321         }
322         if (_accel) {
323             _accel.measure(SIZE_UNSPECIFIED, SIZE_UNSPECIFIED);
324             if (maxAccelWidth < _accel.measuredWidth)
325                 maxAccelWidth = _accel.measuredWidth;
326             if (maxHeight < _accel.measuredHeight)
327                 maxHeight = _accel.measuredHeight;
328         }
329     }
330     /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
331     override void measure(int parentWidth, int parentHeight) { 
332         updateState();
333         Rect m = margins;
334         Rect p = padding;
335         // calc size constraints for children
336         int pwidth = parentWidth;
337         int pheight = parentHeight;
338         if (parentWidth != SIZE_UNSPECIFIED)
339             pwidth -= m.left + m.right + p.left + p.right;
340         if (parentHeight != SIZE_UNSPECIFIED)
341             pheight -= m.top + m.bottom + p.top + p.bottom;
342         if (_labelWidth)
343             measuredContent(parentWidth, parentHeight, _iconWidth + _labelWidth + _accelWidth, _height); // for vertical (popup menu)
344         else {
345             _label.measure(pwidth, pheight);
346             measuredContent(parentWidth, parentHeight, _label.measuredWidth, _label.measuredHeight); // for horizonral (main) menu
347         }
348     }
349 
350     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
351     override void layout(Rect rc) {
352         _needLayout = false;
353         if (visibility == Visibility.Gone) {
354             return;
355         }
356         _pos = rc;
357         applyMargins(rc);
358         applyPadding(rc);
359         Rect labelRc = rc;
360         Rect iconRc = rc;
361         Rect accelRc = rc;
362         iconRc.right = iconRc.left + _iconWidth;
363         accelRc.left = accelRc.right - _accelWidth;
364         labelRc.left += _iconWidth;
365         labelRc.right -= _accelWidth;
366         if (_icon)
367             _icon.layout(iconRc);
368         if (_accel)
369             _accel.layout(accelRc);
370         _label.layout(labelRc);
371     }
372 
373     protected void updateState() {
374         if (_item.enabled)
375             setState(State.Enabled);
376         else
377             resetState(State.Enabled);
378         if (_item.checked)
379             setState(State.Checked);
380         else
381             resetState(State.Checked);
382     }
383 
384     ///// call to update state for action (if action is assigned for widget)
385     //override void updateActionState(bool force = false) {
386     //    if (!_item.action)
387     //        return;
388     //    super.updateActionState(_item._action, force);
389     //    _item.enabled = _item._action.state.enabled;
390     //    _item.checked = _item._action.state.checked;
391     //    updateState();
392     //}
393 
394     this(MenuItem item, bool mainMenu) {
395         id="menuitem";
396         _mainMenu = mainMenu;
397         _item = item;
398         styleId = STYLE_MENU_ITEM;
399         updateState();
400         string iconId = _item.action !is null ? _item.action.iconId : "";
401         if (_item.type == MenuItemType.Check)
402             iconId = "btn_check";
403         else if (_item.type == MenuItemType.Radio)
404             iconId = "btn_radio";
405         // icon
406         if (_item.action && iconId.length) {
407             _icon = new ImageWidget("MENU_ICON", iconId);
408             _icon.styleId = STYLE_MENU_ICON;
409             _icon.state = State.Parent;
410             addChild(_icon);
411         }
412         // label
413         _label = new TextWidget("MENU_LABEL");
414         _label.text = _item.label;
415         _label.styleId = _mainMenu ? "MAIN_MENU_LABEL" : "MENU_LABEL";
416         _label.state = State.Parent;
417         addChild(_label);
418         // accelerator
419         dstring acc = _item.acceleratorText;
420         if (_item.isSubmenu && !mainMenu) {
421             version (Windows) {
422                 acc = ">"d;
423                 //acc = "►"d;
424             } else {
425                 acc = "‣"d;
426             }
427         }
428         if (acc !is null) {
429             _accel = new TextWidget("MENU_ACCEL");
430             _accel.styleId = STYLE_MENU_ACCEL;
431             _accel.text = acc;
432             _accel.state = State.Parent;
433             if (_item.isSubmenu && !mainMenu)
434                 _accel.alignment = Align.Right | Align.VCenter;
435             addChild(_accel);
436         }
437         trackHover = true;
438         clickable = true;
439     }
440 }
441 
442 /// base class for menus
443 class MenuWidgetBase : ListWidget {
444     protected MenuWidgetBase _parentMenu;
445     protected MenuItem _item;
446     protected PopupMenu _openedMenu;
447     protected PopupWidget _openedPopup;
448     protected int _openedPopupIndex;
449 
450     /// menu item click listener
451     Signal!MenuItemClickHandler menuItemClick;
452     /// menu item action listener
453     Signal!MenuItemActionHandler menuItemAction;
454 
455     this(MenuWidgetBase parentMenu, MenuItem item, Orientation orientation) {
456         _parentMenu = parentMenu;
457         this.orientation = orientation;
458         id = "popup_menu";
459         styleId = STYLE_POPUP_MENU;
460         menuItems = item;
461     }
462 
463     @property void menuItems(MenuItem item) {
464         if (_item) {
465             destroy(_item);
466             _item = null;
467         }
468         _item = item;
469         WidgetListAdapter adapter = new WidgetListAdapter();
470         if (item) {
471             for (int i=0; i < _item.subitemCount; i++) {
472                 MenuItem subitem = _item.subitem(i);
473                 MenuItemWidget widget = new MenuItemWidget(subitem, orientation == Orientation.Horizontal);
474                 if (orientation == Orientation.Horizontal)
475                     widget.styleId = STYLE_MAIN_MENU_ITEM;
476                 widget.parent = this;
477                 adapter.add(widget);
478             }
479         }
480         ownAdapter = adapter;
481         requestLayout();
482     }
483 
484     @property protected bool isMainMenu() {
485         return _orientation == Orientation.Horizontal;
486     }
487 
488     /// call to update state for action (if action is assigned for widget)
489     override void updateActionState(bool force = false) {
490         for (int i = 0; i < itemCount; i++) {
491             MenuItemWidget w = cast(MenuItemWidget)itemWidget(i);
492             if (w)
493                 w.updateActionState(force);
494         }
495     }
496 
497     /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
498     override void measure(int parentWidth, int parentHeight) {
499         if (_orientation == Orientation.Horizontal) {
500             // for horizontal (main) menu, don't align items
501             super.measure(parentWidth, parentHeight);
502             return;
503         }
504 
505         if (visibility == Visibility.Gone) {
506             _measuredWidth = _measuredHeight = 0;
507             return;
508         }
509         int maxLabelWidth;
510         int maxHeight;
511         int maxIconWidth;
512         int maxAccelWidth;
513         /// find max dimensions for item icon and accelerator sizes
514         for (int i = 0; i < itemCount; i++) {
515             MenuItemWidget w = cast(MenuItemWidget)itemWidget(i);
516             if (w)
517                 w.measureSubitems(maxLabelWidth, maxHeight, maxIconWidth, maxAccelWidth);
518         }
519         /// set equal dimensions for item icon and accelerator sizes
520         for (int i = 0; i < itemCount; i++) {
521             MenuItemWidget w = cast(MenuItemWidget)itemWidget(i);
522             if (w)
523                 w.setSubitemSizes(maxLabelWidth, maxHeight, maxIconWidth, maxAccelWidth);
524         }
525         super.measure(parentWidth, parentHeight);
526     }
527 
528     protected void performUndoSelection() {
529         selectItem(-1);
530         setHoverItem(-1);
531     }
532 
533     protected long _lastClosedPopupTs;
534     protected MenuItem _lastClosedPopupMenu;
535     protected enum REOPEN_MENU_THRESHOLD_MS = 200;
536 
537     protected bool isRecentlyClosedItem(MenuItem item) {
538         if (!isMainMenu)
539             return false;
540         long ts = currentTimeMillis;
541         if (ts - _lastClosedPopupTs < REOPEN_MENU_THRESHOLD_MS && item && item is _lastClosedPopupMenu)
542             return true;
543         return false;
544     }
545 
546     protected void onPopupClosed(PopupWidget p) {
547         debug(DebugMenus) Log.d("menu ", id, " onPopupClosed selectionChanging=", _selectionChangingInProgress);
548         if (_openedPopup) {
549             if (_openedPopup is p) {
550                 _lastClosedPopupTs = currentTimeMillis;
551                 _lastClosedPopupMenu = _openedMenu ? _openedMenu._item : null;
552                 _openedMenu.onPopupClosed(p);
553                 //bool undoSelection = _openedPopupIndex == _selectedItemIndex;
554                 _openedPopup = null;
555                 _openedMenu = null;
556                 //if (undoSelection) {
557                 //    performUndoSelection();
558                 //}
559                 if (!isMainMenu)
560                     window.setFocus(this);
561                 //else
562                 //    performUndoSelection();
563                 if (isMainMenu && !_selectionChangingInProgress)
564                     close();
565             } else if (thisPopup is p) {
566                 _openedPopup.close();
567                 _openedPopup = null;
568             }
569         }
570     }
571 
572     void deactivate(bool force = false) {
573         // override in main menu
574     }
575 
576     protected void openSubmenu(int index, MenuItemWidget itemWidget, bool selectFirstItem) {
577         debug(DebugMenus) Log.d("menu", id, " open submenu ", index);
578         if (_openedPopup !is null) {
579             if (_openedPopupIndex == index) {
580                 if (selectFirstItem) {
581                     window.setFocus(_openedMenu);
582                     _openedMenu.selectItem(0);
583                 }
584                 return;
585             } else {
586                 _openedPopup.close();
587                 _openedPopup = null;
588             }
589         }
590 
591         if (isRecentlyClosedItem(itemWidget.item)) {
592             // don't reopen main menu item on duplicate click on the same menu item - deactivate instead
593             // deactivate main menu
594             deactivate();
595             _ignoreItemSelection = itemWidget.item;
596             return;
597         }
598 
599         PopupMenu popupMenu = new PopupMenu(itemWidget.item, this);
600         PopupWidget popup = window.showPopup(popupMenu, itemWidget, orientation == Orientation.Horizontal ? PopupAlign.Below :  PopupAlign.Right);
601         requestActionsUpdate();
602         popup.popupClosed = &onPopupClosed;
603         popup.flags = PopupFlags.CloseOnClickOutside;
604         _openedPopup = popup;
605         _openedMenu = popupMenu;
606         _openedPopupIndex = index;
607         _selectedItemIndex = index;
608         if (selectFirstItem) {
609             debug(DebugMenus) Log.d("menu: selecting first item");
610             window.setFocus(popupMenu);
611             _openedMenu.selectItem(0);
612         }
613     }
614 
615     enum MENU_OPEN_DELAY_MS = 400;
616     ulong _submenuOpenTimer = 0;
617     int _submenuOpenItemIndex = -1;
618     protected void scheduleOpenSubmenu(int index) {
619         if (_submenuOpenTimer) {
620             cancelTimer(_submenuOpenTimer);
621             _submenuOpenTimer = 0;
622         }
623         _submenuOpenItemIndex = index;
624         _submenuOpenTimer = setTimer(MENU_OPEN_DELAY_MS);
625     }
626     protected void cancelOpenSubmenu() {
627         if (_submenuOpenTimer) {
628             cancelTimer(_submenuOpenTimer);
629             _submenuOpenTimer = 0;
630         }
631     }
632     /// handle timer; return true to repeat timer event after next interval, false cancel timer
633     override bool onTimer(ulong id) {
634         if (id == _submenuOpenTimer) {
635             _submenuOpenTimer = 0;
636             MenuItemWidget itemWidget = _submenuOpenItemIndex >= 0 ? cast(MenuItemWidget)_adapter.itemWidget(_submenuOpenItemIndex) : null;
637             if (itemWidget !is null) {
638                 if (itemWidget.item.isSubmenu()) {
639                     Log.d("Opening submenu by timer");
640                     openSubmenu(_submenuOpenItemIndex, itemWidget, _orientation == Orientation.Horizontal); // for main menu, select first item
641                 } else {
642                     // normal item
643                 }
644             }
645         }
646         // override to do something useful
647         // return true to repeat after the same interval, false to stop timer
648         return false;
649     }
650 
651 
652     protected MenuItem _ignoreItemSelection;
653     protected bool _selectionChangingInProgress;
654     /// override to handle change of selection
655     override protected void selectionChanged(int index, int previouslySelectedItem = -1) {
656         debug(DebugMenus) Log.d("menu ", id, " selectionChanged ", index, ", ", previouslySelectedItem, " _selectedItemIndex=", _selectedItemIndex);
657         _selectionChangingInProgress = true;
658         MenuItemWidget itemWidget = index >= 0 ? cast(MenuItemWidget)_adapter.itemWidget(index) : null;
659         MenuItemWidget prevWidget = previouslySelectedItem >= 0 ? cast(MenuItemWidget)_adapter.itemWidget(previouslySelectedItem) : null;
660         if (itemWidget._item is _ignoreItemSelection && isMainMenu) {
661             _ignoreItemSelection = null;
662             deactivate();
663             return;
664         }
665         if (index >= 0)
666             setFocus();
667         bool popupWasOpen = false;
668         if (prevWidget !is null) {
669             if (_openedPopup !is null) {
670                 _openedPopup.close();
671                 _openedPopup = null;
672                 popupWasOpen = true;
673             }
674         }
675         if (itemWidget !is null) {
676             if (itemWidget.item.isSubmenu()) {
677                 if (_selectOnHover || popupWasOpen) {
678                     if (popupWasOpen && _orientation == Orientation.Horizontal) {
679                         // instantly open submenu in main menu if previous submenu was opened
680                         openSubmenu(index, itemWidget, false); // _orientation == Orientation.Horizontal for main menu, select first item
681                     } else {
682                         if (!isMainMenu)
683                             scheduleOpenSubmenu(index);
684                     }
685                 }
686             } else {
687                 // normal item
688             }
689         }
690         _selectionChangingInProgress = false;
691     }
692 
693     protected void handleMenuItemClick(MenuItem item) {
694         // precessing for CheckBox and RadioButton menus
695         if (item.type == MenuItemType.Check) {
696             item.checked = !item.checked;
697         } else if (item.type == MenuItemType.Radio) {
698             item.checked = true;
699         }
700         MenuItem p = item;
701         while (p) {
702             if (p.menuItemClick.assigned) {
703                 p.menuItemClick(item);
704                 break;
705             }
706             if (p.menuItemAction.assigned && item.action) {
707                 p.menuItemAction(item.action);
708                 break;
709             }
710             p = p._parent;
711         }
712     }
713 
714     protected void onMenuItem(MenuItem item) {
715         debug(DebugMenus) Log.d("onMenuItem ", item.action.label);
716         if (_openedPopup !is null) {
717             _openedPopup.close();
718             _openedPopup = null;
719         }
720         if (_parentMenu !is null)
721             _parentMenu.onMenuItem(item);
722         else {
723             // top level handling
724             debug(DebugMenus) Log.d("onMenuItem ", item.id);
725             selectItem(-1);
726             setHoverItem(-1);
727             selectOnHover = false;
728 
729             // copy menu item click listeners
730             Signal!MenuItemClickHandler onMenuItemClickListenerCopy = menuItemClick;
731             // copy item action listeners
732             Signal!MenuItemActionHandler onMenuItemActionListenerCopy = menuItemAction;
733 
734             handleMenuItemClick(item);
735 
736             PopupWidget popup = cast(PopupWidget)parent;
737             if (popup)
738                 popup.close();
739 
740             // this pointer now can be invalid - if popup removed
741             if (onMenuItemClickListenerCopy.assigned)
742                 if (onMenuItemClickListenerCopy(item))
743                     return;
744             // this pointer now can be invalid - if popup removed
745             if (onMenuItemActionListenerCopy.assigned)
746                 onMenuItemActionListenerCopy(item.action);
747         }
748     }
749 
750     @property MenuItemWidget selectedMenuItemWidget() {
751         return _selectedItemIndex >= 0 ? cast(MenuItemWidget)_adapter.itemWidget(_selectedItemIndex) : null;
752     }
753 
754     /// override to handle mouse up on item
755     override protected void itemClicked(int index) {
756         MenuItemWidget itemWidget = index >= 0 ? cast(MenuItemWidget)_adapter.itemWidget(index) : null;
757         if (itemWidget !is null) {
758             debug(DebugMenus) Log.d("Menu ", id, " Item clicked ", itemWidget.item.action.id);
759             if (itemWidget.item.isSubmenu()) {
760                 // submenu clicked
761                 if (_clickOnButtonDown && _openedPopup !is null && _openedMenu._item is itemWidget.item) {
762 
763                     if (_selectedItemIndex == index) {
764                         _openedMenu.setFocus();
765                         return;
766                     }
767 
768                     // second click on main menu opened item
769                     _openedPopup.close();
770                     _openedPopup = null;
771                     //selectItem(-1);
772                     selectOnHover = false;
773                 } else {
774                     openSubmenu(index, itemWidget, _orientation == Orientation.Horizontal); // for main menu, select first item
775                     selectOnHover = true;
776                 }
777             } else {
778                 // normal item
779                 onMenuItem(itemWidget.item);
780             }
781         }
782     }
783 
784     /// returns popup this menu is located in
785     @property PopupWidget thisPopup() {
786         return cast(PopupWidget)parent;
787     }
788 
789     protected int _menuToggleState;
790     protected Widget _menuTogglePreviousFocus;
791 
792     /// override to handle specific actions state (e.g. change enabled state for supported actions)
793     override bool handleActionStateRequest(const Action a) {
794         if (_menuTogglePreviousFocus) {
795             debug(DebugMenus) Log.d("Menu.handleActionStateRequest forwarding to ", _menuTogglePreviousFocus);
796             bool res = _menuTogglePreviousFocus.handleActionStateRequest(a);
797             debug(DebugMenus) Log.d("Menu.handleActionStateRequest forwarding handled successful: ", a.state.toString);
798             return res;
799         }
800         return false;
801     }
802 
803     /// list navigation using keys
804     override bool onKeyEvent(KeyEvent event) {
805         if (orientation == Orientation.Horizontal) {
806             // no special processing
807             if (event.action == KeyAction.KeyDown) {
808                 if (event.keyCode == KeyCode.ESCAPE) {
809                     close();
810                     return true;
811                 }
812             }
813         } else {
814             // for vertical (popup) menu
815             if (!focused)
816                 return false;
817             if (event.action == KeyAction.KeyDown) {
818                 if (event.keyCode == KeyCode.LEFT) {
819                     if (_parentMenu !is null) {
820                         if (_parentMenu.orientation == Orientation.Vertical) {
821                             if (thisPopup !is null) {
822                                 //int selectedItem = _selectedItemIndex;
823                                 // back to parent menu on Left key
824                                 thisPopup.close();
825                                 //if (selectedItem >= 0)
826                                 //    selectItem(selectedItem);
827                                 return true;
828                             }
829                         } else {
830                             // parent is main menu
831                             _parentMenu.moveSelection(-1);
832                             return true;
833                         }
834                     }
835                     return true;
836                 } else if (event.keyCode == KeyCode.RIGHT) {
837                     MenuItemWidget thisItem = selectedMenuItemWidget();
838                     if (thisItem !is null && thisItem.item.isSubmenu) {
839                         openSubmenu(_selectedItemIndex, thisItem, true);
840                         return true;
841                     } else if (_parentMenu !is null && _parentMenu.orientation == Orientation.Horizontal) {
842                         _parentMenu.moveSelection(1);
843                         return true;
844                     }
845                     return true;
846                 } else if (event.keyCode == KeyCode.ESCAPE) {
847                     close();
848                     return true;
849                 }
850             } else if (event.action == KeyAction.KeyUp) {
851                 if (event.keyCode == KeyCode.LEFT || event.keyCode == KeyCode.RIGHT) {
852                     return true;
853                 }
854             } else if (event.action == KeyAction.Text && event.flags == 0) {
855                 dchar ch = event.text[0];
856                 int index = _item.findSubitemByHotkey(ch);
857                 if (index >= 0) {
858                     itemClicked(index);
859                     return true;
860                 }
861             }
862         }
863         if (_selectedItemIndex >= 0 && event.action == KeyAction.KeyDown && /*event.flags == 0 &&*/ (event.keyCode == KeyCode.RETURN || event.keyCode == KeyCode.SPACE)) {
864             itemClicked(_selectedItemIndex);
865             return true;
866         }
867         bool res = super.onKeyEvent(event);
868         return res;
869     }
870     /// closes this menu - handle ESC key
871     void close() {
872         cancelOpenSubmenu();
873         if (thisPopup !is null)
874             thisPopup.close();
875     }
876 
877     /// map key to action
878     override Action findKeyAction(uint keyCode, uint flags) {
879         if (!_item)
880             return null;
881         Action action = _item.findKeyAction(keyCode, flags);
882         return action;
883     }
884 
885 }
886 
887 /// main menu (horizontal)
888 class MainMenu : MenuWidgetBase {
889 
890     this() {
891         super(null, null, Orientation.Horizontal);
892         id = "MAIN_MENU";
893         styleId = STYLE_MAIN_MENU;
894         _clickOnButtonDown = true;
895         selectOnHover = false;
896     }
897 
898     this(MenuItem item) {
899         super(null, item, Orientation.Horizontal);
900         id = "MAIN_MENU";
901         styleId = STYLE_MAIN_MENU;
902         _clickOnButtonDown = true;
903         selectOnHover = false;
904     }
905 
906     /// call to update state for action (if action is assigned for widget)
907     override void updateActionState(bool force) {
908         //Log.d("MainMenu: updateActionState");
909         //_item.updateActionState(this);
910 
911     }
912 
913     /// override and return true to track key events even when not focused
914     @property override bool wantsKeyTracking() {
915         return true;
916     }
917 
918     /// get text flags (bit set of TextFlag enum values)
919     @property override uint textFlags() {
920         // override text flags for main menu
921         if (_selectedItemIndex >= 0)
922             return TextFlag.UnderlineHotKeys | TextFlag.HotKeys;
923         else
924             return TextFlag.UnderlineHotKeysWhenAltPressed | TextFlag.HotKeys;
925     }
926 
927     protected int _menuToggleState;
928     protected Widget _menuTogglePreviousFocus;
929 
930     override protected void onMenuItem(MenuItem item) {
931         debug(DebugMenus) Log.d("MainMenu.onMenuItem ", item.action.label);
932 
933         // copy menu item click listeners
934         Signal!MenuItemClickHandler onMenuItemClickListenerCopy = menuItemClick;
935         // copy item action listeners
936         Signal!MenuItemActionHandler onMenuItemActionListenerCopy = menuItemAction;
937         
938         deactivate();
939 
940         handleMenuItemClick(item);
941 
942         // this pointer now can be invalid - if popup removed
943         if (onMenuItemClickListenerCopy.assigned)
944             if (onMenuItemClickListenerCopy(item))
945                 return;
946         // this pointer now can be invalid - if popup removed
947         if (onMenuItemActionListenerCopy.assigned)
948             onMenuItemActionListenerCopy(item.action);
949     }
950 
951     /// return true if main menu is activated (focused or has open submenu)
952     @property bool activated() {
953         return focused || _selectedItemIndex >= 0 || _openedPopup !is null;
954     }
955 
956     override protected void performUndoSelection() {
957         deactivate();
958     }
959 
960     /// closes this menu - ESC handling
961     override void close() {
962         debug(DebugMenus) Log.d("menu ", id, " close called");
963         if (_openedPopup !is null) {
964             _openedPopup.close();
965             _openedPopup = null;
966         } else
967             deactivate();
968     }
969 
970     /// request relayout of widget and its children
971     //override void requestLayout() {
972     //    Log.d("MainMenu.requestLayout is called");
973     //    super.requestLayout();
974     //}
975 
976     /// bring focus to main menu, if not yet activated
977     void activate() {
978         debug(DebugMenus) Log.d("activating main menu");
979         if (activated)
980             return;
981         window.setFocus(this);
982         selectItem(0);
983     }
984 
985     /// close and remove focus, if activated
986     override void deactivate(bool force = false) {
987         debug(DebugMenus) Log.d("deactivating main menu");
988         if (!activated && !force)
989             return;
990         if (_openedPopup !is null) {
991             _openedPopup.close();
992             _openedPopup = null;
993         }
994         selectItem(-1);
995         setHoverItem(-1);
996         selectOnHover = false;
997         window.setFocus(_menuTogglePreviousFocus);
998     }
999 
1000     /// activate or deactivate main menu, return true if it has been activated
1001     bool toggle() {
1002         if (activated) {
1003             // unfocus
1004             deactivate();
1005             return false;
1006         } else {
1007             // focus
1008             activate();
1009             return true;
1010         }
1011 
1012     }
1013 
1014     /// override to handle focus changes
1015     override protected void handleFocusChange(bool focused, bool receivedFocusFromKeyboard = false) {
1016         debug(DebugMenus) Log.d("menu ", id, "handling focus change to ", focused);
1017         if (focused && _openedPopup is null) {
1018             // activating!
1019             _menuTogglePreviousFocus = window.focusedWidget;
1020             //updateActionState(true);
1021             debug(DebugMenus) Log.d("MainMenu: updateActionState");
1022             _item.updateActionState(this);
1023         }
1024         super.handleFocusChange(focused);
1025     }
1026 
1027     /// list navigation using keys
1028     override bool onKeyEvent(KeyEvent event) {
1029         // handle MainMenu activation / deactivation (Alt, Esc...)
1030         bool toggleMenu = false;
1031         bool isAlt = event.keyCode == KeyCode.ALT || event.keyCode == KeyCode.LALT || event.keyCode == KeyCode.RALT;
1032         bool altPressed = !!(event.flags & KeyFlag.Alt);
1033         bool noOtherModifiers = !(event.flags & (KeyFlag.Shift | KeyFlag.Control));
1034 
1035         if (event.action == KeyAction.KeyDown && event.keyCode == KeyCode.ESCAPE && event.flags == 0 && activated) {
1036             deactivate();
1037             return true;
1038         }
1039         dchar hotkey = 0;
1040         if (event.action == KeyAction.KeyDown && event.keyCode >= KeyCode.KEY_A && event.keyCode <= KeyCode.KEY_Z && altPressed && noOtherModifiers) {
1041 //            Log.d("Alt + a..z");
1042             hotkey = cast(dchar)((event.keyCode - KeyCode.KEY_A) + 'a');
1043         }
1044         if (event.action == KeyAction.Text && altPressed && noOtherModifiers) {
1045             hotkey = event.text[0];
1046         }
1047         if (hotkey) {
1048             int index = _item.findSubitemByHotkey(hotkey);
1049             if (index >= 0) {
1050                 activate();
1051                 itemClicked(index);
1052                 return true;
1053             } else {
1054                 MenuItem item = _item.findSubitemByHotkeyRecursive(hotkey);
1055                 if (item) {
1056                     Log.d("found menu item recursive");
1057                     onMenuItem(item);
1058                     return true;
1059                 }
1060                 return false;
1061             }
1062         }
1063 
1064         // toggle menu by single Alt press - for Windows only!
1065         version (Windows) {
1066             if (event.action == KeyAction.KeyDown && isAlt && noOtherModifiers) {
1067                 _menuToggleState = 1;
1068             } else if (event.action == KeyAction.KeyUp && isAlt && noOtherModifiers) {
1069                 if (_menuToggleState == 1)
1070                     toggleMenu = true;
1071                 _menuToggleState = 0;
1072             } else {
1073                 _menuToggleState = 0;
1074             }
1075             if (toggleMenu) {
1076                 toggle();
1077                 return true;
1078             }
1079         }
1080         if (!focused)
1081             return false;
1082         if (_selectedItemIndex >= 0 && event.action == KeyAction.KeyDown && ((event.keyCode == KeyCode.DOWN) || (event.keyCode == KeyCode.SPACE) || (event.keyCode == KeyCode.RETURN))) {
1083             itemClicked(_selectedItemIndex);
1084             return true;
1085         }
1086         return super.onKeyEvent(event);
1087     }
1088 
1089     override @property protected uint overrideStateForItem() {
1090         uint res = state;
1091         if (_openedPopup)
1092             res |= State.Focused; // main menu with opened popup as focused for items display
1093         return res;
1094     }
1095 
1096 }
1097 
1098 
1099 /// popup menu widget (vertical layout of items)
1100 class PopupMenu : MenuWidgetBase {
1101 
1102     this(MenuItem item, MenuWidgetBase parentMenu = null) {
1103         super(parentMenu, item, Orientation.Vertical);
1104         id = "POPUP_MENU";
1105         styleId = STYLE_POPUP_MENU;
1106         selectOnHover = true;
1107     }
1108 }