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