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, bool selectFirstItem) { 623 debug(DebugMenus) Log.d("menu", id, " open submenu ", index); 624 if (_openedPopup !is null) { 625 if (_openedPopupIndex == index) { 626 if (selectFirstItem) { 627 window.setFocus(_openedMenu); 628 _openedMenu.selectItem(0); 629 } 630 return; 631 } else { 632 _openedPopup.close(); 633 _openedPopup = null; 634 } 635 } 636 637 if (isRecentlyClosedItem(itemWidget.item)) { 638 // don't reopen main menu item on duplicate click on the same menu item - deactivate instead 639 // deactivate main menu 640 deactivate(); 641 _ignoreItemSelection = itemWidget.item; 642 return; 643 } 644 645 PopupMenu popupMenu = new PopupMenu(itemWidget.item, this); 646 PopupWidget popup = window.showPopup(popupMenu, itemWidget, orientation == Orientation.Horizontal ? PopupAlign.Below : PopupAlign.Right); 647 requestActionsUpdate(); 648 popup.popupClosed = &onPopupClosed; 649 popup.flags = PopupFlags.CloseOnClickOutside; 650 _openedPopup = popup; 651 _openedMenu = popupMenu; 652 _openedPopupIndex = index; 653 _selectedItemIndex = index; 654 if (selectFirstItem) { 655 debug(DebugMenus) Log.d("menu: selecting first item"); 656 window.setFocus(popupMenu); 657 _openedMenu.selectItem(0); 658 } 659 } 660 661 enum MENU_OPEN_DELAY_MS = 400; 662 ulong _submenuOpenTimer = 0; 663 int _submenuOpenItemIndex = -1; 664 protected void scheduleOpenSubmenu(int index) { 665 if (_submenuOpenTimer) { 666 cancelTimer(_submenuOpenTimer); 667 _submenuOpenTimer = 0; 668 } 669 _submenuOpenItemIndex = index; 670 _submenuOpenTimer = setTimer(MENU_OPEN_DELAY_MS); 671 } 672 protected void cancelOpenSubmenu() { 673 if (_submenuOpenTimer) { 674 cancelTimer(_submenuOpenTimer); 675 _submenuOpenTimer = 0; 676 } 677 } 678 /// handle timer; return true to repeat timer event after next interval, false cancel timer 679 override bool onTimer(ulong id) { 680 if (id == _submenuOpenTimer) { 681 _submenuOpenTimer = 0; 682 MenuItemWidget itemWidget = _submenuOpenItemIndex >= 0 ? cast(MenuItemWidget)_adapter.itemWidget(_submenuOpenItemIndex) : null; 683 if (itemWidget !is null) { 684 if (itemWidget.item.isSubmenu()) { 685 Log.d("Opening submenu by timer"); 686 openSubmenu(_submenuOpenItemIndex, itemWidget, _orientation == Orientation.Horizontal); // for main menu, select first item 687 } else { 688 // normal item 689 } 690 } 691 } 692 // override to do something useful 693 // return true to repeat after the same interval, false to stop timer 694 return false; 695 } 696 697 698 protected MenuItem _ignoreItemSelection; 699 protected bool _selectionChangingInProgress; 700 /// override to handle change of selection 701 override protected void selectionChanged(int index, int previouslySelectedItem = -1) { 702 debug(DebugMenus) Log.d("menu ", id, " selectionChanged ", index, ", ", previouslySelectedItem, " _selectedItemIndex=", _selectedItemIndex); 703 _selectionChangingInProgress = true; 704 MenuItemWidget itemWidget = index >= 0 ? cast(MenuItemWidget)_adapter.itemWidget(index) : null; 705 MenuItemWidget prevWidget = previouslySelectedItem >= 0 ? cast(MenuItemWidget)_adapter.itemWidget(previouslySelectedItem) : null; 706 if (itemWidget._item is _ignoreItemSelection && isMainMenu) { 707 _ignoreItemSelection = null; 708 deactivate(); 709 return; 710 } 711 if (index >= 0) 712 setFocus(); 713 bool popupWasOpen = false; 714 if (prevWidget !is null) { 715 if (_openedPopup !is null) { 716 _openedPopup.close(); 717 _openedPopup = null; 718 popupWasOpen = true; 719 } 720 } 721 if (itemWidget !is null) { 722 if (itemWidget.item.isSubmenu()) { 723 if (_selectOnHover || popupWasOpen) { 724 if (popupWasOpen && _orientation == Orientation.Horizontal) { 725 // instantly open submenu in main menu if previous submenu was opened 726 openSubmenu(index, itemWidget, false); // _orientation == Orientation.Horizontal for main menu, select first item 727 } else { 728 if (!isMainMenu) 729 scheduleOpenSubmenu(index); 730 } 731 } 732 } else { 733 // normal item 734 } 735 } 736 _selectionChangingInProgress = false; 737 } 738 739 protected void handleMenuItemClick(MenuItem item) { 740 // precessing for CheckBox and RadioButton menus 741 if (item.type == MenuItemType.Check) { 742 item.checked = !item.checked; 743 } else if (item.type == MenuItemType.Radio) { 744 item.checked = true; 745 } 746 MenuItem p = item; 747 while (p) { 748 if (p.menuItemClick.assigned) { 749 p.menuItemClick(item); 750 break; 751 } 752 if (p.menuItemAction.assigned && item.action) { 753 p.menuItemAction(item.action); 754 break; 755 } 756 p = p._parent; 757 } 758 } 759 760 protected void onMenuItem(MenuItem item) { 761 debug(DebugMenus) Log.d("onMenuItem ", item.action.label); 762 if (_openedPopup !is null) { 763 _openedPopup.close(); 764 _openedPopup = null; 765 } 766 if (_parentMenu !is null) 767 _parentMenu.onMenuItem(item); 768 else { 769 // top level handling 770 debug(DebugMenus) Log.d("onMenuItem ", item.id); 771 selectItem(-1); 772 setHoverItem(-1); 773 selectOnHover = false; 774 775 // copy menu item click listeners 776 Signal!MenuItemClickHandler onMenuItemClickListenerCopy = menuItemClick; 777 // copy item action listeners 778 Signal!MenuItemActionHandler onMenuItemActionListenerCopy = menuItemAction; 779 780 handleMenuItemClick(item); 781 782 PopupWidget popup = cast(PopupWidget)parent; 783 if (popup) 784 popup.close(); 785 786 // this pointer now can be invalid - if popup removed 787 if (onMenuItemClickListenerCopy.assigned) 788 if (onMenuItemClickListenerCopy(item)) 789 return; 790 // this pointer now can be invalid - if popup removed 791 if (onMenuItemActionListenerCopy.assigned) 792 onMenuItemActionListenerCopy(item.action); 793 } 794 } 795 796 @property MenuItemWidget selectedMenuItemWidget() { 797 return _selectedItemIndex >= 0 ? cast(MenuItemWidget)_adapter.itemWidget(_selectedItemIndex) : null; 798 } 799 800 /// override to handle mouse up on item 801 override protected void itemClicked(int index) { 802 MenuItemWidget itemWidget = index >= 0 ? cast(MenuItemWidget)_adapter.itemWidget(index) : null; 803 if (itemWidget !is null) { 804 debug(DebugMenus) Log.d("Menu ", id, " Item clicked ", itemWidget.item.action.id); 805 if (itemWidget.item.isSubmenu()) { 806 // submenu clicked 807 if (_clickOnButtonDown && _openedPopup !is null && _openedMenu._item is itemWidget.item) { 808 809 if (_selectedItemIndex == index) { 810 _openedMenu.setFocus(); 811 return; 812 } 813 814 // second click on main menu opened item 815 _openedPopup.close(); 816 _openedPopup = null; 817 //selectItem(-1); 818 selectOnHover = false; 819 } else { 820 openSubmenu(index, itemWidget, _orientation == Orientation.Horizontal); // for main menu, select first item 821 selectOnHover = true; 822 } 823 } else { 824 // normal item 825 onMenuItem(itemWidget.item); 826 } 827 } 828 } 829 830 /// returns popup this menu is located in 831 @property PopupWidget thisPopup() { 832 return cast(PopupWidget)parent; 833 } 834 835 protected int _menuToggleState; 836 protected Widget _menuTogglePreviousFocus; 837 838 /// override to handle specific actions state (e.g. change enabled state for supported actions) 839 override bool handleActionStateRequest(const Action a) { 840 if (_menuTogglePreviousFocus) { 841 debug(DebugMenus) Log.d("Menu.handleActionStateRequest forwarding to ", _menuTogglePreviousFocus); 842 bool res = _menuTogglePreviousFocus.handleActionStateRequest(a); 843 debug(DebugMenus) Log.d("Menu.handleActionStateRequest forwarding handled successful: ", a.state.toString); 844 return res; 845 } 846 return false; 847 } 848 849 /// list navigation using keys 850 override bool onKeyEvent(KeyEvent event) { 851 if (orientation == Orientation.Horizontal) { 852 // no special processing 853 if (event.action == KeyAction.KeyDown) { 854 if (event.keyCode == KeyCode.ESCAPE) { 855 close(); 856 return true; 857 } 858 } 859 } else { 860 // for vertical (popup) menu 861 if (!focused) 862 return false; 863 if (event.action == KeyAction.KeyDown) { 864 if (event.keyCode == KeyCode.LEFT) { 865 if (_parentMenu !is null) { 866 if (_parentMenu.orientation == Orientation.Vertical) { 867 if (thisPopup !is null) { 868 //int selectedItem = _selectedItemIndex; 869 // back to parent menu on Left key 870 thisPopup.close(); 871 //if (selectedItem >= 0) 872 // selectItem(selectedItem); 873 return true; 874 } 875 } else { 876 // parent is main menu 877 _parentMenu.moveSelection(-1); 878 return true; 879 } 880 } 881 return true; 882 } else if (event.keyCode == KeyCode.RIGHT) { 883 MenuItemWidget thisItem = selectedMenuItemWidget(); 884 if (thisItem !is null && thisItem.item.isSubmenu) { 885 openSubmenu(_selectedItemIndex, thisItem, true); 886 return true; 887 } else if (_parentMenu !is null && _parentMenu.orientation == Orientation.Horizontal) { 888 _parentMenu.moveSelection(1); 889 return true; 890 } 891 return true; 892 } else if (event.keyCode == KeyCode.ESCAPE) { 893 close(); 894 return true; 895 } 896 } else if (event.action == KeyAction.KeyUp) { 897 if (event.keyCode == KeyCode.LEFT || event.keyCode == KeyCode.RIGHT) { 898 return true; 899 } 900 } else if (event.action == KeyAction.Text && event.flags == 0) { 901 dchar ch = event.text[0]; 902 int index = _item.findSubitemByHotkey(ch); 903 if (index >= 0) { 904 itemClicked(index); 905 return true; 906 } 907 } 908 } 909 if (_selectedItemIndex >= 0 && event.action == KeyAction.KeyDown && /*event.flags == 0 &&*/ (event.keyCode == KeyCode.RETURN || event.keyCode == KeyCode.SPACE)) { 910 itemClicked(_selectedItemIndex); 911 return true; 912 } 913 bool res = super.onKeyEvent(event); 914 return res; 915 } 916 /// closes this menu - handle ESC key 917 void close() { 918 cancelOpenSubmenu(); 919 if (thisPopup !is null) 920 thisPopup.close(); 921 } 922 923 /// map key to action 924 override Action findKeyAction(uint keyCode, uint flags) { 925 if (!_item) 926 return null; 927 Action action = _item.findKeyAction(keyCode, flags); 928 return action; 929 } 930 931 } 932 933 /// main menu (horizontal) 934 class MainMenu : MenuWidgetBase { 935 936 this() { 937 super(null, null, Orientation.Horizontal); 938 id = "MAIN_MENU"; 939 styleId = STYLE_MAIN_MENU; 940 _clickOnButtonDown = false; 941 selectOnHover = false; 942 } 943 944 this(MenuItem item) { 945 super(null, item, Orientation.Horizontal); 946 id = "MAIN_MENU"; 947 styleId = STYLE_MAIN_MENU; 948 _clickOnButtonDown = false; 949 selectOnHover = false; 950 } 951 952 /// call to update state for action (if action is assigned for widget) 953 override void updateActionState(bool force) { 954 //Log.d("MainMenu: updateActionState"); 955 //_item.updateActionState(this); 956 957 } 958 959 /// override and return true to track key events even when not focused 960 @property override bool wantsKeyTracking() { 961 return true; 962 } 963 964 /// get text flags (bit set of TextFlag enum values) 965 @property override uint textFlags() { 966 // override text flags for main menu 967 if (_selectedItemIndex >= 0) 968 return TextFlag.UnderlineHotKeys | TextFlag.HotKeys; 969 else 970 return TextFlag.UnderlineHotKeysWhenAltPressed | TextFlag.HotKeys; 971 } 972 973 protected int _menuToggleState; 974 protected Widget _menuTogglePreviousFocus; 975 976 override protected void onMenuItem(MenuItem item) { 977 debug(DebugMenus) Log.d("MainMenu.onMenuItem ", item.action.label); 978 979 // copy menu item click listeners 980 Signal!MenuItemClickHandler onMenuItemClickListenerCopy = menuItemClick; 981 // copy item action listeners 982 Signal!MenuItemActionHandler onMenuItemActionListenerCopy = menuItemAction; 983 984 deactivate(); 985 986 handleMenuItemClick(item); 987 988 // this pointer now can be invalid - if popup removed 989 if (onMenuItemClickListenerCopy.assigned) 990 if (onMenuItemClickListenerCopy(item)) 991 return; 992 // this pointer now can be invalid - if popup removed 993 if (onMenuItemActionListenerCopy.assigned) 994 onMenuItemActionListenerCopy(item.action); 995 } 996 997 /// return true if main menu is activated (focused or has open submenu) 998 @property bool activated() { 999 return focused || _selectedItemIndex >= 0 || _openedPopup !is null; 1000 } 1001 1002 override protected void performUndoSelection() { 1003 deactivate(); 1004 } 1005 1006 /// closes this menu - ESC handling 1007 override void close() { 1008 debug(DebugMenus) Log.d("menu ", id, " close called"); 1009 if (_openedPopup !is null) { 1010 _openedPopup.close(); 1011 _openedPopup = null; 1012 } else 1013 deactivate(); 1014 } 1015 1016 /// request relayout of widget and its children 1017 //override void requestLayout() { 1018 // Log.d("MainMenu.requestLayout is called"); 1019 // super.requestLayout(); 1020 //} 1021 1022 /// bring focus to main menu, if not yet activated 1023 void activate() { 1024 debug(DebugMenus) Log.d("activating main menu"); 1025 if (activated) 1026 return; 1027 window.setFocus(this); 1028 selectItem(0); 1029 } 1030 1031 /// close and remove focus, if activated 1032 override void deactivate(bool force = false) { 1033 debug(DebugMenus) Log.d("deactivating main menu"); 1034 if (!activated && !force) 1035 return; 1036 if (_openedPopup !is null) { 1037 _openedPopup.close(); 1038 _openedPopup = null; 1039 } 1040 selectItem(-1); 1041 setHoverItem(-1); 1042 selectOnHover = false; 1043 window.setFocus(_menuTogglePreviousFocus); 1044 } 1045 1046 /// activate or deactivate main menu, return true if it has been activated 1047 bool toggle() { 1048 if (activated) { 1049 // unfocus 1050 deactivate(); 1051 return false; 1052 } else { 1053 // focus 1054 activate(); 1055 return true; 1056 } 1057 1058 } 1059 1060 /// override to handle focus changes 1061 override protected void handleFocusChange(bool focused, bool receivedFocusFromKeyboard = false) { 1062 debug(DebugMenus) Log.d("menu ", id, "handling focus change to ", focused); 1063 if (focused && _openedPopup is null) { 1064 // activating! 1065 _menuTogglePreviousFocus = window.focusedWidget; 1066 //updateActionState(true); 1067 debug(DebugMenus) Log.d("MainMenu: updateActionState"); 1068 _item.updateActionState(this); 1069 } 1070 super.handleFocusChange(focused); 1071 } 1072 1073 /// list navigation using keys 1074 override bool onKeyEvent(KeyEvent event) { 1075 // handle MainMenu activation / deactivation (Alt, Esc...) 1076 bool toggleMenu = false; 1077 bool isAlt = event.keyCode == KeyCode.ALT || event.keyCode == KeyCode.LALT || event.keyCode == KeyCode.RALT; 1078 bool altPressed = !!(event.flags & KeyFlag.Alt); 1079 bool noOtherModifiers = !(event.flags & (KeyFlag.Shift | KeyFlag.Control)); 1080 bool noAltGrKey = !((event.flags & KeyFlag.RAlt) == KeyFlag.RAlt); 1081 1082 if (event.action == KeyAction.KeyDown && event.keyCode == KeyCode.ESCAPE && event.flags == 0 && activated) { 1083 deactivate(); 1084 return true; 1085 } 1086 dchar hotkey = 0; 1087 if (event.action == KeyAction.KeyDown && event.keyCode >= KeyCode.KEY_A && event.keyCode <= KeyCode.KEY_Z && altPressed && noOtherModifiers && noAltGrKey) { 1088 // Log.d("Alt + a..z"); 1089 hotkey = cast(dchar)((event.keyCode - KeyCode.KEY_A) + 'a'); 1090 } 1091 if (event.action == KeyAction.Text && altPressed && noOtherModifiers && noAltGrKey) { 1092 hotkey = event.text[0]; 1093 } 1094 if (hotkey) { 1095 int index = _item.findSubitemByHotkey(hotkey); 1096 if (index >= 0) { 1097 activate(); 1098 itemClicked(index); 1099 return true; 1100 } else { 1101 MenuItem item = _item.findSubitemByHotkeyRecursive(hotkey); 1102 if (item) { 1103 Log.d("found menu item recursive"); 1104 onMenuItem(item); 1105 return true; 1106 } 1107 return false; 1108 } 1109 } 1110 1111 // toggle menu by single Alt press - for Windows only! 1112 version (Windows) { 1113 if (event.action == KeyAction.KeyDown && isAlt && noOtherModifiers) { 1114 _menuToggleState = 1; 1115 } else if (event.action == KeyAction.KeyUp && isAlt && noOtherModifiers) { 1116 if (_menuToggleState == 1) 1117 toggleMenu = true; 1118 _menuToggleState = 0; 1119 } else { 1120 _menuToggleState = 0; 1121 } 1122 if (toggleMenu) { 1123 toggle(); 1124 return true; 1125 } 1126 } 1127 if (!focused) 1128 return false; 1129 if (_selectedItemIndex >= 0 && event.action == KeyAction.KeyDown && ((event.keyCode == KeyCode.DOWN) || (event.keyCode == KeyCode.SPACE) || (event.keyCode == KeyCode.RETURN))) { 1130 itemClicked(_selectedItemIndex); 1131 return true; 1132 } 1133 return super.onKeyEvent(event); 1134 } 1135 1136 override @property protected uint overrideStateForItem() { 1137 uint res = state; 1138 if (_openedPopup) 1139 res |= State.Focused; // main menu with opened popup as focused for items display 1140 return res; 1141 } 1142 1143 } 1144 1145 1146 /// popup menu widget (vertical layout of items) 1147 class PopupMenu : MenuWidgetBase { 1148 1149 this(MenuItem item, MenuWidgetBase parentMenu = null) { 1150 super(parentMenu, item, Orientation.Vertical); 1151 id = "POPUP_MENU"; 1152 styleId = STYLE_POPUP_MENU; 1153 selectOnHover = true; 1154 } 1155 }