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 }