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