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