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