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