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 }