1 /* 2 * Copyright 2001-2015 Haiku, Inc. All rights reserved. 3 * Distributed under the terms of the MIT license. 4 * 5 * Authors: 6 * Stephan Aßmus, superstippi@gmx.de 7 * Stefano Ceccherini, stefano.ceccherini@gmail.com 8 * Marc Flerackers, mflerackers@androme.be 9 * Rene Gollent, anevilyak@gmail.com 10 * John Scipione, jscipione@gmail.com 11 */ 12 13 14 #include <Menu.h> 15 16 #include <algorithm> 17 #include <new> 18 19 #include <ctype.h> 20 #include <string.h> 21 22 #include <Application.h> 23 #include <Bitmap.h> 24 #include <ControlLook.h> 25 #include <Debug.h> 26 #include <File.h> 27 #include <FindDirectory.h> 28 #include <Layout.h> 29 #include <LayoutUtils.h> 30 #include <MenuBar.h> 31 #include <MenuItem.h> 32 #include <Messenger.h> 33 #include <Path.h> 34 #include <PropertyInfo.h> 35 #include <Screen.h> 36 #include <ScrollBar.h> 37 #include <SystemCatalog.h> 38 #include <Window.h> 39 40 #include <AppServerLink.h> 41 #include <binary_compatibility/Interface.h> 42 #include <BMCPrivate.h> 43 #include <MenuPrivate.h> 44 #include <MenuWindow.h> 45 #include <ServerProtocol.h> 46 47 #include "utf8_functions.h" 48 49 50 #define USE_CACHED_MENUWINDOW 1 51 52 using BPrivate::gSystemCatalog; 53 54 #undef B_TRANSLATION_CONTEXT 55 #define B_TRANSLATION_CONTEXT "Menu" 56 57 #undef B_TRANSLATE 58 #define B_TRANSLATE(str) \ 59 gSystemCatalog.GetString(B_TRANSLATE_MARK(str), "Menu") 60 61 62 using std::nothrow; 63 using BPrivate::BMenuWindow; 64 65 namespace BPrivate { 66 67 class TriggerList { 68 public: 69 TriggerList() {} 70 ~TriggerList() {} 71 72 // TODO: make this work with Unicode characters! 73 74 bool HasTrigger(uint32 c) 75 { return fList.HasItem((void*)(addr_t)tolower(c)); } 76 bool AddTrigger(uint32 c) 77 { return fList.AddItem((void*)(addr_t)tolower(c)); } 78 79 private: 80 BList fList; 81 }; 82 83 84 class ExtraMenuData { 85 public: 86 menu_tracking_hook trackingHook; 87 void* trackingState; 88 89 ExtraMenuData(menu_tracking_hook func, void* state) 90 { 91 trackingHook = func; 92 trackingState = state; 93 } 94 }; 95 96 97 } // namespace BPrivate 98 99 100 menu_info BMenu::sMenuInfo; 101 102 uint32 BMenu::sShiftKey; 103 uint32 BMenu::sControlKey; 104 uint32 BMenu::sOptionKey; 105 uint32 BMenu::sCommandKey; 106 uint32 BMenu::sMenuKey; 107 108 static property_info sPropList[] = { 109 { "Enabled", { B_GET_PROPERTY, 0 }, 110 { B_DIRECT_SPECIFIER, 0 }, "Returns true if menu or menu item is " 111 "enabled; false otherwise.", 112 0, { B_BOOL_TYPE } 113 }, 114 115 { "Enabled", { B_SET_PROPERTY, 0 }, 116 { B_DIRECT_SPECIFIER, 0 }, "Enables or disables menu or menu item.", 117 0, { B_BOOL_TYPE } 118 }, 119 120 { "Label", { B_GET_PROPERTY, 0 }, 121 { B_DIRECT_SPECIFIER, 0 }, "Returns the string label of the menu or " 122 "menu item.", 123 0, { B_STRING_TYPE } 124 }, 125 126 { "Label", { B_SET_PROPERTY, 0 }, 127 { B_DIRECT_SPECIFIER, 0 }, "Sets the string label of the menu or menu " 128 "item.", 129 0, { B_STRING_TYPE } 130 }, 131 132 { "Mark", { B_GET_PROPERTY, 0 }, 133 { B_DIRECT_SPECIFIER, 0 }, "Returns true if the menu item or the " 134 "menu's superitem is marked; false otherwise.", 135 0, { B_BOOL_TYPE } 136 }, 137 138 { "Mark", { B_SET_PROPERTY, 0 }, 139 { B_DIRECT_SPECIFIER, 0 }, "Marks or unmarks the menu item or the " 140 "menu's superitem.", 141 0, { B_BOOL_TYPE } 142 }, 143 144 { "Menu", { B_CREATE_PROPERTY, 0 }, 145 { B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 }, 146 "Adds a new menu item at the specified index with the text label " 147 "found in \"data\" and the int32 command found in \"what\" (used as " 148 "the what field in the BMessage sent by the item)." , 0, {}, 149 { {{{"data", B_STRING_TYPE}}} 150 } 151 }, 152 153 { "Menu", { B_DELETE_PROPERTY, 0 }, 154 { B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 }, 155 "Removes the selected menu or menus.", 0, {} 156 }, 157 158 { "Menu", { }, 159 { B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 }, 160 "Directs scripting message to the specified menu, first popping the " 161 "current specifier off the stack.", 0, {} 162 }, 163 164 { "MenuItem", { B_COUNT_PROPERTIES, 0 }, 165 { B_DIRECT_SPECIFIER, 0 }, "Counts the number of menu items in the " 166 "specified menu.", 167 0, { B_INT32_TYPE } 168 }, 169 170 { "MenuItem", { B_CREATE_PROPERTY, 0 }, 171 { B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 }, 172 "Adds a new menu item at the specified index with the text label " 173 "found in \"data\" and the int32 command found in \"what\" (used as " 174 "the what field in the BMessage sent by the item).", 0, {}, 175 { { {{"data", B_STRING_TYPE }, 176 {"be:invoke_message", B_MESSAGE_TYPE}, 177 {"what", B_INT32_TYPE}, 178 {"be:target", B_MESSENGER_TYPE}} } 179 } 180 }, 181 182 { "MenuItem", { B_DELETE_PROPERTY, 0 }, 183 { B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 }, 184 "Removes the specified menu item from its parent menu." 185 }, 186 187 { "MenuItem", { B_EXECUTE_PROPERTY, 0 }, 188 { B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 }, 189 "Invokes the specified menu item." 190 }, 191 192 { "MenuItem", { }, 193 { B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 }, 194 "Directs scripting message to the specified menu, first popping the " 195 "current specifier off the stack." 196 }, 197 198 {} 199 }; 200 201 202 // note: this is redefined to localized one in BMenu::_InitData 203 const char* BPrivate::kEmptyMenuLabel = "<empty>"; 204 205 206 struct BMenu::LayoutData { 207 BSize preferred; 208 uint32 lastResizingMode; 209 }; 210 211 212 // #pragma mark - BMenu 213 214 215 BMenu::BMenu(const char* name, menu_layout layout) 216 : 217 BView(BRect(0, 0, 0, 0), name, 0, B_WILL_DRAW), 218 fChosenItem(NULL), 219 fPad(std::max(14.0f, be_plain_font->Size() + 2.0f), 2.0f, 20.0f, 0.0f), 220 fSelected(NULL), 221 fCachedMenuWindow(NULL), 222 fSuper(NULL), 223 fSuperitem(NULL), 224 fAscent(-1.0f), 225 fDescent(-1.0f), 226 fFontHeight(-1.0f), 227 fState(MENU_STATE_CLOSED), 228 fLayout(layout), 229 fExtraRect(NULL), 230 fMaxContentWidth(0.0f), 231 fInitMatrixSize(NULL), 232 fExtraMenuData(NULL), 233 fTrigger(0), 234 fResizeToFit(true), 235 fUseCachedMenuLayout(false), 236 fEnabled(true), 237 fDynamicName(false), 238 fRadioMode(false), 239 fTrackNewBounds(false), 240 fStickyMode(false), 241 fIgnoreHidden(true), 242 fTriggerEnabled(true), 243 fRedrawAfterSticky(false), 244 fAttachAborted(false) 245 { 246 _InitData(NULL); 247 } 248 249 250 BMenu::BMenu(const char* name, float width, float height) 251 : 252 BView(BRect(0.0f, 0.0f, 0.0f, 0.0f), name, 0, B_WILL_DRAW), 253 fChosenItem(NULL), 254 fPad(14.0f, 2.0f, 20.0f, 0.0f), 255 fSelected(NULL), 256 fCachedMenuWindow(NULL), 257 fSuper(NULL), 258 fSuperitem(NULL), 259 fAscent(-1.0f), 260 fDescent(-1.0f), 261 fFontHeight(-1.0f), 262 fState(0), 263 fLayout(B_ITEMS_IN_MATRIX), 264 fExtraRect(NULL), 265 fMaxContentWidth(0.0f), 266 fInitMatrixSize(NULL), 267 fExtraMenuData(NULL), 268 fTrigger(0), 269 fResizeToFit(true), 270 fUseCachedMenuLayout(false), 271 fEnabled(true), 272 fDynamicName(false), 273 fRadioMode(false), 274 fTrackNewBounds(false), 275 fStickyMode(false), 276 fIgnoreHidden(true), 277 fTriggerEnabled(true), 278 fRedrawAfterSticky(false), 279 fAttachAborted(false) 280 { 281 _InitData(NULL); 282 } 283 284 285 BMenu::BMenu(BMessage* archive) 286 : 287 BView(archive), 288 fChosenItem(NULL), 289 fPad(14.0f, 2.0f, 20.0f, 0.0f), 290 fSelected(NULL), 291 fCachedMenuWindow(NULL), 292 fSuper(NULL), 293 fSuperitem(NULL), 294 fAscent(-1.0f), 295 fDescent(-1.0f), 296 fFontHeight(-1.0f), 297 fState(MENU_STATE_CLOSED), 298 fLayout(B_ITEMS_IN_ROW), 299 fExtraRect(NULL), 300 fMaxContentWidth(0.0f), 301 fInitMatrixSize(NULL), 302 fExtraMenuData(NULL), 303 fTrigger(0), 304 fResizeToFit(true), 305 fUseCachedMenuLayout(false), 306 fEnabled(true), 307 fDynamicName(false), 308 fRadioMode(false), 309 fTrackNewBounds(false), 310 fStickyMode(false), 311 fIgnoreHidden(true), 312 fTriggerEnabled(true), 313 fRedrawAfterSticky(false), 314 fAttachAborted(false) 315 { 316 _InitData(archive); 317 } 318 319 320 BMenu::~BMenu() 321 { 322 _DeleteMenuWindow(); 323 324 RemoveItems(0, CountItems(), true); 325 326 delete fInitMatrixSize; 327 delete fExtraMenuData; 328 delete fLayoutData; 329 } 330 331 332 BArchivable* 333 BMenu::Instantiate(BMessage* archive) 334 { 335 if (validate_instantiation(archive, "BMenu")) 336 return new (nothrow) BMenu(archive); 337 338 return NULL; 339 } 340 341 342 status_t 343 BMenu::Archive(BMessage* data, bool deep) const 344 { 345 status_t err = BView::Archive(data, deep); 346 347 if (err == B_OK && Layout() != B_ITEMS_IN_ROW) 348 err = data->AddInt32("_layout", Layout()); 349 if (err == B_OK) 350 err = data->AddBool("_rsize_to_fit", fResizeToFit); 351 if (err == B_OK) 352 err = data->AddBool("_disable", !IsEnabled()); 353 if (err == B_OK) 354 err = data->AddBool("_radio", IsRadioMode()); 355 if (err == B_OK) 356 err = data->AddBool("_trig_disabled", AreTriggersEnabled()); 357 if (err == B_OK) 358 err = data->AddBool("_dyn_label", fDynamicName); 359 if (err == B_OK) 360 err = data->AddFloat("_maxwidth", fMaxContentWidth); 361 if (err == B_OK && deep) { 362 BMenuItem* item = NULL; 363 int32 index = 0; 364 while ((item = ItemAt(index++)) != NULL) { 365 BMessage itemData; 366 item->Archive(&itemData, deep); 367 err = data->AddMessage("_items", &itemData); 368 if (err != B_OK) 369 break; 370 if (fLayout == B_ITEMS_IN_MATRIX) { 371 err = data->AddRect("_i_frames", item->fBounds); 372 } 373 } 374 } 375 376 return err; 377 } 378 379 380 void 381 BMenu::AttachedToWindow() 382 { 383 BView::AttachedToWindow(); 384 385 _GetShiftKey(sShiftKey); 386 _GetControlKey(sControlKey); 387 _GetCommandKey(sCommandKey); 388 _GetOptionKey(sOptionKey); 389 _GetMenuKey(sMenuKey); 390 391 fAttachAborted = _AddDynamicItems(); 392 393 if (!fAttachAborted) { 394 _CacheFontInfo(); 395 _LayoutItems(0); 396 _UpdateWindowViewSize(false); 397 } 398 } 399 400 401 void 402 BMenu::DetachedFromWindow() 403 { 404 BView::DetachedFromWindow(); 405 } 406 407 408 void 409 BMenu::AllAttached() 410 { 411 BView::AllAttached(); 412 } 413 414 415 void 416 BMenu::AllDetached() 417 { 418 BView::AllDetached(); 419 } 420 421 422 void 423 BMenu::Draw(BRect updateRect) 424 { 425 if (_RelayoutIfNeeded()) { 426 Invalidate(); 427 return; 428 } 429 430 DrawBackground(updateRect); 431 DrawItems(updateRect); 432 } 433 434 435 void 436 BMenu::MessageReceived(BMessage* message) 437 { 438 switch (message->what) { 439 case B_MOUSE_WHEEL_CHANGED: 440 { 441 float deltaY = 0; 442 message->FindFloat("be:wheel_delta_y", &deltaY); 443 if (deltaY == 0) 444 return; 445 446 BMenuWindow* window = dynamic_cast<BMenuWindow*>(Window()); 447 if (window == NULL) 448 return; 449 450 float largeStep; 451 float smallStep; 452 window->GetSteps(&smallStep, &largeStep); 453 454 // pressing the shift key scrolls faster 455 if ((modifiers() & B_SHIFT_KEY) != 0) 456 deltaY *= largeStep; 457 else 458 deltaY *= smallStep; 459 460 window->TryScrollBy(deltaY); 461 break; 462 } 463 464 default: 465 BView::MessageReceived(message); 466 break; 467 } 468 } 469 470 471 void 472 BMenu::KeyDown(const char* bytes, int32 numBytes) 473 { 474 // TODO: Test how it works on BeOS R5 and implement this correctly 475 switch (bytes[0]) { 476 case B_UP_ARROW: 477 if (fLayout == B_ITEMS_IN_COLUMN) 478 _SelectNextItem(fSelected, false); 479 break; 480 481 case B_DOWN_ARROW: 482 { 483 BMenuBar* bar = dynamic_cast<BMenuBar*>(Supermenu()); 484 if (bar != NULL && fState == MENU_STATE_CLOSED) { 485 // tell MenuBar's _Track: 486 bar->fState = MENU_STATE_KEY_TO_SUBMENU; 487 } 488 if (fLayout == B_ITEMS_IN_COLUMN) 489 _SelectNextItem(fSelected, true); 490 break; 491 } 492 493 case B_LEFT_ARROW: 494 if (fLayout == B_ITEMS_IN_ROW) 495 _SelectNextItem(fSelected, false); 496 else { 497 // this case has to be handled a bit specially. 498 BMenuItem* item = Superitem(); 499 if (item) { 500 if (dynamic_cast<BMenuBar*>(Supermenu())) { 501 // If we're at the top menu below the menu bar, pass 502 // the keypress to the menu bar so we can move to 503 // another top level menu. 504 BMessenger messenger(Supermenu()); 505 messenger.SendMessage(Window()->CurrentMessage()); 506 } else { 507 // tell _Track 508 fState = MENU_STATE_KEY_LEAVE_SUBMENU; 509 } 510 } 511 } 512 break; 513 514 case B_RIGHT_ARROW: 515 if (fLayout == B_ITEMS_IN_ROW) 516 _SelectNextItem(fSelected, true); 517 else { 518 if (fSelected != NULL && fSelected->Submenu() != NULL) { 519 fSelected->Submenu()->_SetStickyMode(true); 520 // fix me: this shouldn't be needed but dynamic menus 521 // aren't getting it set correctly when keyboard 522 // navigating, which aborts the attach 523 fState = MENU_STATE_KEY_TO_SUBMENU; 524 _SelectItem(fSelected, true, true, true); 525 } else if (dynamic_cast<BMenuBar*>(Supermenu())) { 526 // if we have no submenu and we're an 527 // item in the top menu below the menubar, 528 // pass the keypress to the menubar 529 // so you can use the keypress to switch menus. 530 BMessenger messenger(Supermenu()); 531 messenger.SendMessage(Window()->CurrentMessage()); 532 } 533 } 534 break; 535 536 case B_PAGE_UP: 537 case B_PAGE_DOWN: 538 { 539 BMenuWindow* window = dynamic_cast<BMenuWindow*>(Window()); 540 if (window == NULL || !window->HasScrollers()) 541 break; 542 543 int32 deltaY = bytes[0] == B_PAGE_UP ? -1 : 1; 544 545 float largeStep; 546 window->GetSteps(NULL, &largeStep); 547 window->TryScrollBy(deltaY * largeStep); 548 break; 549 } 550 551 case B_ENTER: 552 case B_SPACE: 553 if (fSelected != NULL) { 554 fChosenItem = fSelected; 555 // preserve for exit handling 556 _QuitTracking(false); 557 } 558 break; 559 560 case B_ESCAPE: 561 _SelectItem(NULL); 562 if (fState == MENU_STATE_CLOSED 563 && dynamic_cast<BMenuBar*>(Supermenu())) { 564 // Keyboard may show menu without tracking it 565 BMessenger messenger(Supermenu()); 566 messenger.SendMessage(Window()->CurrentMessage()); 567 } else 568 _QuitTracking(false); 569 break; 570 571 default: 572 { 573 uint32 trigger = UTF8ToCharCode(&bytes); 574 575 for (uint32 i = CountItems(); i-- > 0;) { 576 BMenuItem* item = ItemAt(i); 577 if (item->fTriggerIndex < 0 || item->fTrigger != trigger) 578 continue; 579 580 _InvokeItem(item); 581 _QuitTracking(false); 582 break; 583 } 584 break; 585 } 586 } 587 } 588 589 590 BSize 591 BMenu::MinSize() 592 { 593 _ValidatePreferredSize(); 594 595 BSize size = (GetLayout() != NULL ? GetLayout()->MinSize() 596 : fLayoutData->preferred); 597 598 return BLayoutUtils::ComposeSize(ExplicitMinSize(), size); 599 } 600 601 602 BSize 603 BMenu::MaxSize() 604 { 605 _ValidatePreferredSize(); 606 607 BSize size = (GetLayout() != NULL ? GetLayout()->MaxSize() 608 : fLayoutData->preferred); 609 610 return BLayoutUtils::ComposeSize(ExplicitMaxSize(), size); 611 } 612 613 614 BSize 615 BMenu::PreferredSize() 616 { 617 _ValidatePreferredSize(); 618 619 BSize size = (GetLayout() != NULL ? GetLayout()->PreferredSize() 620 : fLayoutData->preferred); 621 622 return BLayoutUtils::ComposeSize(ExplicitPreferredSize(), size); 623 } 624 625 626 void 627 BMenu::GetPreferredSize(float* _width, float* _height) 628 { 629 _ValidatePreferredSize(); 630 631 if (_width) 632 *_width = fLayoutData->preferred.width; 633 634 if (_height) 635 *_height = fLayoutData->preferred.height; 636 } 637 638 639 void 640 BMenu::ResizeToPreferred() 641 { 642 BView::ResizeToPreferred(); 643 } 644 645 646 void 647 BMenu::DoLayout() 648 { 649 // If the user set a layout, we let the base class version call its 650 // hook. 651 if (GetLayout() != NULL) { 652 BView::DoLayout(); 653 return; 654 } 655 656 if (_RelayoutIfNeeded()) 657 Invalidate(); 658 } 659 660 661 void 662 BMenu::FrameMoved(BPoint new_position) 663 { 664 BView::FrameMoved(new_position); 665 } 666 667 668 void 669 BMenu::FrameResized(float new_width, float new_height) 670 { 671 BView::FrameResized(new_width, new_height); 672 } 673 674 675 void 676 BMenu::InvalidateLayout() 677 { 678 fUseCachedMenuLayout = false; 679 // This method exits for backwards compatibility reasons, it is used to 680 // invalidate the menu layout, but we also use call 681 // BView::InvalidateLayout() for good measure. Don't delete this method! 682 BView::InvalidateLayout(false); 683 } 684 685 686 void 687 BMenu::MakeFocus(bool focused) 688 { 689 BView::MakeFocus(focused); 690 } 691 692 693 bool 694 BMenu::AddItem(BMenuItem* item) 695 { 696 return AddItem(item, CountItems()); 697 } 698 699 700 bool 701 BMenu::AddItem(BMenuItem* item, int32 index) 702 { 703 if (fLayout == B_ITEMS_IN_MATRIX) { 704 debugger("BMenu::AddItem(BMenuItem*, int32) this method can only " 705 "be called if the menu layout is not B_ITEMS_IN_MATRIX"); 706 } 707 708 if (item == NULL || !_AddItem(item, index)) 709 return false; 710 711 InvalidateLayout(); 712 if (LockLooper()) { 713 if (!Window()->IsHidden()) { 714 _LayoutItems(index); 715 _UpdateWindowViewSize(false); 716 Invalidate(); 717 } 718 UnlockLooper(); 719 } 720 721 return true; 722 } 723 724 725 bool 726 BMenu::AddItem(BMenuItem* item, BRect frame) 727 { 728 if (fLayout != B_ITEMS_IN_MATRIX) { 729 debugger("BMenu::AddItem(BMenuItem*, BRect) this method can only " 730 "be called if the menu layout is B_ITEMS_IN_MATRIX"); 731 } 732 733 if (item == NULL) 734 return false; 735 736 item->fBounds = frame; 737 738 int32 index = CountItems(); 739 if (!_AddItem(item, index)) 740 return false; 741 742 if (LockLooper()) { 743 if (!Window()->IsHidden()) { 744 _LayoutItems(index); 745 Invalidate(); 746 } 747 UnlockLooper(); 748 } 749 750 return true; 751 } 752 753 754 bool 755 BMenu::AddItem(BMenu* submenu) 756 { 757 BMenuItem* item = new (nothrow) BMenuItem(submenu); 758 if (item == NULL) 759 return false; 760 761 if (!AddItem(item, CountItems())) { 762 item->fSubmenu = NULL; 763 delete item; 764 return false; 765 } 766 767 return true; 768 } 769 770 771 bool 772 BMenu::AddItem(BMenu* submenu, int32 index) 773 { 774 if (fLayout == B_ITEMS_IN_MATRIX) { 775 debugger("BMenu::AddItem(BMenuItem*, int32) this method can only " 776 "be called if the menu layout is not B_ITEMS_IN_MATRIX"); 777 } 778 779 BMenuItem* item = new (nothrow) BMenuItem(submenu); 780 if (item == NULL) 781 return false; 782 783 if (!AddItem(item, index)) { 784 item->fSubmenu = NULL; 785 delete item; 786 return false; 787 } 788 789 return true; 790 } 791 792 793 bool 794 BMenu::AddItem(BMenu* submenu, BRect frame) 795 { 796 if (fLayout != B_ITEMS_IN_MATRIX) { 797 debugger("BMenu::AddItem(BMenu*, BRect) this method can only " 798 "be called if the menu layout is B_ITEMS_IN_MATRIX"); 799 } 800 801 BMenuItem* item = new (nothrow) BMenuItem(submenu); 802 if (item == NULL) 803 return false; 804 805 if (!AddItem(item, frame)) { 806 item->fSubmenu = NULL; 807 delete item; 808 return false; 809 } 810 811 return true; 812 } 813 814 815 bool 816 BMenu::AddList(BList* list, int32 index) 817 { 818 // TODO: test this function, it's not documented in the bebook. 819 if (list == NULL) 820 return false; 821 822 bool locked = LockLooper(); 823 824 int32 numItems = list->CountItems(); 825 for (int32 i = 0; i < numItems; i++) { 826 BMenuItem* item = static_cast<BMenuItem*>(list->ItemAt(i)); 827 if (item != NULL) { 828 if (!_AddItem(item, index + i)) 829 break; 830 } 831 } 832 833 InvalidateLayout(); 834 if (locked && Window() != NULL && !Window()->IsHidden()) { 835 // Make sure we update the layout if needed. 836 _LayoutItems(index); 837 _UpdateWindowViewSize(false); 838 Invalidate(); 839 } 840 841 if (locked) 842 UnlockLooper(); 843 844 return true; 845 } 846 847 848 bool 849 BMenu::AddSeparatorItem() 850 { 851 BMenuItem* item = new (nothrow) BSeparatorItem(); 852 if (!item || !AddItem(item, CountItems())) { 853 delete item; 854 return false; 855 } 856 857 return true; 858 } 859 860 861 bool 862 BMenu::RemoveItem(BMenuItem* item) 863 { 864 return _RemoveItems(0, 0, item, false); 865 } 866 867 868 BMenuItem* 869 BMenu::RemoveItem(int32 index) 870 { 871 BMenuItem* item = ItemAt(index); 872 if (item != NULL) 873 _RemoveItems(0, 0, item, false); 874 return item; 875 } 876 877 878 bool 879 BMenu::RemoveItems(int32 index, int32 count, bool deleteItems) 880 { 881 return _RemoveItems(index, count, NULL, deleteItems); 882 } 883 884 885 bool 886 BMenu::RemoveItem(BMenu* submenu) 887 { 888 for (int32 i = 0; i < fItems.CountItems(); i++) { 889 if (static_cast<BMenuItem*>(fItems.ItemAtFast(i))->Submenu() 890 == submenu) { 891 return _RemoveItems(i, 1, NULL, false); 892 } 893 } 894 895 return false; 896 } 897 898 899 int32 900 BMenu::CountItems() const 901 { 902 return fItems.CountItems(); 903 } 904 905 906 BMenuItem* 907 BMenu::ItemAt(int32 index) const 908 { 909 return static_cast<BMenuItem*>(fItems.ItemAt(index)); 910 } 911 912 913 BMenu* 914 BMenu::SubmenuAt(int32 index) const 915 { 916 BMenuItem* item = static_cast<BMenuItem*>(fItems.ItemAt(index)); 917 return item != NULL ? item->Submenu() : NULL; 918 } 919 920 921 int32 922 BMenu::IndexOf(BMenuItem* item) const 923 { 924 return fItems.IndexOf(item); 925 } 926 927 928 int32 929 BMenu::IndexOf(BMenu* submenu) const 930 { 931 for (int32 i = 0; i < fItems.CountItems(); i++) { 932 if (ItemAt(i)->Submenu() == submenu) 933 return i; 934 } 935 936 return -1; 937 } 938 939 940 BMenuItem* 941 BMenu::FindItem(const char* label) const 942 { 943 BMenuItem* item = NULL; 944 945 for (int32 i = 0; i < CountItems(); i++) { 946 item = ItemAt(i); 947 948 if (item->Label() && strcmp(item->Label(), label) == 0) 949 return item; 950 951 if (item->Submenu() != NULL) { 952 item = item->Submenu()->FindItem(label); 953 if (item != NULL) 954 return item; 955 } 956 } 957 958 return NULL; 959 } 960 961 962 BMenuItem* 963 BMenu::FindItem(uint32 command) const 964 { 965 BMenuItem* item = NULL; 966 967 for (int32 i = 0; i < CountItems(); i++) { 968 item = ItemAt(i); 969 970 if (item->Command() == command) 971 return item; 972 973 if (item->Submenu() != NULL) { 974 item = item->Submenu()->FindItem(command); 975 if (item != NULL) 976 return item; 977 } 978 } 979 980 return NULL; 981 } 982 983 984 status_t 985 BMenu::SetTargetForItems(BHandler* handler) 986 { 987 status_t status = B_OK; 988 for (int32 i = 0; i < fItems.CountItems(); i++) { 989 status = ItemAt(i)->SetTarget(handler); 990 if (status < B_OK) 991 break; 992 } 993 994 return status; 995 } 996 997 998 status_t 999 BMenu::SetTargetForItems(BMessenger messenger) 1000 { 1001 status_t status = B_OK; 1002 for (int32 i = 0; i < fItems.CountItems(); i++) { 1003 status = ItemAt(i)->SetTarget(messenger); 1004 if (status < B_OK) 1005 break; 1006 } 1007 1008 return status; 1009 } 1010 1011 1012 void 1013 BMenu::SetEnabled(bool enable) 1014 { 1015 if (fEnabled == enable) 1016 return; 1017 1018 fEnabled = enable; 1019 1020 if (dynamic_cast<_BMCMenuBar_*>(Supermenu()) != NULL) 1021 Supermenu()->SetEnabled(enable); 1022 1023 if (fSuperitem) 1024 fSuperitem->SetEnabled(enable); 1025 } 1026 1027 1028 void 1029 BMenu::SetRadioMode(bool on) 1030 { 1031 fRadioMode = on; 1032 if (!on) 1033 SetLabelFromMarked(false); 1034 } 1035 1036 1037 void 1038 BMenu::SetTriggersEnabled(bool enable) 1039 { 1040 fTriggerEnabled = enable; 1041 } 1042 1043 1044 void 1045 BMenu::SetMaxContentWidth(float width) 1046 { 1047 fMaxContentWidth = width; 1048 } 1049 1050 1051 void 1052 BMenu::SetLabelFromMarked(bool on) 1053 { 1054 fDynamicName = on; 1055 if (on) 1056 SetRadioMode(true); 1057 } 1058 1059 1060 bool 1061 BMenu::IsLabelFromMarked() 1062 { 1063 return fDynamicName; 1064 } 1065 1066 1067 bool 1068 BMenu::IsEnabled() const 1069 { 1070 if (!fEnabled) 1071 return false; 1072 1073 return fSuper ? fSuper->IsEnabled() : true ; 1074 } 1075 1076 1077 bool 1078 BMenu::IsRadioMode() const 1079 { 1080 return fRadioMode; 1081 } 1082 1083 1084 bool 1085 BMenu::AreTriggersEnabled() const 1086 { 1087 return fTriggerEnabled; 1088 } 1089 1090 1091 bool 1092 BMenu::IsRedrawAfterSticky() const 1093 { 1094 return fRedrawAfterSticky; 1095 } 1096 1097 1098 float 1099 BMenu::MaxContentWidth() const 1100 { 1101 return fMaxContentWidth; 1102 } 1103 1104 1105 BMenuItem* 1106 BMenu::FindMarked() 1107 { 1108 for (int32 i = 0; i < fItems.CountItems(); i++) { 1109 BMenuItem* item = ItemAt(i); 1110 1111 if (item->IsMarked()) 1112 return item; 1113 } 1114 1115 return NULL; 1116 } 1117 1118 1119 int32 1120 BMenu::FindMarkedIndex() 1121 { 1122 for (int32 i = 0; i < fItems.CountItems(); i++) { 1123 BMenuItem* item = ItemAt(i); 1124 1125 if (item->IsMarked()) 1126 return i; 1127 } 1128 1129 return -1; 1130 } 1131 1132 1133 BMenu* 1134 BMenu::Supermenu() const 1135 { 1136 return fSuper; 1137 } 1138 1139 1140 BMenuItem* 1141 BMenu::Superitem() const 1142 { 1143 return fSuperitem; 1144 } 1145 1146 1147 BHandler* 1148 BMenu::ResolveSpecifier(BMessage* msg, int32 index, BMessage* specifier, 1149 int32 form, const char* property) 1150 { 1151 BPropertyInfo propInfo(sPropList); 1152 BHandler* target = NULL; 1153 1154 switch (propInfo.FindMatch(msg, 0, specifier, form, property)) { 1155 case B_ERROR: 1156 break; 1157 1158 case 0: 1159 case 1: 1160 case 2: 1161 case 3: 1162 case 4: 1163 case 5: 1164 case 6: 1165 case 7: 1166 target = this; 1167 break; 1168 case 8: 1169 // TODO: redirect to menu 1170 target = this; 1171 break; 1172 case 9: 1173 case 10: 1174 case 11: 1175 case 12: 1176 target = this; 1177 break; 1178 case 13: 1179 // TODO: redirect to menuitem 1180 target = this; 1181 break; 1182 } 1183 1184 if (!target) 1185 target = BView::ResolveSpecifier(msg, index, specifier, form, 1186 property); 1187 1188 return target; 1189 } 1190 1191 1192 status_t 1193 BMenu::GetSupportedSuites(BMessage* data) 1194 { 1195 if (data == NULL) 1196 return B_BAD_VALUE; 1197 1198 status_t err = data->AddString("suites", "suite/vnd.Be-menu"); 1199 1200 if (err < B_OK) 1201 return err; 1202 1203 BPropertyInfo propertyInfo(sPropList); 1204 err = data->AddFlat("messages", &propertyInfo); 1205 1206 if (err < B_OK) 1207 return err; 1208 1209 return BView::GetSupportedSuites(data); 1210 } 1211 1212 1213 status_t 1214 BMenu::Perform(perform_code code, void* _data) 1215 { 1216 switch (code) { 1217 case PERFORM_CODE_MIN_SIZE: 1218 ((perform_data_min_size*)_data)->return_value 1219 = BMenu::MinSize(); 1220 return B_OK; 1221 1222 case PERFORM_CODE_MAX_SIZE: 1223 ((perform_data_max_size*)_data)->return_value 1224 = BMenu::MaxSize(); 1225 return B_OK; 1226 1227 case PERFORM_CODE_PREFERRED_SIZE: 1228 ((perform_data_preferred_size*)_data)->return_value 1229 = BMenu::PreferredSize(); 1230 return B_OK; 1231 1232 case PERFORM_CODE_LAYOUT_ALIGNMENT: 1233 ((perform_data_layout_alignment*)_data)->return_value 1234 = BMenu::LayoutAlignment(); 1235 return B_OK; 1236 1237 case PERFORM_CODE_HAS_HEIGHT_FOR_WIDTH: 1238 ((perform_data_has_height_for_width*)_data)->return_value 1239 = BMenu::HasHeightForWidth(); 1240 return B_OK; 1241 1242 case PERFORM_CODE_GET_HEIGHT_FOR_WIDTH: 1243 { 1244 perform_data_get_height_for_width* data 1245 = (perform_data_get_height_for_width*)_data; 1246 BMenu::GetHeightForWidth(data->width, &data->min, &data->max, 1247 &data->preferred); 1248 return B_OK; 1249 } 1250 1251 case PERFORM_CODE_SET_LAYOUT: 1252 { 1253 perform_data_set_layout* data = (perform_data_set_layout*)_data; 1254 BMenu::SetLayout(data->layout); 1255 return B_OK; 1256 } 1257 1258 case PERFORM_CODE_LAYOUT_INVALIDATED: 1259 { 1260 perform_data_layout_invalidated* data 1261 = (perform_data_layout_invalidated*)_data; 1262 BMenu::LayoutInvalidated(data->descendants); 1263 return B_OK; 1264 } 1265 1266 case PERFORM_CODE_DO_LAYOUT: 1267 { 1268 BMenu::DoLayout(); 1269 return B_OK; 1270 } 1271 } 1272 1273 return BView::Perform(code, _data); 1274 } 1275 1276 1277 // #pragma mark - BMenu protected methods 1278 1279 1280 BMenu::BMenu(BRect frame, const char* name, uint32 resizingMode, uint32 flags, 1281 menu_layout layout, bool resizeToFit) 1282 : 1283 BView(frame, name, resizingMode, flags), 1284 fChosenItem(NULL), 1285 fSelected(NULL), 1286 fCachedMenuWindow(NULL), 1287 fSuper(NULL), 1288 fSuperitem(NULL), 1289 fAscent(-1.0f), 1290 fDescent(-1.0f), 1291 fFontHeight(-1.0f), 1292 fState(MENU_STATE_CLOSED), 1293 fLayout(layout), 1294 fExtraRect(NULL), 1295 fMaxContentWidth(0.0f), 1296 fInitMatrixSize(NULL), 1297 fExtraMenuData(NULL), 1298 fTrigger(0), 1299 fResizeToFit(resizeToFit), 1300 fUseCachedMenuLayout(false), 1301 fEnabled(true), 1302 fDynamicName(false), 1303 fRadioMode(false), 1304 fTrackNewBounds(false), 1305 fStickyMode(false), 1306 fIgnoreHidden(true), 1307 fTriggerEnabled(true), 1308 fRedrawAfterSticky(false), 1309 fAttachAborted(false) 1310 { 1311 _InitData(NULL); 1312 } 1313 1314 1315 void 1316 BMenu::SetItemMargins(float left, float top, float right, float bottom) 1317 { 1318 fPad.Set(left, top, right, bottom); 1319 } 1320 1321 1322 void 1323 BMenu::GetItemMargins(float* _left, float* _top, float* _right, 1324 float* _bottom) const 1325 { 1326 if (_left != NULL) 1327 *_left = fPad.left; 1328 1329 if (_top != NULL) 1330 *_top = fPad.top; 1331 1332 if (_right != NULL) 1333 *_right = fPad.right; 1334 1335 if (_bottom != NULL) 1336 *_bottom = fPad.bottom; 1337 } 1338 1339 1340 menu_layout 1341 BMenu::Layout() const 1342 { 1343 return fLayout; 1344 } 1345 1346 1347 void 1348 BMenu::Show() 1349 { 1350 Show(false); 1351 } 1352 1353 1354 void 1355 BMenu::Show(bool selectFirst) 1356 { 1357 _Install(NULL); 1358 _Show(selectFirst); 1359 } 1360 1361 1362 void 1363 BMenu::Hide() 1364 { 1365 _Hide(); 1366 _Uninstall(); 1367 } 1368 1369 1370 BMenuItem* 1371 BMenu::Track(bool sticky, BRect* clickToOpenRect) 1372 { 1373 if (sticky && LockLooper()) { 1374 //RedrawAfterSticky(Bounds()); 1375 // the call above didn't do anything, so I've removed it for now 1376 UnlockLooper(); 1377 } 1378 1379 if (clickToOpenRect != NULL && LockLooper()) { 1380 fExtraRect = clickToOpenRect; 1381 ConvertFromScreen(fExtraRect); 1382 UnlockLooper(); 1383 } 1384 1385 _SetStickyMode(sticky); 1386 1387 int action; 1388 BMenuItem* menuItem = _Track(&action); 1389 1390 fExtraRect = NULL; 1391 1392 return menuItem; 1393 } 1394 1395 1396 // #pragma mark - BMenu private methods 1397 1398 1399 bool 1400 BMenu::AddDynamicItem(add_state state) 1401 { 1402 // Implemented in subclasses 1403 return false; 1404 } 1405 1406 1407 void 1408 BMenu::DrawBackground(BRect updateRect) 1409 { 1410 if (be_control_look != NULL) { 1411 rgb_color base = ui_color(B_MENU_BACKGROUND_COLOR); 1412 uint32 flags = 0; 1413 if (!IsEnabled()) 1414 flags |= BControlLook::B_DISABLED; 1415 1416 if (IsFocus()) 1417 flags |= BControlLook::B_FOCUSED; 1418 1419 BRect rect = Bounds(); 1420 uint32 borders = BControlLook::B_LEFT_BORDER 1421 | BControlLook::B_RIGHT_BORDER; 1422 if (Window() != NULL && Parent() != NULL) { 1423 if (Parent()->Frame().top == Window()->Bounds().top) 1424 borders |= BControlLook::B_TOP_BORDER; 1425 1426 if (Parent()->Frame().bottom == Window()->Bounds().bottom) 1427 borders |= BControlLook::B_BOTTOM_BORDER; 1428 } else { 1429 borders |= BControlLook::B_TOP_BORDER 1430 | BControlLook::B_BOTTOM_BORDER; 1431 } 1432 be_control_look->DrawMenuBackground(this, rect, updateRect, base, 0, 1433 borders); 1434 1435 return; 1436 } 1437 1438 rgb_color oldColor = HighColor(); 1439 SetHighColor(ui_color(B_MENU_BACKGROUND_COLOR)); 1440 FillRect(Bounds() & updateRect, B_SOLID_HIGH); 1441 SetHighColor(oldColor); 1442 } 1443 1444 1445 void 1446 BMenu::SetTrackingHook(menu_tracking_hook func, void* state) 1447 { 1448 delete fExtraMenuData; 1449 fExtraMenuData = new (nothrow) BPrivate::ExtraMenuData(func, state); 1450 } 1451 1452 1453 void BMenu::_ReservedMenu3() {} 1454 void BMenu::_ReservedMenu4() {} 1455 void BMenu::_ReservedMenu5() {} 1456 void BMenu::_ReservedMenu6() {} 1457 1458 1459 void 1460 BMenu::_InitData(BMessage* archive) 1461 { 1462 BPrivate::kEmptyMenuLabel = B_TRANSLATE("<empty>"); 1463 1464 // TODO: Get _color, _fname, _fflt from the message, if present 1465 BFont font; 1466 font.SetFamilyAndStyle(sMenuInfo.f_family, sMenuInfo.f_style); 1467 font.SetSize(sMenuInfo.font_size); 1468 SetFont(&font, B_FONT_FAMILY_AND_STYLE | B_FONT_SIZE); 1469 1470 fLayoutData = new LayoutData; 1471 fLayoutData->lastResizingMode = ResizingMode(); 1472 1473 SetLowUIColor(B_MENU_BACKGROUND_COLOR); 1474 SetViewColor(B_TRANSPARENT_COLOR); 1475 1476 fTriggerEnabled = sMenuInfo.triggers_always_shown; 1477 1478 if (archive != NULL) { 1479 archive->FindInt32("_layout", (int32*)&fLayout); 1480 archive->FindBool("_rsize_to_fit", &fResizeToFit); 1481 bool disabled; 1482 if (archive->FindBool("_disable", &disabled) == B_OK) 1483 fEnabled = !disabled; 1484 archive->FindBool("_radio", &fRadioMode); 1485 1486 bool disableTrigger = false; 1487 archive->FindBool("_trig_disabled", &disableTrigger); 1488 fTriggerEnabled = !disableTrigger; 1489 1490 archive->FindBool("_dyn_label", &fDynamicName); 1491 archive->FindFloat("_maxwidth", &fMaxContentWidth); 1492 1493 BMessage msg; 1494 for (int32 i = 0; archive->FindMessage("_items", i, &msg) == B_OK; i++) { 1495 BArchivable* object = instantiate_object(&msg); 1496 if (BMenuItem* item = dynamic_cast<BMenuItem*>(object)) { 1497 BRect bounds; 1498 if (fLayout == B_ITEMS_IN_MATRIX 1499 && archive->FindRect("_i_frames", i, &bounds) == B_OK) 1500 AddItem(item, bounds); 1501 else 1502 AddItem(item); 1503 } 1504 } 1505 } 1506 } 1507 1508 1509 bool 1510 BMenu::_Show(bool selectFirstItem, bool keyDown) 1511 { 1512 if (Window() != NULL) 1513 return false; 1514 1515 // See if the supermenu has a cached menuwindow, 1516 // and use that one if possible. 1517 BMenuWindow* window = NULL; 1518 bool ourWindow = false; 1519 if (fSuper != NULL) { 1520 fSuperbounds = fSuper->ConvertToScreen(fSuper->Bounds()); 1521 window = fSuper->_MenuWindow(); 1522 } 1523 1524 // Otherwise, create a new one 1525 // This happens for "stand alone" BPopUpMenus 1526 // (i.e. not within a BMenuField) 1527 if (window == NULL) { 1528 // Menu windows get the BMenu's handler name 1529 window = new (nothrow) BMenuWindow(Name()); 1530 ourWindow = true; 1531 } 1532 1533 if (window == NULL) 1534 return false; 1535 1536 if (window->Lock()) { 1537 bool addAborted = false; 1538 if (keyDown) 1539 addAborted = _AddDynamicItems(keyDown); 1540 1541 if (addAborted) { 1542 if (ourWindow) 1543 window->Quit(); 1544 else 1545 window->Unlock(); 1546 return false; 1547 } 1548 fAttachAborted = false; 1549 1550 window->AttachMenu(this); 1551 1552 if (ItemAt(0) != NULL) { 1553 float width, height; 1554 ItemAt(0)->GetContentSize(&width, &height); 1555 1556 window->SetSmallStep(ceilf(height)); 1557 } 1558 1559 // Menu didn't have the time to add its items: aborting... 1560 if (fAttachAborted) { 1561 window->DetachMenu(); 1562 // TODO: Probably not needed, we can just let _hide() quit the 1563 // window. 1564 if (ourWindow) 1565 window->Quit(); 1566 else 1567 window->Unlock(); 1568 return false; 1569 } 1570 1571 _UpdateWindowViewSize(true); 1572 window->Show(); 1573 1574 if (selectFirstItem) 1575 _SelectItem(ItemAt(0), false); 1576 1577 window->Unlock(); 1578 } 1579 1580 return true; 1581 } 1582 1583 1584 void 1585 BMenu::_Hide() 1586 { 1587 BMenuWindow* window = dynamic_cast<BMenuWindow*>(Window()); 1588 if (window == NULL || !window->Lock()) 1589 return; 1590 1591 if (fSelected != NULL) 1592 _SelectItem(NULL); 1593 1594 window->Hide(); 1595 window->DetachMenu(); 1596 // we don't want to be deleted when the window is removed 1597 1598 #if USE_CACHED_MENUWINDOW 1599 if (fSuper != NULL) 1600 window->Unlock(); 1601 else 1602 #endif 1603 window->Quit(); 1604 // it's our window, quit it 1605 1606 _DeleteMenuWindow(); 1607 // Delete the menu window used by our submenus 1608 } 1609 1610 1611 // #pragma mark - mouse tracking 1612 1613 1614 const static bigtime_t kOpenSubmenuDelay = 225000; 1615 const static bigtime_t kNavigationAreaTimeout = 1000000; 1616 1617 1618 BMenuItem* 1619 BMenu::_Track(int* action, long start) 1620 { 1621 // TODO: cleanup 1622 BMenuItem* item = NULL; 1623 BRect navAreaRectAbove; 1624 BRect navAreaRectBelow; 1625 bigtime_t selectedTime = system_time(); 1626 bigtime_t navigationAreaTime = 0; 1627 1628 fState = MENU_STATE_TRACKING; 1629 fChosenItem = NULL; 1630 // we will use this for keyboard selection 1631 1632 BPoint location; 1633 uint32 buttons = 0; 1634 if (LockLooper()) { 1635 GetMouse(&location, &buttons); 1636 UnlockLooper(); 1637 } 1638 1639 bool releasedOnce = buttons == 0; 1640 while (fState != MENU_STATE_CLOSED) { 1641 if (_CustomTrackingWantsToQuit()) 1642 break; 1643 1644 if (!LockLooper()) 1645 break; 1646 1647 BMenuWindow* window = static_cast<BMenuWindow*>(Window()); 1648 BPoint screenLocation = ConvertToScreen(location); 1649 if (window->CheckForScrolling(screenLocation)) { 1650 UnlockLooper(); 1651 continue; 1652 } 1653 1654 // The order of the checks is important 1655 // to be able to handle overlapping menus: 1656 // first we check if mouse is inside a submenu, 1657 // then if the mouse is inside this menu, 1658 // then if it's over a super menu. 1659 if (_OverSubmenu(fSelected, screenLocation) 1660 || fState == MENU_STATE_KEY_TO_SUBMENU) { 1661 if (fState == MENU_STATE_TRACKING) { 1662 // not if from R.Arrow 1663 fState = MENU_STATE_TRACKING_SUBMENU; 1664 } 1665 navAreaRectAbove = BRect(); 1666 navAreaRectBelow = BRect(); 1667 1668 // Since the submenu has its own looper, 1669 // we can unlock ours. Doing so also make sure 1670 // that our window gets any update message to 1671 // redraw itself 1672 UnlockLooper(); 1673 int submenuAction = MENU_STATE_TRACKING; 1674 BMenu* submenu = fSelected->Submenu(); 1675 submenu->_SetStickyMode(_IsStickyMode()); 1676 1677 // The following call blocks until the submenu 1678 // gives control back to us, either because the mouse 1679 // pointer goes out of the submenu's bounds, or because 1680 // the user closes the menu 1681 BMenuItem* submenuItem = submenu->_Track(&submenuAction); 1682 if (submenuAction == MENU_STATE_CLOSED) { 1683 item = submenuItem; 1684 fState = MENU_STATE_CLOSED; 1685 } else if (submenuAction == MENU_STATE_KEY_LEAVE_SUBMENU) { 1686 if (LockLooper()) { 1687 BMenuItem* temp = fSelected; 1688 // close the submenu: 1689 _SelectItem(NULL); 1690 // but reselect the item itself for user: 1691 _SelectItem(temp, false); 1692 UnlockLooper(); 1693 } 1694 // cancel key-nav state 1695 fState = MENU_STATE_TRACKING; 1696 } else 1697 fState = MENU_STATE_TRACKING; 1698 if (!LockLooper()) 1699 break; 1700 } else if ((item = _HitTestItems(location, B_ORIGIN)) != NULL) { 1701 _UpdateStateOpenSelect(item, location, navAreaRectAbove, 1702 navAreaRectBelow, selectedTime, navigationAreaTime); 1703 releasedOnce = true; 1704 } else if (_OverSuper(screenLocation) 1705 && fSuper->fState != MENU_STATE_KEY_TO_SUBMENU) { 1706 fState = MENU_STATE_TRACKING; 1707 UnlockLooper(); 1708 break; 1709 } else if (fState == MENU_STATE_KEY_LEAVE_SUBMENU) { 1710 UnlockLooper(); 1711 break; 1712 } else if (fSuper == NULL 1713 || fSuper->fState != MENU_STATE_KEY_TO_SUBMENU) { 1714 // Mouse pointer outside menu: 1715 // If there's no other submenu opened, 1716 // deselect the current selected item 1717 if (fSelected != NULL 1718 && (fSelected->Submenu() == NULL 1719 || fSelected->Submenu()->Window() == NULL)) { 1720 _SelectItem(NULL); 1721 fState = MENU_STATE_TRACKING; 1722 } 1723 1724 if (fSuper != NULL) { 1725 // Give supermenu the chance to continue tracking 1726 *action = fState; 1727 UnlockLooper(); 1728 return NULL; 1729 } 1730 } 1731 1732 UnlockLooper(); 1733 1734 if (releasedOnce) 1735 _UpdateStateClose(item, location, buttons); 1736 1737 if (fState != MENU_STATE_CLOSED) { 1738 bigtime_t snoozeAmount = 50000; 1739 1740 BPoint newLocation = location; 1741 uint32 newButtons = buttons; 1742 1743 // If user doesn't move the mouse, loop here, 1744 // so we don't interfere with keyboard menu navigation 1745 do { 1746 snooze(snoozeAmount); 1747 if (!LockLooper()) 1748 break; 1749 GetMouse(&newLocation, &newButtons, true); 1750 UnlockLooper(); 1751 } while (newLocation == location && newButtons == buttons 1752 && !(item != NULL && item->Submenu() != NULL 1753 && item->Submenu()->Window() == NULL) 1754 && fState == MENU_STATE_TRACKING); 1755 1756 if (newLocation != location || newButtons != buttons) { 1757 if (!releasedOnce && newButtons == 0 && buttons != 0) 1758 releasedOnce = true; 1759 location = newLocation; 1760 buttons = newButtons; 1761 } 1762 1763 if (releasedOnce) 1764 _UpdateStateClose(item, location, buttons); 1765 } 1766 } 1767 1768 if (action != NULL) 1769 *action = fState; 1770 1771 // keyboard Enter will set this 1772 if (fChosenItem != NULL) 1773 item = fChosenItem; 1774 else if (fSelected == NULL) { 1775 // needed to cover (rare) mouse/ESC combination 1776 item = NULL; 1777 } 1778 1779 if (fSelected != NULL && LockLooper()) { 1780 _SelectItem(NULL); 1781 UnlockLooper(); 1782 } 1783 1784 // delete the menu window recycled for all the child menus 1785 _DeleteMenuWindow(); 1786 1787 return item; 1788 } 1789 1790 1791 void 1792 BMenu::_UpdateNavigationArea(BPoint position, BRect& navAreaRectAbove, 1793 BRect& navAreaRectBelow) 1794 { 1795 #define NAV_AREA_THRESHOLD 8 1796 1797 // The navigation area is a region in which mouse-overs won't select 1798 // the item under the cursor. This makes it easier to navigate to 1799 // submenus, as the cursor can be moved to submenu items directly instead 1800 // of having to move it horizontally into the submenu first. The concept 1801 // is illustrated below: 1802 // 1803 // +-------+----+---------+ 1804 // | | /| | 1805 // | | /*| | 1806 // |[2]--> | /**| | 1807 // | |/[4]| | 1808 // |------------| | 1809 // | [1] | [6] | 1810 // |------------| | 1811 // | |\[5]| | 1812 // |[3]--> | \**| | 1813 // | | \*| | 1814 // | | \| | 1815 // | +----|---------+ 1816 // | | 1817 // +------------+ 1818 // 1819 // [1] Selected item, cursor position ('position') 1820 // [2] Upper navigation area rectangle ('navAreaRectAbove') 1821 // [3] Lower navigation area rectangle ('navAreaRectBelow') 1822 // [4] Upper navigation area 1823 // [5] Lower navigation area 1824 // [6] Submenu 1825 // 1826 // The rectangles are used to calculate if the cursor is in the actual 1827 // navigation area (see _UpdateStateOpenSelect()). 1828 1829 if (fSelected == NULL) 1830 return; 1831 1832 BMenu* submenu = fSelected->Submenu(); 1833 1834 if (submenu != NULL) { 1835 BRect menuBounds = ConvertToScreen(Bounds()); 1836 1837 BRect submenuBounds; 1838 if (fSelected->Submenu()->LockLooper()) { 1839 submenuBounds = fSelected->Submenu()->ConvertToScreen( 1840 fSelected->Submenu()->Bounds()); 1841 fSelected->Submenu()->UnlockLooper(); 1842 } 1843 1844 if (menuBounds.left < submenuBounds.left) { 1845 navAreaRectAbove.Set(position.x + NAV_AREA_THRESHOLD, 1846 submenuBounds.top, menuBounds.right, 1847 position.y); 1848 navAreaRectBelow.Set(position.x + NAV_AREA_THRESHOLD, 1849 position.y, menuBounds.right, 1850 submenuBounds.bottom); 1851 } else { 1852 navAreaRectAbove.Set(menuBounds.left, 1853 submenuBounds.top, position.x - NAV_AREA_THRESHOLD, 1854 position.y); 1855 navAreaRectBelow.Set(menuBounds.left, 1856 position.y, position.x - NAV_AREA_THRESHOLD, 1857 submenuBounds.bottom); 1858 } 1859 } else { 1860 navAreaRectAbove = BRect(); 1861 navAreaRectBelow = BRect(); 1862 } 1863 } 1864 1865 1866 void 1867 BMenu::_UpdateStateOpenSelect(BMenuItem* item, BPoint position, 1868 BRect& navAreaRectAbove, BRect& navAreaRectBelow, bigtime_t& selectedTime, 1869 bigtime_t& navigationAreaTime) 1870 { 1871 if (fState == MENU_STATE_CLOSED) 1872 return; 1873 1874 if (item != fSelected) { 1875 if (navigationAreaTime == 0) 1876 navigationAreaTime = system_time(); 1877 1878 position = ConvertToScreen(position); 1879 1880 bool inNavAreaRectAbove = navAreaRectAbove.Contains(position); 1881 bool inNavAreaRectBelow = navAreaRectBelow.Contains(position); 1882 1883 if (fSelected == NULL 1884 || (!inNavAreaRectAbove && !inNavAreaRectBelow)) { 1885 _SelectItem(item, false); 1886 navAreaRectAbove = BRect(); 1887 navAreaRectBelow = BRect(); 1888 selectedTime = system_time(); 1889 navigationAreaTime = 0; 1890 return; 1891 } 1892 1893 BRect menuBounds = ConvertToScreen(Bounds()); 1894 1895 BRect submenuBounds; 1896 if (fSelected->Submenu()->LockLooper()) { 1897 fSelected->Submenu()->ConvertToScreen( 1898 fSelected->Submenu()->Bounds()); 1899 fSelected->Submenu()->UnlockLooper(); 1900 } 1901 1902 float xOffset; 1903 1904 // navAreaRectAbove and navAreaRectBelow have the same X 1905 // position and width, so it doesn't matter which one we use to 1906 // calculate the X offset 1907 if (menuBounds.left < submenuBounds.left) 1908 xOffset = position.x - navAreaRectAbove.left; 1909 else 1910 xOffset = navAreaRectAbove.right - position.x; 1911 1912 bool inNavArea; 1913 1914 if (inNavAreaRectAbove) { 1915 float yOffset = navAreaRectAbove.bottom - position.y; 1916 float ratio = navAreaRectAbove.Width() / navAreaRectAbove.Height(); 1917 1918 inNavArea = yOffset <= xOffset / ratio; 1919 } else { 1920 float yOffset = navAreaRectBelow.bottom - position.y; 1921 float ratio = navAreaRectBelow.Width() / navAreaRectBelow.Height(); 1922 1923 inNavArea = yOffset >= (navAreaRectBelow.Height() - xOffset 1924 / ratio); 1925 } 1926 1927 bigtime_t systime = system_time(); 1928 1929 if (!inNavArea || (navigationAreaTime > 0 && systime - 1930 navigationAreaTime > kNavigationAreaTimeout)) { 1931 // Don't delay opening of submenu if the user had 1932 // to wait for the navigation area timeout anyway 1933 _SelectItem(item, inNavArea); 1934 1935 if (inNavArea) { 1936 _UpdateNavigationArea(position, navAreaRectAbove, 1937 navAreaRectBelow); 1938 } else { 1939 navAreaRectAbove = BRect(); 1940 navAreaRectBelow = BRect(); 1941 } 1942 1943 selectedTime = system_time(); 1944 navigationAreaTime = 0; 1945 } 1946 } else if (fSelected->Submenu() != NULL && 1947 system_time() - selectedTime > kOpenSubmenuDelay) { 1948 _SelectItem(fSelected, true); 1949 1950 if (!navAreaRectAbove.IsValid() && !navAreaRectBelow.IsValid()) { 1951 position = ConvertToScreen(position); 1952 _UpdateNavigationArea(position, navAreaRectAbove, 1953 navAreaRectBelow); 1954 } 1955 } 1956 1957 if (fState != MENU_STATE_TRACKING) 1958 fState = MENU_STATE_TRACKING; 1959 } 1960 1961 1962 void 1963 BMenu::_UpdateStateClose(BMenuItem* item, const BPoint& where, 1964 const uint32& buttons) 1965 { 1966 if (fState == MENU_STATE_CLOSED) 1967 return; 1968 1969 if (buttons != 0 && _IsStickyMode()) { 1970 if (item == NULL) { 1971 if (item != fSelected && LockLooper()) { 1972 _SelectItem(item, false); 1973 UnlockLooper(); 1974 } 1975 fState = MENU_STATE_CLOSED; 1976 } else 1977 _SetStickyMode(false); 1978 } else if (buttons == 0 && !_IsStickyMode()) { 1979 if (fExtraRect != NULL && fExtraRect->Contains(where)) { 1980 _SetStickyMode(true); 1981 fExtraRect = NULL; 1982 // Setting this to NULL will prevent this code 1983 // to be executed next time 1984 } else { 1985 if (item != fSelected && LockLooper()) { 1986 _SelectItem(item, false); 1987 UnlockLooper(); 1988 } 1989 fState = MENU_STATE_CLOSED; 1990 } 1991 } 1992 } 1993 1994 1995 bool 1996 BMenu::_AddItem(BMenuItem* item, int32 index) 1997 { 1998 ASSERT(item != NULL); 1999 if (index < 0 || index > fItems.CountItems()) 2000 return false; 2001 2002 if (item->IsMarked()) 2003 _ItemMarked(item); 2004 2005 if (!fItems.AddItem(item, index)) 2006 return false; 2007 2008 // install the item on the supermenu's window 2009 // or onto our window, if we are a root menu 2010 BWindow* window = NULL; 2011 if (Superitem() != NULL) 2012 window = Superitem()->fWindow; 2013 else 2014 window = Window(); 2015 if (window != NULL) 2016 item->Install(window); 2017 2018 item->SetSuper(this); 2019 return true; 2020 } 2021 2022 2023 bool 2024 BMenu::_RemoveItems(int32 index, int32 count, BMenuItem* item, 2025 bool deleteItems) 2026 { 2027 bool success = false; 2028 bool invalidateLayout = false; 2029 2030 bool locked = LockLooper(); 2031 BWindow* window = Window(); 2032 2033 // The plan is simple: If we're given a BMenuItem directly, we use it 2034 // and ignore index and count. Otherwise, we use them instead. 2035 if (item != NULL) { 2036 if (fItems.RemoveItem(item)) { 2037 if (item == fSelected && window != NULL) 2038 _SelectItem(NULL); 2039 item->Uninstall(); 2040 item->SetSuper(NULL); 2041 if (deleteItems) 2042 delete item; 2043 success = invalidateLayout = true; 2044 } 2045 } else { 2046 // We iterate backwards because it's simpler 2047 int32 i = std::min(index + count - 1, fItems.CountItems() - 1); 2048 // NOTE: the range check for "index" is done after 2049 // calculating the last index to be removed, so 2050 // that the range is not "shifted" unintentionally 2051 index = std::max((int32)0, index); 2052 for (; i >= index; i--) { 2053 item = static_cast<BMenuItem*>(fItems.ItemAt(i)); 2054 if (item != NULL) { 2055 if (fItems.RemoveItem(item)) { 2056 if (item == fSelected && window != NULL) 2057 _SelectItem(NULL); 2058 item->Uninstall(); 2059 item->SetSuper(NULL); 2060 if (deleteItems) 2061 delete item; 2062 success = true; 2063 invalidateLayout = true; 2064 } else { 2065 // operation not entirely successful 2066 success = false; 2067 break; 2068 } 2069 } 2070 } 2071 } 2072 2073 if (invalidateLayout) { 2074 InvalidateLayout(); 2075 if (locked && window != NULL) { 2076 _LayoutItems(0); 2077 _UpdateWindowViewSize(false); 2078 Invalidate(); 2079 } 2080 } 2081 2082 if (locked) 2083 UnlockLooper(); 2084 2085 return success; 2086 } 2087 2088 2089 bool 2090 BMenu::_RelayoutIfNeeded() 2091 { 2092 if (!fUseCachedMenuLayout) { 2093 fUseCachedMenuLayout = true; 2094 _CacheFontInfo(); 2095 _LayoutItems(0); 2096 return true; 2097 } 2098 return false; 2099 } 2100 2101 2102 void 2103 BMenu::_LayoutItems(int32 index) 2104 { 2105 _CalcTriggers(); 2106 2107 float width; 2108 float height; 2109 _ComputeLayout(index, fResizeToFit, true, &width, &height); 2110 2111 if (fResizeToFit) 2112 ResizeTo(width, height); 2113 } 2114 2115 2116 BSize 2117 BMenu::_ValidatePreferredSize() 2118 { 2119 if (!fLayoutData->preferred.IsWidthSet() || ResizingMode() 2120 != fLayoutData->lastResizingMode) { 2121 _ComputeLayout(0, true, false, NULL, NULL); 2122 ResetLayoutInvalidation(); 2123 } 2124 2125 return fLayoutData->preferred; 2126 } 2127 2128 2129 void 2130 BMenu::_ComputeLayout(int32 index, bool bestFit, bool moveItems, 2131 float* _width, float* _height) 2132 { 2133 // TODO: Take "bestFit", "moveItems", "index" into account, 2134 // Recalculate only the needed items, 2135 // not the whole layout every time 2136 2137 fLayoutData->lastResizingMode = ResizingMode(); 2138 2139 BRect frame; 2140 switch (fLayout) { 2141 case B_ITEMS_IN_COLUMN: 2142 { 2143 BRect parentFrame; 2144 BRect* overrideFrame = NULL; 2145 if (dynamic_cast<_BMCMenuBar_*>(Supermenu()) != NULL) { 2146 // When the menu is modified while it's open, we get here in a 2147 // situation where trying to lock the looper would deadlock 2148 // (the window is locked waiting for the menu to terminate). 2149 // In that case, just give up on getting the supermenu bounds 2150 // and keep the menu at the current width and position. 2151 if (Supermenu()->LockLooperWithTimeout(0) == B_OK) { 2152 parentFrame = Supermenu()->Bounds(); 2153 Supermenu()->UnlockLooper(); 2154 overrideFrame = &parentFrame; 2155 } 2156 } 2157 2158 _ComputeColumnLayout(index, bestFit, moveItems, overrideFrame, 2159 frame); 2160 break; 2161 } 2162 2163 case B_ITEMS_IN_ROW: 2164 _ComputeRowLayout(index, bestFit, moveItems, frame); 2165 break; 2166 2167 case B_ITEMS_IN_MATRIX: 2168 _ComputeMatrixLayout(frame); 2169 break; 2170 } 2171 2172 // change width depending on resize mode 2173 BSize size; 2174 if ((ResizingMode() & B_FOLLOW_LEFT_RIGHT) == B_FOLLOW_LEFT_RIGHT) { 2175 if (dynamic_cast<_BMCMenuBar_*>(this) != NULL) 2176 size.width = Bounds().Width() - fPad.right; 2177 else if (Parent() != NULL) 2178 size.width = Parent()->Frame().Width() + 1; 2179 else if (Window() != NULL) 2180 size.width = Window()->Frame().Width() + 1; 2181 else 2182 size.width = Bounds().Width(); 2183 } else 2184 size.width = frame.Width(); 2185 2186 size.height = frame.Height(); 2187 2188 if (_width) 2189 *_width = size.width; 2190 2191 if (_height) 2192 *_height = size.height; 2193 2194 if (bestFit) 2195 fLayoutData->preferred = size; 2196 2197 if (moveItems) 2198 fUseCachedMenuLayout = true; 2199 } 2200 2201 2202 void 2203 BMenu::_ComputeColumnLayout(int32 index, bool bestFit, bool moveItems, 2204 BRect* overrideFrame, BRect& frame) 2205 { 2206 bool command = false; 2207 bool control = false; 2208 bool shift = false; 2209 bool option = false; 2210 2211 if (index > 0) 2212 frame = ItemAt(index - 1)->Frame(); 2213 else if (overrideFrame != NULL) 2214 frame.Set(0, 0, overrideFrame->right, -1); 2215 else 2216 frame.Set(0, 0, 0, -1); 2217 2218 BFont font; 2219 GetFont(&font); 2220 2221 for (; index < fItems.CountItems(); index++) { 2222 BMenuItem* item = ItemAt(index); 2223 2224 float width; 2225 float height; 2226 item->GetContentSize(&width, &height); 2227 2228 if (item->fModifiers && item->fShortcutChar) { 2229 width += font.Size(); 2230 if ((item->fModifiers & B_COMMAND_KEY) != 0) 2231 command = true; 2232 2233 if ((item->fModifiers & B_CONTROL_KEY) != 0) 2234 control = true; 2235 2236 if ((item->fModifiers & B_SHIFT_KEY) != 0) 2237 shift = true; 2238 2239 if ((item->fModifiers & B_OPTION_KEY) != 0) 2240 option = true; 2241 } 2242 2243 item->fBounds.left = 0.0f; 2244 item->fBounds.top = frame.bottom + 1.0f; 2245 item->fBounds.bottom = item->fBounds.top + height + fPad.top 2246 + fPad.bottom; 2247 2248 if (item->fSubmenu != NULL) 2249 width += item->Frame().Height(); 2250 2251 frame.right = std::max(frame.right, width + fPad.left + fPad.right); 2252 frame.bottom = item->fBounds.bottom; 2253 } 2254 2255 if (command) { 2256 frame.right 2257 += BPrivate::MenuPrivate::MenuItemCommand()->Bounds().Width() + 1; 2258 } 2259 if (control) { 2260 frame.right 2261 += BPrivate::MenuPrivate::MenuItemControl()->Bounds().Width() + 1; 2262 } 2263 if (option) { 2264 frame.right 2265 += BPrivate::MenuPrivate::MenuItemOption()->Bounds().Width() + 1; 2266 } 2267 if (shift) { 2268 frame.right 2269 += BPrivate::MenuPrivate::MenuItemShift()->Bounds().Width() + 1; 2270 } 2271 2272 if (fMaxContentWidth > 0) 2273 frame.right = std::min(frame.right, fMaxContentWidth); 2274 2275 if (moveItems) { 2276 for (int32 i = 0; i < fItems.CountItems(); i++) 2277 ItemAt(i)->fBounds.right = frame.right; 2278 } 2279 2280 frame.top = 0; 2281 frame.right = ceilf(frame.right); 2282 } 2283 2284 2285 void 2286 BMenu::_ComputeRowLayout(int32 index, bool bestFit, bool moveItems, 2287 BRect& frame) 2288 { 2289 font_height fh; 2290 GetFontHeight(&fh); 2291 frame.Set(0.0f, 0.0f, 0.0f, ceilf(fh.ascent + fh.descent + fPad.top 2292 + fPad.bottom)); 2293 2294 for (int32 i = 0; i < fItems.CountItems(); i++) { 2295 BMenuItem* item = ItemAt(i); 2296 2297 float width, height; 2298 item->GetContentSize(&width, &height); 2299 2300 item->fBounds.left = frame.right; 2301 item->fBounds.top = 0.0f; 2302 item->fBounds.right = item->fBounds.left + width + fPad.left 2303 + fPad.right; 2304 2305 frame.right = item->Frame().right + 1.0f; 2306 frame.bottom = std::max(frame.bottom, height + fPad.top + fPad.bottom); 2307 } 2308 2309 if (moveItems) { 2310 for (int32 i = 0; i < fItems.CountItems(); i++) 2311 ItemAt(i)->fBounds.bottom = frame.bottom; 2312 } 2313 2314 if (bestFit) 2315 frame.right = ceilf(frame.right); 2316 else 2317 frame.right = Bounds().right; 2318 } 2319 2320 2321 void 2322 BMenu::_ComputeMatrixLayout(BRect &frame) 2323 { 2324 frame.Set(0, 0, 0, 0); 2325 for (int32 i = 0; i < CountItems(); i++) { 2326 BMenuItem* item = ItemAt(i); 2327 if (item != NULL) { 2328 frame.left = std::min(frame.left, item->Frame().left); 2329 frame.right = std::max(frame.right, item->Frame().right); 2330 frame.top = std::min(frame.top, item->Frame().top); 2331 frame.bottom = std::max(frame.bottom, item->Frame().bottom); 2332 } 2333 } 2334 } 2335 2336 2337 void 2338 BMenu::LayoutInvalidated(bool descendants) 2339 { 2340 fUseCachedMenuLayout = false; 2341 fLayoutData->preferred.Set(B_SIZE_UNSET, B_SIZE_UNSET); 2342 } 2343 2344 2345 // Assumes the SuperMenu to be locked (due to calling ConvertToScreen()) 2346 BPoint 2347 BMenu::ScreenLocation() 2348 { 2349 BMenu* superMenu = Supermenu(); 2350 BMenuItem* superItem = Superitem(); 2351 2352 if (superMenu == NULL || superItem == NULL) { 2353 debugger("BMenu can't determine where to draw." 2354 "Override BMenu::ScreenLocation() to determine location."); 2355 } 2356 2357 BPoint point; 2358 if (superMenu->Layout() == B_ITEMS_IN_COLUMN) 2359 point = superItem->Frame().RightTop() + BPoint(1.0f, 1.0f); 2360 else 2361 point = superItem->Frame().LeftBottom() + BPoint(1.0f, 1.0f); 2362 2363 superMenu->ConvertToScreen(&point); 2364 2365 return point; 2366 } 2367 2368 2369 BRect 2370 BMenu::_CalcFrame(BPoint where, bool* scrollOn) 2371 { 2372 // TODO: Improve me 2373 BRect bounds = Bounds(); 2374 BRect frame = bounds.OffsetToCopy(where); 2375 2376 BScreen screen(Window()); 2377 BRect screenFrame = screen.Frame(); 2378 2379 BMenu* superMenu = Supermenu(); 2380 BMenuItem* superItem = Superitem(); 2381 2382 // TODO: Horrible hack: 2383 // When added to a BMenuField, a BPopUpMenu is the child of 2384 // a _BMCMenuBar_ to "fake" the menu hierarchy 2385 bool inMenuField = dynamic_cast<_BMCMenuBar_*>(superMenu) != NULL; 2386 2387 // Offset the menu field menu window left by the width of the checkmark 2388 // so that the text when the menu is closed lines up with the text when 2389 // the menu is open. 2390 if (inMenuField) 2391 frame.OffsetBy(-8.0f, 0.0f); 2392 2393 bool scroll = false; 2394 if (superMenu == NULL || superItem == NULL || inMenuField) { 2395 // just move the window on screen 2396 2397 if (frame.bottom > screenFrame.bottom) 2398 frame.OffsetBy(0, screenFrame.bottom - frame.bottom); 2399 else if (frame.top < screenFrame.top) 2400 frame.OffsetBy(0, -frame.top); 2401 2402 if (frame.right > screenFrame.right) 2403 frame.OffsetBy(screenFrame.right - frame.right, 0); 2404 else if (frame.left < screenFrame.left) 2405 frame.OffsetBy(-frame.left, 0); 2406 } else if (superMenu->Layout() == B_ITEMS_IN_COLUMN) { 2407 if (frame.right > screenFrame.right) 2408 frame.OffsetBy(-superItem->Frame().Width() - frame.Width() - 2, 0); 2409 2410 if (frame.left < 0) 2411 frame.OffsetBy(-frame.left + 6, 0); 2412 2413 if (frame.bottom > screenFrame.bottom) 2414 frame.OffsetBy(0, screenFrame.bottom - frame.bottom); 2415 } else { 2416 if (frame.bottom > screenFrame.bottom) { 2417 if (scrollOn != NULL && superMenu != NULL 2418 && dynamic_cast<BMenuBar*>(superMenu) != NULL 2419 && frame.top < (screenFrame.bottom - 80)) { 2420 scroll = true; 2421 } else { 2422 frame.OffsetBy(0, -superItem->Frame().Height() 2423 - frame.Height() - 3); 2424 } 2425 } 2426 2427 if (frame.right > screenFrame.right) 2428 frame.OffsetBy(screenFrame.right - frame.right, 0); 2429 } 2430 2431 if (!scroll) { 2432 // basically, if this returns false, it means 2433 // that the menu frame won't fit completely inside the screen 2434 // TODO: Scrolling will currently only work up/down, 2435 // not left/right 2436 scroll = screenFrame.Height() < frame.Height(); 2437 } 2438 2439 if (scrollOn != NULL) 2440 *scrollOn = scroll; 2441 2442 return frame; 2443 } 2444 2445 2446 void 2447 BMenu::DrawItems(BRect updateRect) 2448 { 2449 int32 itemCount = fItems.CountItems(); 2450 for (int32 i = 0; i < itemCount; i++) { 2451 BMenuItem* item = ItemAt(i); 2452 if (item->Frame().Intersects(updateRect)) 2453 item->Draw(); 2454 } 2455 } 2456 2457 2458 int 2459 BMenu::_State(BMenuItem** item) const 2460 { 2461 if (fState == MENU_STATE_TRACKING || fState == MENU_STATE_CLOSED) 2462 return fState; 2463 2464 if (fSelected != NULL && fSelected->Submenu() != NULL) 2465 return fSelected->Submenu()->_State(item); 2466 2467 return fState; 2468 } 2469 2470 2471 void 2472 BMenu::_InvokeItem(BMenuItem* item, bool now) 2473 { 2474 if (!item->IsEnabled()) 2475 return; 2476 2477 // Do the "selected" animation 2478 // TODO: Doesn't work. This is supposed to highlight 2479 // and dehighlight the item, works on beos but not on haiku. 2480 if (!item->Submenu() && LockLooper()) { 2481 snooze(50000); 2482 item->Select(true); 2483 Window()->UpdateIfNeeded(); 2484 snooze(50000); 2485 item->Select(false); 2486 Window()->UpdateIfNeeded(); 2487 snooze(50000); 2488 item->Select(true); 2489 Window()->UpdateIfNeeded(); 2490 snooze(50000); 2491 item->Select(false); 2492 Window()->UpdateIfNeeded(); 2493 UnlockLooper(); 2494 } 2495 2496 // Lock the root menu window before calling BMenuItem::Invoke() 2497 BMenu* parent = this; 2498 BMenu* rootMenu = NULL; 2499 do { 2500 rootMenu = parent; 2501 parent = rootMenu->Supermenu(); 2502 } while (parent != NULL); 2503 2504 if (rootMenu->LockLooper()) { 2505 item->Invoke(); 2506 rootMenu->UnlockLooper(); 2507 } 2508 } 2509 2510 2511 bool 2512 BMenu::_OverSuper(BPoint location) 2513 { 2514 if (!Supermenu()) 2515 return false; 2516 2517 return fSuperbounds.Contains(location); 2518 } 2519 2520 2521 bool 2522 BMenu::_OverSubmenu(BMenuItem* item, BPoint loc) 2523 { 2524 if (item == NULL) 2525 return false; 2526 2527 BMenu* subMenu = item->Submenu(); 2528 if (subMenu == NULL || subMenu->Window() == NULL) 2529 return false; 2530 2531 // assume that loc is in screen coordinates 2532 if (subMenu->Window()->Frame().Contains(loc)) 2533 return true; 2534 2535 return subMenu->_OverSubmenu(subMenu->fSelected, loc); 2536 } 2537 2538 2539 BMenuWindow* 2540 BMenu::_MenuWindow() 2541 { 2542 #if USE_CACHED_MENUWINDOW 2543 if (fCachedMenuWindow == NULL) { 2544 char windowName[64]; 2545 snprintf(windowName, 64, "%s cached menu", Name()); 2546 fCachedMenuWindow = new (nothrow) BMenuWindow(windowName); 2547 } 2548 #endif 2549 return fCachedMenuWindow; 2550 } 2551 2552 2553 void 2554 BMenu::_DeleteMenuWindow() 2555 { 2556 if (fCachedMenuWindow != NULL) { 2557 fCachedMenuWindow->Lock(); 2558 fCachedMenuWindow->Quit(); 2559 fCachedMenuWindow = NULL; 2560 } 2561 } 2562 2563 2564 BMenuItem* 2565 BMenu::_HitTestItems(BPoint where, BPoint slop) const 2566 { 2567 // TODO: Take "slop" into account ? 2568 2569 // if the point doesn't lie within the menu's 2570 // bounds, bail out immediately 2571 if (!Bounds().Contains(where)) 2572 return NULL; 2573 2574 int32 itemCount = CountItems(); 2575 for (int32 i = 0; i < itemCount; i++) { 2576 BMenuItem* item = ItemAt(i); 2577 if (item->Frame().Contains(where) 2578 && dynamic_cast<BSeparatorItem*>(item) == NULL) { 2579 return item; 2580 } 2581 } 2582 2583 return NULL; 2584 } 2585 2586 2587 BRect 2588 BMenu::_Superbounds() const 2589 { 2590 return fSuperbounds; 2591 } 2592 2593 2594 void 2595 BMenu::_CacheFontInfo() 2596 { 2597 font_height fh; 2598 GetFontHeight(&fh); 2599 fAscent = fh.ascent; 2600 fDescent = fh.descent; 2601 fFontHeight = ceilf(fh.ascent + fh.descent + fh.leading); 2602 } 2603 2604 2605 void 2606 BMenu::_ItemMarked(BMenuItem* item) 2607 { 2608 if (IsRadioMode()) { 2609 for (int32 i = 0; i < CountItems(); i++) { 2610 if (ItemAt(i) != item) 2611 ItemAt(i)->SetMarked(false); 2612 } 2613 } 2614 2615 if (IsLabelFromMarked() && Superitem() != NULL) 2616 Superitem()->SetLabel(item->Label()); 2617 } 2618 2619 2620 void 2621 BMenu::_Install(BWindow* target) 2622 { 2623 for (int32 i = 0; i < CountItems(); i++) 2624 ItemAt(i)->Install(target); 2625 } 2626 2627 2628 void 2629 BMenu::_Uninstall() 2630 { 2631 for (int32 i = 0; i < CountItems(); i++) 2632 ItemAt(i)->Uninstall(); 2633 } 2634 2635 2636 void 2637 BMenu::_SelectItem(BMenuItem* item, bool showSubmenu, bool selectFirstItem, 2638 bool keyDown) 2639 { 2640 // Avoid deselecting and then reselecting the same item 2641 // which would cause flickering 2642 if (item != fSelected) { 2643 if (fSelected != NULL) { 2644 fSelected->Select(false); 2645 BMenu* subMenu = fSelected->Submenu(); 2646 if (subMenu != NULL && subMenu->Window() != NULL) 2647 subMenu->_Hide(); 2648 } 2649 2650 fSelected = item; 2651 if (fSelected != NULL) 2652 fSelected->Select(true); 2653 } 2654 2655 if (fSelected != NULL && showSubmenu) { 2656 BMenu* subMenu = fSelected->Submenu(); 2657 if (subMenu != NULL && subMenu->Window() == NULL) { 2658 if (!subMenu->_Show(selectFirstItem, keyDown)) { 2659 // something went wrong, deselect the item 2660 fSelected->Select(false); 2661 fSelected = NULL; 2662 } 2663 } 2664 } 2665 } 2666 2667 2668 bool 2669 BMenu::_SelectNextItem(BMenuItem* item, bool forward) 2670 { 2671 if (CountItems() == 0) // cannot select next item in an empty menu 2672 return false; 2673 2674 BMenuItem* nextItem = _NextItem(item, forward); 2675 if (nextItem == NULL) 2676 return false; 2677 2678 _SelectItem(nextItem, dynamic_cast<BMenuBar*>(this) != NULL); 2679 2680 if (LockLooper()) { 2681 be_app->ObscureCursor(); 2682 UnlockLooper(); 2683 } 2684 2685 return true; 2686 } 2687 2688 2689 BMenuItem* 2690 BMenu::_NextItem(BMenuItem* item, bool forward) const 2691 { 2692 const int32 numItems = fItems.CountItems(); 2693 if (numItems == 0) 2694 return NULL; 2695 2696 int32 index = fItems.IndexOf(item); 2697 int32 loopCount = numItems; 2698 while (--loopCount) { 2699 // Cycle through menu items in the given direction... 2700 if (forward) 2701 index++; 2702 else 2703 index--; 2704 2705 // ... wrap around... 2706 if (index < 0) 2707 index = numItems - 1; 2708 else if (index >= numItems) 2709 index = 0; 2710 2711 // ... and return the first suitable item found. 2712 BMenuItem* nextItem = ItemAt(index); 2713 if (nextItem->IsEnabled()) 2714 return nextItem; 2715 } 2716 2717 // If no other suitable item was found, return NULL. 2718 return NULL; 2719 } 2720 2721 2722 void 2723 BMenu::_SetStickyMode(bool sticky) 2724 { 2725 if (fStickyMode == sticky) 2726 return; 2727 2728 fStickyMode = sticky; 2729 2730 if (fSuper != NULL) { 2731 // propagate the status to the super menu 2732 fSuper->_SetStickyMode(sticky); 2733 } else { 2734 // TODO: Ugly hack, but it needs to be done in this method 2735 BMenuBar* menuBar = dynamic_cast<BMenuBar*>(this); 2736 if (sticky && menuBar != NULL && menuBar->LockLooper()) { 2737 // If we are switching to sticky mode, 2738 // steal the focus from the current focus view 2739 // (needed to handle keyboard navigation) 2740 menuBar->_StealFocus(); 2741 menuBar->UnlockLooper(); 2742 } 2743 } 2744 } 2745 2746 2747 bool 2748 BMenu::_IsStickyMode() const 2749 { 2750 return fStickyMode; 2751 } 2752 2753 2754 void 2755 BMenu::_GetShiftKey(uint32 &value) const 2756 { 2757 // TODO: Move into init_interface_kit(). 2758 // Currently we can't do that, as get_modifier_key() blocks forever 2759 // when called on input_server initialization, since it tries 2760 // to send a synchronous message to itself (input_server is 2761 // a BApplication) 2762 2763 if (get_modifier_key(B_LEFT_SHIFT_KEY, &value) != B_OK) 2764 value = 0x4b; 2765 } 2766 2767 2768 void 2769 BMenu::_GetControlKey(uint32 &value) const 2770 { 2771 // TODO: Move into init_interface_kit(). 2772 // Currently we can't do that, as get_modifier_key() blocks forever 2773 // when called on input_server initialization, since it tries 2774 // to send a synchronous message to itself (input_server is 2775 // a BApplication) 2776 2777 if (get_modifier_key(B_LEFT_CONTROL_KEY, &value) != B_OK) 2778 value = 0x5c; 2779 } 2780 2781 2782 void 2783 BMenu::_GetCommandKey(uint32 &value) const 2784 { 2785 // TODO: Move into init_interface_kit(). 2786 // Currently we can't do that, as get_modifier_key() blocks forever 2787 // when called on input_server initialization, since it tries 2788 // to send a synchronous message to itself (input_server is 2789 // a BApplication) 2790 2791 if (get_modifier_key(B_LEFT_COMMAND_KEY, &value) != B_OK) 2792 value = 0x66; 2793 } 2794 2795 2796 void 2797 BMenu::_GetOptionKey(uint32 &value) const 2798 { 2799 // TODO: Move into init_interface_kit(). 2800 // Currently we can't do that, as get_modifier_key() blocks forever 2801 // when called on input_server initialization, since it tries 2802 // to send a synchronous message to itself (input_server is 2803 // a BApplication) 2804 2805 if (get_modifier_key(B_LEFT_OPTION_KEY, &value) != B_OK) 2806 value = 0x5d; 2807 } 2808 2809 2810 void 2811 BMenu::_GetMenuKey(uint32 &value) const 2812 { 2813 // TODO: Move into init_interface_kit(). 2814 // Currently we can't do that, as get_modifier_key() blocks forever 2815 // when called on input_server initialization, since it tries 2816 // to send a synchronous message to itself (input_server is 2817 // a BApplication) 2818 2819 if (get_modifier_key(B_MENU_KEY, &value) != B_OK) 2820 value = 0x68; 2821 } 2822 2823 2824 void 2825 BMenu::_CalcTriggers() 2826 { 2827 BPrivate::TriggerList triggerList; 2828 2829 // Gathers the existing triggers set by the user 2830 for (int32 i = 0; i < CountItems(); i++) { 2831 char trigger = ItemAt(i)->Trigger(); 2832 if (trigger != 0) 2833 triggerList.AddTrigger(trigger); 2834 } 2835 2836 // Set triggers for items which don't have one yet 2837 for (int32 i = 0; i < CountItems(); i++) { 2838 BMenuItem* item = ItemAt(i); 2839 if (item->Trigger() == 0) { 2840 uint32 trigger; 2841 int32 index; 2842 if (_ChooseTrigger(item->Label(), index, trigger, triggerList)) 2843 item->SetAutomaticTrigger(index, trigger); 2844 } 2845 } 2846 } 2847 2848 2849 bool 2850 BMenu::_ChooseTrigger(const char* title, int32& index, uint32& trigger, 2851 BPrivate::TriggerList& triggers) 2852 { 2853 if (title == NULL) 2854 return false; 2855 2856 uint32 c; 2857 2858 // two runs: first we look out for uppercase letters 2859 // TODO: support Unicode characters correctly! 2860 for (uint32 i = 0; (c = title[i]) != '\0'; i++) { 2861 if (!IsInsideGlyph(c) && isupper(c) && !triggers.HasTrigger(c)) { 2862 index = i; 2863 trigger = tolower(c); 2864 return triggers.AddTrigger(c); 2865 } 2866 } 2867 2868 // then, if we still haven't found anything, we accept them all 2869 index = 0; 2870 while ((c = UTF8ToCharCode(&title)) != 0) { 2871 if (!isspace(c) && !triggers.HasTrigger(c)) { 2872 trigger = tolower(c); 2873 return triggers.AddTrigger(c); 2874 } 2875 2876 index++; 2877 } 2878 2879 return false; 2880 } 2881 2882 2883 void 2884 BMenu::_UpdateWindowViewSize(const bool &move) 2885 { 2886 BMenuWindow* window = static_cast<BMenuWindow*>(Window()); 2887 if (window == NULL) 2888 return; 2889 2890 if (dynamic_cast<BMenuBar*>(this) != NULL) 2891 return; 2892 2893 if (!fResizeToFit) 2894 return; 2895 2896 bool scroll = false; 2897 const BPoint screenLocation = move ? ScreenLocation() 2898 : window->Frame().LeftTop(); 2899 BRect frame = _CalcFrame(screenLocation, &scroll); 2900 ResizeTo(frame.Width(), frame.Height()); 2901 2902 if (fItems.CountItems() > 0) { 2903 if (!scroll) { 2904 window->ResizeTo(Bounds().Width(), Bounds().Height()); 2905 } else { 2906 BScreen screen(window); 2907 2908 // If we need scrolling, resize the window to fit the screen and 2909 // attach scrollers to our cached BMenuWindow. 2910 if (dynamic_cast<BMenuBar*>(Supermenu()) == NULL || frame.top < 0) { 2911 window->ResizeTo(Bounds().Width(), screen.Frame().Height()); 2912 frame.top = 0; 2913 } else { 2914 // Or, in case our parent was a BMenuBar enable scrolling with 2915 // normal size. 2916 window->ResizeTo(Bounds().Width(), 2917 screen.Frame().bottom - frame.top); 2918 } 2919 2920 if (fLayout == B_ITEMS_IN_COLUMN) { 2921 // we currently only support scrolling for B_ITEMS_IN_COLUMN 2922 window->AttachScrollers(); 2923 2924 BMenuItem* selectedItem = FindMarked(); 2925 if (selectedItem != NULL) { 2926 // scroll to the selected item 2927 if (Supermenu() == NULL) { 2928 window->TryScrollTo(selectedItem->Frame().top); 2929 } else { 2930 BPoint point = selectedItem->Frame().LeftTop(); 2931 BPoint superPoint = Superitem()->Frame().LeftTop(); 2932 Supermenu()->ConvertToScreen(&superPoint); 2933 ConvertToScreen(&point); 2934 window->TryScrollTo(point.y - superPoint.y); 2935 } 2936 } 2937 } 2938 } 2939 } else { 2940 _CacheFontInfo(); 2941 window->ResizeTo(StringWidth(BPrivate::kEmptyMenuLabel) 2942 + fPad.left + fPad.right, 2943 fFontHeight + fPad.top + fPad.bottom); 2944 } 2945 2946 if (move) 2947 window->MoveTo(frame.LeftTop()); 2948 } 2949 2950 2951 bool 2952 BMenu::_AddDynamicItems(bool keyDown) 2953 { 2954 bool addAborted = false; 2955 if (AddDynamicItem(B_INITIAL_ADD)) { 2956 BMenuItem* superItem = Superitem(); 2957 BMenu* superMenu = Supermenu(); 2958 do { 2959 if (superMenu != NULL 2960 && !superMenu->_OkToProceed(superItem, keyDown)) { 2961 AddDynamicItem(B_ABORT); 2962 addAborted = true; 2963 break; 2964 } 2965 } while (AddDynamicItem(B_PROCESSING)); 2966 } 2967 2968 return addAborted; 2969 } 2970 2971 2972 bool 2973 BMenu::_OkToProceed(BMenuItem* item, bool keyDown) 2974 { 2975 BPoint where; 2976 uint32 buttons; 2977 GetMouse(&where, &buttons, false); 2978 bool stickyMode = _IsStickyMode(); 2979 // Quit if user clicks the mouse button in sticky mode 2980 // or releases the mouse button in nonsticky mode 2981 // or moves the pointer over another item 2982 // TODO: I added the check for BMenuBar to solve a problem with Deskbar. 2983 // BeOS seems to do something similar. This could also be a bug in 2984 // Deskbar, though. 2985 if ((buttons != 0 && stickyMode) 2986 || ((dynamic_cast<BMenuBar*>(this) == NULL 2987 && (buttons == 0 && !stickyMode)) 2988 || ((_HitTestItems(where) != item) && !keyDown))) { 2989 return false; 2990 } 2991 2992 return true; 2993 } 2994 2995 2996 bool 2997 BMenu::_CustomTrackingWantsToQuit() 2998 { 2999 if (fExtraMenuData != NULL && fExtraMenuData->trackingHook != NULL 3000 && fExtraMenuData->trackingState != NULL) { 3001 return fExtraMenuData->trackingHook(this, 3002 fExtraMenuData->trackingState); 3003 } 3004 3005 return false; 3006 } 3007 3008 3009 void 3010 BMenu::_QuitTracking(bool onlyThis) 3011 { 3012 _SelectItem(NULL); 3013 if (BMenuBar* menuBar = dynamic_cast<BMenuBar*>(this)) 3014 menuBar->_RestoreFocus(); 3015 3016 fState = MENU_STATE_CLOSED; 3017 3018 if (!onlyThis) { 3019 // Close the whole menu hierarchy 3020 if (Supermenu() != NULL) 3021 Supermenu()->fState = MENU_STATE_CLOSED; 3022 3023 if (_IsStickyMode()) 3024 _SetStickyMode(false); 3025 3026 if (LockLooper()) { 3027 be_app->ShowCursor(); 3028 UnlockLooper(); 3029 } 3030 } 3031 3032 _Hide(); 3033 } 3034 3035 3036 // #pragma mark - menu_info functions 3037 3038 3039 // TODO: Maybe the following two methods would fit better into 3040 // InterfaceDefs.cpp 3041 // In R5, they do all the work client side, we let the app_server handle the 3042 // details. 3043 status_t 3044 set_menu_info(menu_info* info) 3045 { 3046 if (!info) 3047 return B_BAD_VALUE; 3048 3049 BPrivate::AppServerLink link; 3050 link.StartMessage(AS_SET_MENU_INFO); 3051 link.Attach<menu_info>(*info); 3052 3053 status_t status = B_ERROR; 3054 if (link.FlushWithReply(status) == B_OK && status == B_OK) 3055 BMenu::sMenuInfo = *info; 3056 // Update also the local copy, in case anyone relies on it 3057 3058 return status; 3059 } 3060 3061 3062 status_t 3063 get_menu_info(menu_info* info) 3064 { 3065 if (!info) 3066 return B_BAD_VALUE; 3067 3068 BPrivate::AppServerLink link; 3069 link.StartMessage(AS_GET_MENU_INFO); 3070 3071 status_t status = B_ERROR; 3072 if (link.FlushWithReply(status) == B_OK && status == B_OK) 3073 link.Read<menu_info>(info); 3074 3075 return status; 3076 } 3077 3078 3079 extern "C" void 3080 B_IF_GCC_2(InvalidateLayout__5BMenub,_ZN5BMenu16InvalidateLayoutEb)( 3081 BMenu* menu, bool descendants) 3082 { 3083 menu->InvalidateLayout(); 3084 } 3085