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