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         // Do not reassign the same item (https://github.com/buggins/dlangui/issues/613)
511         if(_item == item)
512             return;
513         if (_item) {
514             destroy(_item);
515             _item = null;
516         }
517         _item = item;
518         WidgetListAdapter adapter = new WidgetListAdapter();
519         if (item) {
520             for (int i=0; i < _item.subitemCount; i++) {
521                 MenuItem subitem = _item.subitem(i);
522                 MenuItemWidget widget = new MenuItemWidget(subitem, orientation == Orientation.Horizontal);
523                 if (orientation == Orientation.Horizontal)
524                     widget.styleId = STYLE_MAIN_MENU_ITEM;
525                 widget.parent = this;
526                 adapter.add(widget);
527             }
528         }
529         ownAdapter = adapter;
530         requestLayout();
531     }
532 
533     @property protected bool isMainMenu() {
534         return _orientation == Orientation.Horizontal;
535     }
536 
537     /// call to update state for action (if action is assigned for widget)
538     override void updateActionState(bool force = false) {
539         for (int i = 0; i < itemCount; i++) {
540             MenuItemWidget w = cast(MenuItemWidget)itemWidget(i);
541             if (w)
542                 w.updateActionState(force);
543         }
544     }
545 
546     /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
547     override void measure(int parentWidth, int parentHeight) {
548         if (_orientation == Orientation.Horizontal) {
549             // for horizontal (main) menu, don't align items
550             super.measure(parentWidth, parentHeight);
551             return;
552         }
553 
554         if (visibility == Visibility.Gone) {
555             _measuredWidth = _measuredHeight = 0;
556             return;
557         }
558         int maxLabelWidth;
559         int maxHeight;
560         int maxIconWidth;
561         int maxAccelWidth;
562         /// find max dimensions for item icon and accelerator sizes
563         for (int i = 0; i < itemCount; i++) {
564             MenuItemWidget w = cast(MenuItemWidget)itemWidget(i);
565             if (w)
566                 w.measureSubitems(maxLabelWidth, maxHeight, maxIconWidth, maxAccelWidth);
567         }
568         /// set equal dimensions for item icon and accelerator sizes
569         for (int i = 0; i < itemCount; i++) {
570             MenuItemWidget w = cast(MenuItemWidget)itemWidget(i);
571             if (w)
572                 w.setSubitemSizes(maxLabelWidth, maxHeight, maxIconWidth, maxAccelWidth);
573         }
574         super.measure(parentWidth, parentHeight);
575     }
576 
577     protected void performUndoSelection() {
578         selectItem(-1);
579         setHoverItem(-1);
580     }
581 
582     protected long _lastClosedPopupTs;
583     protected MenuItem _lastClosedPopupMenu;
584     protected enum REOPEN_MENU_THRESHOLD_MS = 200;
585 
586     protected bool isRecentlyClosedItem(MenuItem item) {
587         if (!isMainMenu)
588             return false;
589         long ts = currentTimeMillis;
590         if (ts - _lastClosedPopupTs < REOPEN_MENU_THRESHOLD_MS && item && item is _lastClosedPopupMenu)
591             return true;
592         return false;
593     }
594 
595     protected void onPopupClosed(PopupWidget p) {
596         debug(DebugMenus) Log.d("menu ", id, " onPopupClosed selectionChanging=", _selectionChangingInProgress);
597         if (_openedPopup) {
598             if (_openedPopup is p) {
599                 _lastClosedPopupTs = currentTimeMillis;
600                 _lastClosedPopupMenu = _openedMenu ? _openedMenu._item : null;
601                 _openedMenu.onPopupClosed(p);
602                 //bool undoSelection = _openedPopupIndex == _selectedItemIndex;
603                 _openedPopup = null;
604                 _openedMenu = null;
605                 //if (undoSelection) {
606                 //    performUndoSelection();
607                 //}
608                 if (!isMainMenu)
609                     window.setFocus(this);
610                 //else
611                 //    performUndoSelection();
612                 if (isMainMenu && !_selectionChangingInProgress)
613                     close();
614             } else if (thisPopup is p) {
615                 _openedPopup.close();
616                 _openedPopup = null;
617             }
618         }
619     }
620 
621     void deactivate(bool force = false) {
622         // override in main menu
623     }
624 
625     protected void openSubmenu(int index, MenuItemWidget itemWidget) {
626         debug(DebugMenus) Log.d("menu", id, " open submenu ", index);
627         if (_openedPopup !is null) {
628             if (_openedPopupIndex == index) {
629                 return;
630             } else {
631                 _openedPopup.close();
632                 _openedPopup = null;
633             }
634         }
635 
636         if (isRecentlyClosedItem(itemWidget.item)) {
637             // don't reopen main menu item on duplicate click on the same menu item - deactivate instead
638             // deactivate main menu
639             deactivate();
640             _ignoreItemSelection = itemWidget.item;
641             return;
642         }
643 
644         PopupMenu popupMenu = new PopupMenu(itemWidget.item, this);
645         PopupWidget popup = window.showPopup(popupMenu, itemWidget, orientation == Orientation.Horizontal ? PopupAlign.Below :  PopupAlign.Right);
646         requestActionsUpdate();
647         popup.popupClosed = &onPopupClosed;
648         popup.flags = PopupFlags.CloseOnClickOutside;
649         _openedPopup = popup;
650         _openedMenu = popupMenu;
651         _openedPopupIndex = index;
652         _selectedItemIndex = index;
653         /*if (selectFirstItem) {
654             debug(DebugMenus) Log.d("menu: selecting first item");
655             window.setFocus(popupMenu);
656             _openedMenu.selectItem(-1);
657         }*/
658     }
659 
660     enum MENU_OPEN_DELAY_MS = 400;
661     ulong _submenuOpenTimer = 0;
662     int _submenuOpenItemIndex = -1;
663     protected void scheduleOpenSubmenu(int index) {
664         if (_submenuOpenTimer) {
665             cancelTimer(_submenuOpenTimer);
666             _submenuOpenTimer = 0;
667         }
668         _submenuOpenItemIndex = index;
669         _submenuOpenTimer = setTimer(MENU_OPEN_DELAY_MS);
670     }
671     protected void cancelOpenSubmenu() {
672         if (_submenuOpenTimer) {
673             cancelTimer(_submenuOpenTimer);
674             _submenuOpenTimer = 0;
675         }
676     }
677     /// handle timer; return true to repeat timer event after next interval, false cancel timer
678     override bool onTimer(ulong id) {
679         if (id == _submenuOpenTimer) {
680             _submenuOpenTimer = 0;
681             MenuItemWidget itemWidget = _submenuOpenItemIndex >= 0 ? cast(MenuItemWidget)_adapter.itemWidget(_submenuOpenItemIndex) : null;
682             if (itemWidget !is null) {
683                 if (itemWidget.item.isSubmenu()) {
684                     Log.d("Opening submenu by timer");
685                     openSubmenu(_submenuOpenItemIndex, itemWidget);
686                 } else {
687                     // normal item
688                 }
689             }
690         }
691         // override to do something useful
692         // return true to repeat after the same interval, false to stop timer
693         return false;
694     }
695 
696 
697     protected MenuItem _ignoreItemSelection;
698     protected bool _selectionChangingInProgress;
699     /// override to handle change of selection
700     override protected void selectionChanged(int index, int previouslySelectedItem = -1) {
701         debug(DebugMenus) Log.d("menu ", id, " selectionChanged ", index, ", ", previouslySelectedItem, " _selectedItemIndex=", _selectedItemIndex);
702         _selectionChangingInProgress = true;
703         MenuItemWidget itemWidget = index >= 0 ? cast(MenuItemWidget)_adapter.itemWidget(index) : null;
704         MenuItemWidget prevWidget = previouslySelectedItem >= 0 ? cast(MenuItemWidget)_adapter.itemWidget(previouslySelectedItem) : null;
705         if (itemWidget._item is _ignoreItemSelection && isMainMenu) {
706             _ignoreItemSelection = null;
707             deactivate();
708             return;
709         }
710         if (index >= 0)
711             setFocus();
712         bool popupWasOpen = false;
713         if (prevWidget !is null) {
714             if (_openedPopup !is null) {
715                 _openedPopup.close();
716                 _openedPopup = null;
717                 popupWasOpen = true;
718             }
719         }
720         if (itemWidget !is null) {
721             if (itemWidget.item.isSubmenu()) {
722                 if (_selectOnHover || popupWasOpen) {
723                     if (popupWasOpen && _orientation == Orientation.Horizontal) {
724                         // instantly open submenu in main menu if previous submenu was opened
725                         openSubmenu(index, itemWidget);
726                     } else {
727                         if (!isMainMenu)
728                             scheduleOpenSubmenu(index);
729                     }
730                 }
731             } else {
732                 // normal item
733             }
734         }
735         _selectionChangingInProgress = false;
736     }
737 
738     protected void handleMenuItemClick(MenuItem item) {
739         // precessing for CheckBox and RadioButton menus
740         if (item.type == MenuItemType.Check) {
741             item.checked = !item.checked;
742         } else if (item.type == MenuItemType.Radio) {
743             item.checked = true;
744         }
745         MenuItem p = item;
746         while (p) {
747             if (p.menuItemClick.assigned) {
748                 p.menuItemClick(item);
749                 break;
750             }
751             if (p.menuItemAction.assigned && item.action) {
752                 p.menuItemAction(item.action);
753                 break;
754             }
755             p = p._parent;
756         }
757     }
758 
759     protected void onMenuItem(MenuItem item) {
760         debug(DebugMenus) Log.d("onMenuItem ", item.action.label);
761         if (_openedPopup !is null) {
762             _openedPopup.close();
763             _openedPopup = null;
764         }
765         if (_parentMenu !is null)
766             _parentMenu.onMenuItem(item);
767         else {
768             // top level handling
769             debug(DebugMenus) Log.d("onMenuItem ", item.id);
770             selectItem(-1);
771             setHoverItem(-1);
772             selectOnHover = false;
773 
774             // copy menu item click listeners
775             Signal!MenuItemClickHandler onMenuItemClickListenerCopy = menuItemClick;
776             // copy item action listeners
777             Signal!MenuItemActionHandler onMenuItemActionListenerCopy = menuItemAction;
778 
779             handleMenuItemClick(item);
780 
781             PopupWidget popup = cast(PopupWidget)parent;
782             if (popup)
783                 popup.close();
784 
785             // this pointer now can be invalid - if popup removed
786             if (onMenuItemClickListenerCopy.assigned)
787                 if (onMenuItemClickListenerCopy(item))
788                     return;
789             // this pointer now can be invalid - if popup removed
790             if (onMenuItemActionListenerCopy.assigned)
791                 onMenuItemActionListenerCopy(item.action);
792         }
793     }
794 
795     @property MenuItemWidget selectedMenuItemWidget() {
796         return _selectedItemIndex >= 0 ? cast(MenuItemWidget)_adapter.itemWidget(_selectedItemIndex) : null;
797     }
798 
799     /// override to handle mouse up on item
800     override protected void itemClicked(int index) {
801         MenuItemWidget itemWidget = index >= 0 ? cast(MenuItemWidget)_adapter.itemWidget(index) : null;
802         if (itemWidget !is null) {
803             debug(DebugMenus) Log.d("Menu ", id, " Item clicked ", itemWidget.item.action.id);
804             if (itemWidget.item.isSubmenu()) {
805                 // submenu clicked
806                 if (_clickOnButtonDown && _openedPopup !is null && _openedMenu._item is itemWidget.item) {
807 
808                     if (_selectedItemIndex == index) {
809                         _openedMenu.setFocus();
810                         return;
811                     }
812 
813                     // second click on main menu opened item
814                     _openedPopup.close();
815                     _openedPopup = null;
816                     //selectItem(-1);
817                     selectOnHover = false;
818                 } else {
819                     if(_openedPopup !is null && _openedPopupIndex == index)
820                     {
821                         _openedPopup.close();
822                         _openedPopup = null;
823                         _openedPopupIndex = -1;
824                     }
825                     else
826                         openSubmenu(index, itemWidget);
827                     selectOnHover = true;
828                 }
829             } else {
830                 // normal item
831                 onMenuItem(itemWidget.item);
832             }
833         }
834     }
835 
836     /// returns popup this menu is located in
837     @property PopupWidget thisPopup() {
838         return cast(PopupWidget)parent;
839     }
840 
841     protected int _menuToggleState;
842     protected Widget _menuTogglePreviousFocus;
843 
844     /// override to handle specific actions state (e.g. change enabled state for supported actions)
845     override bool handleActionStateRequest(const Action a) {
846         if (_menuTogglePreviousFocus) {
847             debug(DebugMenus) Log.d("Menu.handleActionStateRequest forwarding to ", _menuTogglePreviousFocus);
848             bool res = _menuTogglePreviousFocus.handleActionStateRequest(a);
849             debug(DebugMenus) Log.d("Menu.handleActionStateRequest forwarding handled successful: ", a.state.toString);
850             return res;
851         }
852         return false;
853     }
854 
855     /// list navigation using keys
856     override bool onKeyEvent(KeyEvent event) {
857         if (orientation == Orientation.Horizontal) {
858             // no special processing
859             if (event.action == KeyAction.KeyDown) {
860                 if (event.keyCode == KeyCode.ESCAPE) {
861                     close();
862                     return true;
863                 }
864             }
865         } else {
866             // for vertical (popup) menu
867             if (!focused)
868                 return false;
869             if (event.action == KeyAction.KeyDown) {
870                 if (event.keyCode == KeyCode.LEFT) {
871                     if (_parentMenu !is null) {
872                         if (_parentMenu.orientation == Orientation.Vertical) {
873                             if (thisPopup !is null) {
874                                 //int selectedItem = _selectedItemIndex;
875                                 // back to parent menu on Left key
876                                 thisPopup.close();
877                                 //if (selectedItem >= 0)
878                                 //    selectItem(selectedItem);
879                                 return true;
880                             }
881                         } else {
882                             // parent is main menu
883                             _parentMenu.moveSelection(-1);
884                             return true;
885                         }
886                     }
887                     return true;
888                 } else if (event.keyCode == KeyCode.RIGHT) {
889                     MenuItemWidget thisItem = selectedMenuItemWidget();
890                     if (thisItem !is null && thisItem.item.isSubmenu) {
891                         openSubmenu(_selectedItemIndex, thisItem);
892                         return true;
893                     } else if (_parentMenu !is null && _parentMenu.orientation == Orientation.Horizontal) {
894                         _parentMenu.moveSelection(1);
895                         return true;
896                     }
897                     return true;
898                 } else if (event.keyCode == KeyCode.ESCAPE) {
899                     close();
900                     return true;
901                 }
902             } else if (event.action == KeyAction.KeyUp) {
903                 if (event.keyCode == KeyCode.LEFT || event.keyCode == KeyCode.RIGHT) {
904                     return true;
905                 }
906             } else if (event.action == KeyAction.Text && event.flags == 0) {
907                 dchar ch = event.text[0];
908                 int index = _item.findSubitemByHotkey(ch);
909                 if (index >= 0) {
910                     itemClicked(index);
911                     return true;
912                 }
913             }
914         }
915         if (_selectedItemIndex >= 0 && event.action == KeyAction.KeyDown && /*event.flags == 0 &&*/ (event.keyCode == KeyCode.RETURN || event.keyCode == KeyCode.SPACE)) {
916             itemClicked(_selectedItemIndex);
917             return true;
918         }
919         bool res = super.onKeyEvent(event);
920         return res;
921     }
922     /// closes this menu - handle ESC key
923     void close() {
924         cancelOpenSubmenu();
925         if (thisPopup !is null)
926             thisPopup.close();
927     }
928 
929     /// map key to action
930     override Action findKeyAction(uint keyCode, uint flags) {
931         if (!_item)
932             return null;
933         Action action = _item.findKeyAction(keyCode, flags);
934         return action;
935     }
936 
937 }
938 
939 /// main menu (horizontal)
940 class MainMenu : MenuWidgetBase {
941 
942     this() {
943         super(null, null, Orientation.Horizontal);
944         id = "MAIN_MENU";
945         styleId = STYLE_MAIN_MENU;
946         _clickOnButtonDown = false;
947         selectOnHover = false;
948     }
949 
950     this(MenuItem item) {
951         super(null, item, Orientation.Horizontal);
952         id = "MAIN_MENU";
953         styleId = STYLE_MAIN_MENU;
954         _clickOnButtonDown = false;
955         selectOnHover = false;
956     }
957 
958     /// call to update state for action (if action is assigned for widget)
959     override void updateActionState(bool force) {
960         //Log.d("MainMenu: updateActionState");
961         //_item.updateActionState(this);
962 
963     }
964 
965     /// override and return true to track key events even when not focused
966     @property override bool wantsKeyTracking() {
967         return true;
968     }
969 
970     /// get text flags (bit set of TextFlag enum values)
971     @property override uint textFlags() {
972         // override text flags for main menu
973         if (_selectedItemIndex >= 0)
974             return TextFlag.UnderlineHotKeys | TextFlag.HotKeys;
975         else
976             return TextFlag.UnderlineHotKeysWhenAltPressed | TextFlag.HotKeys;
977     }
978 
979     protected int _menuToggleState;
980     protected Widget _menuTogglePreviousFocus;
981 
982     override protected void onMenuItem(MenuItem item) {
983         debug(DebugMenus) Log.d("MainMenu.onMenuItem ", item.action.label);
984 
985         // copy menu item click listeners
986         Signal!MenuItemClickHandler onMenuItemClickListenerCopy = menuItemClick;
987         // copy item action listeners
988         Signal!MenuItemActionHandler onMenuItemActionListenerCopy = menuItemAction;
989 
990         deactivate();
991 
992         handleMenuItemClick(item);
993 
994         // this pointer now can be invalid - if popup removed
995         if (onMenuItemClickListenerCopy.assigned)
996             if (onMenuItemClickListenerCopy(item))
997                 return;
998         // this pointer now can be invalid - if popup removed
999         if (onMenuItemActionListenerCopy.assigned)
1000             onMenuItemActionListenerCopy(item.action);
1001     }
1002 
1003     /// return true if main menu is activated (focused or has open submenu)
1004     @property bool activated() {
1005         return focused || _selectedItemIndex >= 0 || _openedPopup !is null;
1006     }
1007 
1008     override protected void performUndoSelection() {
1009         deactivate();
1010     }
1011 
1012     /// closes this menu - ESC handling
1013     override void close() {
1014         debug(DebugMenus) Log.d("menu ", id, " close called");
1015         if (_openedPopup !is null) {
1016             _openedPopup.close();
1017             _openedPopup = null;
1018         } else
1019             deactivate();
1020     }
1021 
1022     /// request relayout of widget and its children
1023     //override void requestLayout() {
1024     //    Log.d("MainMenu.requestLayout is called");
1025     //    super.requestLayout();
1026     //}
1027 
1028     /// bring focus to main menu, if not yet activated
1029     void activate() {
1030         debug(DebugMenus) Log.d("activating main menu");
1031         if (activated)
1032             return;
1033         window.setFocus(this);
1034         selectItem(0);
1035     }
1036 
1037     /// close and remove focus, if activated
1038     override void deactivate(bool force = false) {
1039         debug(DebugMenus) Log.d("deactivating main menu");
1040         if (!activated && !force)
1041             return;
1042         if (_openedPopup !is null) {
1043             _openedPopup.close();
1044             _openedPopup = null;
1045         }
1046         selectItem(-1);
1047         setHoverItem(-1);
1048         selectOnHover = false;
1049         window.setFocus(_menuTogglePreviousFocus);
1050     }
1051 
1052     /// activate or deactivate main menu, return true if it has been activated
1053     bool toggle() {
1054         if (activated) {
1055             // unfocus
1056             deactivate();
1057             return false;
1058         } else {
1059             // focus
1060             activate();
1061             return true;
1062         }
1063 
1064     }
1065 
1066     /// override to handle focus changes
1067     override protected void handleFocusChange(bool focused, bool receivedFocusFromKeyboard = false) {
1068         debug(DebugMenus) Log.d("menu ", id, "handling focus change to ", focused);
1069         if (focused && _openedPopup is null) {
1070             // activating!
1071             _menuTogglePreviousFocus = window.focusedWidget;
1072             //updateActionState(true);
1073             debug(DebugMenus) Log.d("MainMenu: updateActionState");
1074             _item.updateActionState(this);
1075         }
1076         super.handleFocusChange(focused);
1077     }
1078 
1079     /// list navigation using keys
1080     override bool onKeyEvent(KeyEvent event) {
1081         // handle MainMenu activation / deactivation (Alt, Esc...)
1082         bool toggleMenu = false;
1083         bool isAlt = event.keyCode == KeyCode.ALT || event.keyCode == KeyCode.LALT || event.keyCode == KeyCode.RALT;
1084         bool altPressed = !!(event.flags & KeyFlag.Alt);
1085         bool noOtherModifiers = !(event.flags & (KeyFlag.Shift | KeyFlag.Control));
1086         bool noAltGrKey = !((event.flags & KeyFlag.RAlt) == KeyFlag.RAlt);
1087 
1088         if (event.action == KeyAction.KeyDown && event.keyCode == KeyCode.ESCAPE && event.flags == 0 && activated) {
1089             deactivate();
1090             return true;
1091         }
1092         dchar hotkey = 0;
1093         if (event.action == KeyAction.KeyDown && event.keyCode >= KeyCode.KEY_A && event.keyCode <= KeyCode.KEY_Z && altPressed && noOtherModifiers && noAltGrKey) {
1094 //            Log.d("Alt + a..z");
1095             hotkey = cast(dchar)((event.keyCode - KeyCode.KEY_A) + 'a');
1096         }
1097         if (event.action == KeyAction.Text && altPressed && noOtherModifiers && noAltGrKey) {
1098             hotkey = event.text[0];
1099         }
1100         if (hotkey) {
1101             int index = _item.findSubitemByHotkey(hotkey);
1102             if (index >= 0) {
1103                 activate();
1104                 itemClicked(index);
1105                 return true;
1106             } else {
1107                 MenuItem item = _item.findSubitemByHotkeyRecursive(hotkey);
1108                 if (item) {
1109                     Log.d("found menu item recursive");
1110                     onMenuItem(item);
1111                     return true;
1112                 }
1113                 return false;
1114             }
1115         }
1116 
1117         // toggle menu by single Alt press - for Windows only!
1118         version (Windows) {
1119             if (event.action == KeyAction.KeyDown && isAlt && noOtherModifiers) {
1120                 _menuToggleState = 1;
1121             } else if (event.action == KeyAction.KeyUp && isAlt && noOtherModifiers) {
1122                 if (_menuToggleState == 1)
1123                     toggleMenu = true;
1124                 _menuToggleState = 0;
1125             } else {
1126                 _menuToggleState = 0;
1127             }
1128             if (toggleMenu) {
1129                 toggle();
1130                 return true;
1131             }
1132         }
1133         if (!focused)
1134             return false;
1135         if (_selectedItemIndex >= 0 && event.action == KeyAction.KeyDown && ((event.keyCode == KeyCode.DOWN) || (event.keyCode == KeyCode.SPACE) || (event.keyCode == KeyCode.RETURN))) {
1136             itemClicked(_selectedItemIndex);
1137             return true;
1138         }
1139         return super.onKeyEvent(event);
1140     }
1141 
1142     override @property protected uint overrideStateForItem() {
1143         uint res = state;
1144         if (_openedPopup)
1145             res |= State.Focused; // main menu with opened popup as focused for items display
1146         return res;
1147     }
1148 
1149 }
1150 
1151 
1152 /// popup menu widget (vertical layout of items)
1153 class PopupMenu : MenuWidgetBase {
1154 
1155     this(MenuItem item, MenuWidgetBase parentMenu = null) {
1156         super(parentMenu, item, Orientation.Vertical);
1157         id = "POPUP_MENU";
1158         styleId = STYLE_POPUP_MENU;
1159         selectOnHover = true;
1160     }
1161 }