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 }