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