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