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