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 fHasSubmenus(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 fHasSubmenus(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 fHasSubmenus(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 where) 681 { 682 BView::FrameMoved(where); 683 } 684 685 686 void 687 BMenu::FrameResized(float width, float height) 688 { 689 BView::FrameResized(width, 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 false; 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 fHasSubmenus(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 = 0; 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 bool isLeft = ConvertFromScreen(navAreaRectAbove).left == 0; 1915 BPoint p1, p2; 1916 1917 if (inNavAreaRectAbove) { 1918 if (!isLeft) { 1919 p1 = navAreaRectAbove.LeftBottom(); 1920 p2 = navAreaRectAbove.RightTop(); 1921 } else { 1922 p2 = navAreaRectAbove.RightBottom(); 1923 p1 = navAreaRectAbove.LeftTop(); 1924 } 1925 } else { 1926 if (!isLeft) { 1927 p2 = navAreaRectBelow.LeftTop(); 1928 p1 = navAreaRectBelow.RightBottom(); 1929 } else { 1930 p1 = navAreaRectBelow.RightTop(); 1931 p2 = navAreaRectBelow.LeftBottom(); 1932 } 1933 } 1934 bool inNavArea = 1935 (p1.y - p2.y) * position.x + (p2.x - p1.x) * position.y 1936 + (p1.x - p2.x) * p1.y + (p2.y - p1.y) * p1.x >= 0; 1937 1938 bigtime_t systime = system_time(); 1939 1940 if (!inNavArea || (navigationAreaTime > 0 && systime - 1941 navigationAreaTime > kNavigationAreaTimeout)) { 1942 // Don't delay opening of submenu if the user had 1943 // to wait for the navigation area timeout anyway 1944 _SelectItem(item, inNavArea); 1945 1946 if (inNavArea) { 1947 _UpdateNavigationArea(position, navAreaRectAbove, 1948 navAreaRectBelow); 1949 } else { 1950 navAreaRectAbove = BRect(); 1951 navAreaRectBelow = BRect(); 1952 } 1953 1954 selectedTime = system_time(); 1955 navigationAreaTime = 0; 1956 } 1957 } else if (fSelected->Submenu() != NULL && 1958 system_time() - selectedTime > kOpenSubmenuDelay) { 1959 _SelectItem(fSelected, true); 1960 1961 if (!navAreaRectAbove.IsValid() && !navAreaRectBelow.IsValid()) { 1962 position = ConvertToScreen(position); 1963 _UpdateNavigationArea(position, navAreaRectAbove, 1964 navAreaRectBelow); 1965 } 1966 } 1967 1968 if (fState != MENU_STATE_TRACKING) 1969 fState = MENU_STATE_TRACKING; 1970 } 1971 1972 1973 void 1974 BMenu::_UpdateStateClose(BMenuItem* item, const BPoint& where, 1975 const uint32& buttons) 1976 { 1977 if (fState == MENU_STATE_CLOSED) 1978 return; 1979 1980 if (buttons != 0 && _IsStickyMode()) { 1981 if (item == NULL) { 1982 if (item != fSelected && LockLooper()) { 1983 _SelectItem(item, false); 1984 UnlockLooper(); 1985 } 1986 fState = MENU_STATE_CLOSED; 1987 } else 1988 _SetStickyMode(false); 1989 } else if (buttons == 0 && !_IsStickyMode()) { 1990 if (fExtraRect != NULL && fExtraRect->Contains(where)) { 1991 _SetStickyMode(true); 1992 fExtraRect = NULL; 1993 // Setting this to NULL will prevent this code 1994 // to be executed next time 1995 } else { 1996 if (item != fSelected && LockLooper()) { 1997 _SelectItem(item, false); 1998 UnlockLooper(); 1999 } 2000 fState = MENU_STATE_CLOSED; 2001 } 2002 } 2003 } 2004 2005 2006 bool 2007 BMenu::_AddItem(BMenuItem* item, int32 index) 2008 { 2009 ASSERT(item != NULL); 2010 if (index < 0 || index > fItems.CountItems()) 2011 return false; 2012 2013 if (item->IsMarked()) 2014 _ItemMarked(item); 2015 2016 if (!fItems.AddItem(item, index)) 2017 return false; 2018 2019 // install the item on the supermenu's window 2020 // or onto our window, if we are a root menu 2021 BWindow* window = NULL; 2022 if (Superitem() != NULL) 2023 window = Superitem()->fWindow; 2024 else 2025 window = Window(); 2026 if (window != NULL) 2027 item->Install(window); 2028 2029 item->SetSuper(this); 2030 return true; 2031 } 2032 2033 2034 bool 2035 BMenu::_RemoveItems(int32 index, int32 count, BMenuItem* item, 2036 bool deleteItems) 2037 { 2038 bool success = false; 2039 bool invalidateLayout = false; 2040 2041 bool locked = LockLooper(); 2042 BWindow* window = Window(); 2043 2044 // The plan is simple: If we're given a BMenuItem directly, we use it 2045 // and ignore index and count. Otherwise, we use them instead. 2046 if (item != NULL) { 2047 if (fItems.RemoveItem(item)) { 2048 if (item == fSelected && window != NULL) 2049 _SelectItem(NULL); 2050 item->Uninstall(); 2051 item->SetSuper(NULL); 2052 if (deleteItems) 2053 delete item; 2054 success = invalidateLayout = true; 2055 } 2056 } else { 2057 // We iterate backwards because it's simpler 2058 int32 i = std::min(index + count - 1, fItems.CountItems() - 1); 2059 // NOTE: the range check for "index" is done after 2060 // calculating the last index to be removed, so 2061 // that the range is not "shifted" unintentionally 2062 index = std::max((int32)0, index); 2063 for (; i >= index; i--) { 2064 item = static_cast<BMenuItem*>(fItems.ItemAt(i)); 2065 if (item != NULL) { 2066 if (fItems.RemoveItem(item)) { 2067 if (item == fSelected && window != NULL) 2068 _SelectItem(NULL); 2069 item->Uninstall(); 2070 item->SetSuper(NULL); 2071 if (deleteItems) 2072 delete item; 2073 success = true; 2074 invalidateLayout = true; 2075 } else { 2076 // operation not entirely successful 2077 success = false; 2078 break; 2079 } 2080 } 2081 } 2082 } 2083 2084 if (invalidateLayout) { 2085 InvalidateLayout(); 2086 if (locked && window != NULL) { 2087 _LayoutItems(0); 2088 _UpdateWindowViewSize(false); 2089 Invalidate(); 2090 } 2091 } 2092 2093 if (locked) 2094 UnlockLooper(); 2095 2096 return success; 2097 } 2098 2099 2100 bool 2101 BMenu::_RelayoutIfNeeded() 2102 { 2103 if (!fUseCachedMenuLayout) { 2104 fUseCachedMenuLayout = true; 2105 _CacheFontInfo(); 2106 _LayoutItems(0); 2107 return true; 2108 } 2109 return false; 2110 } 2111 2112 2113 void 2114 BMenu::_LayoutItems(int32 index) 2115 { 2116 _CalcTriggers(); 2117 2118 float width; 2119 float height; 2120 _ComputeLayout(index, fResizeToFit, true, &width, &height); 2121 2122 if (fResizeToFit) 2123 ResizeTo(width, height); 2124 } 2125 2126 2127 BSize 2128 BMenu::_ValidatePreferredSize() 2129 { 2130 if (!fLayoutData->preferred.IsWidthSet() || ResizingMode() 2131 != fLayoutData->lastResizingMode) { 2132 _ComputeLayout(0, true, false, NULL, NULL); 2133 ResetLayoutInvalidation(); 2134 } 2135 2136 return fLayoutData->preferred; 2137 } 2138 2139 2140 void 2141 BMenu::_ComputeLayout(int32 index, bool bestFit, bool moveItems, 2142 float* _width, float* _height) 2143 { 2144 // TODO: Take "bestFit", "moveItems", "index" into account, 2145 // Recalculate only the needed items, 2146 // not the whole layout every time 2147 2148 fLayoutData->lastResizingMode = ResizingMode(); 2149 2150 BRect frame; 2151 switch (fLayout) { 2152 case B_ITEMS_IN_COLUMN: 2153 { 2154 BRect parentFrame; 2155 BRect* overrideFrame = NULL; 2156 if (dynamic_cast<_BMCMenuBar_*>(Supermenu()) != NULL) { 2157 // When the menu is modified while it's open, we get here in a 2158 // situation where trying to lock the looper would deadlock 2159 // (the window is locked waiting for the menu to terminate). 2160 // In that case, just give up on getting the supermenu bounds 2161 // and keep the menu at the current width and position. 2162 if (Supermenu()->LockLooperWithTimeout(0) == B_OK) { 2163 parentFrame = Supermenu()->Bounds(); 2164 Supermenu()->UnlockLooper(); 2165 overrideFrame = &parentFrame; 2166 } 2167 } 2168 2169 _ComputeColumnLayout(index, bestFit, moveItems, overrideFrame, 2170 frame); 2171 break; 2172 } 2173 2174 case B_ITEMS_IN_ROW: 2175 _ComputeRowLayout(index, bestFit, moveItems, frame); 2176 break; 2177 2178 case B_ITEMS_IN_MATRIX: 2179 _ComputeMatrixLayout(frame); 2180 break; 2181 } 2182 2183 // change width depending on resize mode 2184 BSize size; 2185 if ((ResizingMode() & B_FOLLOW_LEFT_RIGHT) == B_FOLLOW_LEFT_RIGHT) { 2186 if (dynamic_cast<_BMCMenuBar_*>(this) != NULL) 2187 size.width = Bounds().Width() - fPad.right; 2188 else if (Parent() != NULL) 2189 size.width = Parent()->Frame().Width(); 2190 else if (Window() != NULL) 2191 size.width = Window()->Frame().Width(); 2192 else 2193 size.width = Bounds().Width(); 2194 } else 2195 size.width = frame.Width(); 2196 2197 size.height = frame.Height(); 2198 2199 if (_width) 2200 *_width = size.width; 2201 2202 if (_height) 2203 *_height = size.height; 2204 2205 if (bestFit) 2206 fLayoutData->preferred = size; 2207 2208 if (moveItems) 2209 fUseCachedMenuLayout = true; 2210 } 2211 2212 2213 void 2214 BMenu::_ComputeColumnLayout(int32 index, bool bestFit, bool moveItems, 2215 BRect* overrideFrame, BRect& frame) 2216 { 2217 bool command = false; 2218 bool control = false; 2219 bool shift = false; 2220 bool option = false; 2221 bool submenu = false; 2222 2223 if (index > 0) 2224 frame = ItemAt(index - 1)->Frame(); 2225 else if (overrideFrame != NULL) 2226 frame.Set(0, 0, overrideFrame->right, -1); 2227 else 2228 frame.Set(0, 0, 0, -1); 2229 2230 BFont font; 2231 GetFont(&font); 2232 2233 // Loop over all items to set their top, bottom and left coordinates, 2234 // all while computing the width of the menu 2235 for (; index < fItems.CountItems(); index++) { 2236 BMenuItem* item = ItemAt(index); 2237 2238 float width; 2239 float height; 2240 item->GetContentSize(&width, &height); 2241 2242 if (item->fModifiers && item->fShortcutChar) { 2243 width += font.Size(); 2244 if ((item->fModifiers & B_COMMAND_KEY) != 0) 2245 command = true; 2246 2247 if ((item->fModifiers & B_CONTROL_KEY) != 0) 2248 control = true; 2249 2250 if ((item->fModifiers & B_SHIFT_KEY) != 0) 2251 shift = true; 2252 2253 if ((item->fModifiers & B_OPTION_KEY) != 0) 2254 option = true; 2255 } 2256 2257 item->fBounds.left = 0.0f; 2258 item->fBounds.top = frame.bottom + 1.0f; 2259 item->fBounds.bottom = item->fBounds.top + height + fPad.top 2260 + fPad.bottom; 2261 2262 if (item->fSubmenu != NULL) 2263 submenu = true; 2264 2265 frame.right = std::max(frame.right, width + fPad.left + fPad.right); 2266 frame.bottom = item->fBounds.bottom; 2267 } 2268 2269 // Compute the extra space needed for shortcuts and submenus 2270 if (command) { 2271 frame.right 2272 += BPrivate::MenuPrivate::MenuItemCommand()->Bounds().Width() + 1; 2273 } 2274 if (control) { 2275 frame.right 2276 += BPrivate::MenuPrivate::MenuItemControl()->Bounds().Width() + 1; 2277 } 2278 if (option) { 2279 frame.right 2280 += BPrivate::MenuPrivate::MenuItemOption()->Bounds().Width() + 1; 2281 } 2282 if (shift) { 2283 frame.right 2284 += BPrivate::MenuPrivate::MenuItemShift()->Bounds().Width() + 1; 2285 } 2286 if (submenu) { 2287 frame.right += ItemAt(0)->Frame().Height() / 2; 2288 fHasSubmenus = true; 2289 } else { 2290 fHasSubmenus = false; 2291 } 2292 2293 if (fMaxContentWidth > 0) 2294 frame.right = std::min(frame.right, fMaxContentWidth); 2295 2296 // Finally update the "right" coordinate of all items 2297 if (moveItems) { 2298 for (int32 i = 0; i < fItems.CountItems(); i++) 2299 ItemAt(i)->fBounds.right = frame.right; 2300 } 2301 2302 frame.top = 0; 2303 frame.right = ceilf(frame.right); 2304 } 2305 2306 2307 void 2308 BMenu::_ComputeRowLayout(int32 index, bool bestFit, bool moveItems, 2309 BRect& frame) 2310 { 2311 font_height fh; 2312 GetFontHeight(&fh); 2313 frame.Set(0.0f, 0.0f, 0.0f, ceilf(fh.ascent + fh.descent + fPad.top 2314 + fPad.bottom)); 2315 2316 for (int32 i = 0; i < fItems.CountItems(); i++) { 2317 BMenuItem* item = ItemAt(i); 2318 2319 float width, height; 2320 item->GetContentSize(&width, &height); 2321 2322 item->fBounds.left = frame.right; 2323 item->fBounds.top = 0.0f; 2324 item->fBounds.right = item->fBounds.left + width + fPad.left 2325 + fPad.right; 2326 2327 frame.right = item->Frame().right + 1.0f; 2328 frame.bottom = std::max(frame.bottom, height + fPad.top + fPad.bottom); 2329 } 2330 2331 if (moveItems) { 2332 for (int32 i = 0; i < fItems.CountItems(); i++) 2333 ItemAt(i)->fBounds.bottom = frame.bottom; 2334 } 2335 2336 if (bestFit) 2337 frame.right = ceilf(frame.right); 2338 else 2339 frame.right = Bounds().right; 2340 } 2341 2342 2343 void 2344 BMenu::_ComputeMatrixLayout(BRect &frame) 2345 { 2346 frame.Set(0, 0, 0, 0); 2347 for (int32 i = 0; i < CountItems(); i++) { 2348 BMenuItem* item = ItemAt(i); 2349 if (item != NULL) { 2350 frame.left = std::min(frame.left, item->Frame().left); 2351 frame.right = std::max(frame.right, item->Frame().right); 2352 frame.top = std::min(frame.top, item->Frame().top); 2353 frame.bottom = std::max(frame.bottom, item->Frame().bottom); 2354 } 2355 } 2356 } 2357 2358 2359 void 2360 BMenu::LayoutInvalidated(bool descendants) 2361 { 2362 fUseCachedMenuLayout = false; 2363 fLayoutData->preferred.Set(B_SIZE_UNSET, B_SIZE_UNSET); 2364 } 2365 2366 2367 // Assumes the SuperMenu to be locked (due to calling ConvertToScreen()) 2368 BPoint 2369 BMenu::ScreenLocation() 2370 { 2371 BMenu* superMenu = Supermenu(); 2372 BMenuItem* superItem = Superitem(); 2373 2374 if (superMenu == NULL || superItem == NULL) { 2375 debugger("BMenu can't determine where to draw." 2376 "Override BMenu::ScreenLocation() to determine location."); 2377 } 2378 2379 BPoint point; 2380 if (superMenu->Layout() == B_ITEMS_IN_COLUMN) 2381 point = superItem->Frame().RightTop() + BPoint(1.0f, 1.0f); 2382 else 2383 point = superItem->Frame().LeftBottom() + BPoint(1.0f, 1.0f); 2384 2385 superMenu->ConvertToScreen(&point); 2386 2387 return point; 2388 } 2389 2390 2391 BRect 2392 BMenu::_CalcFrame(BPoint where, bool* scrollOn) 2393 { 2394 // TODO: Improve me 2395 BRect bounds = Bounds(); 2396 BRect frame = bounds.OffsetToCopy(where); 2397 2398 BScreen screen(Window()); 2399 BRect screenFrame = screen.Frame(); 2400 2401 BMenu* superMenu = Supermenu(); 2402 BMenuItem* superItem = Superitem(); 2403 2404 // Reset frame shifted state since this menu is being redrawn 2405 fExtraMenuData->frameShiftedLeft = false; 2406 2407 // TODO: Horrible hack: 2408 // When added to a BMenuField, a BPopUpMenu is the child of 2409 // a _BMCMenuBar_ to "fake" the menu hierarchy 2410 bool inMenuField = dynamic_cast<_BMCMenuBar_*>(superMenu) != NULL; 2411 2412 // Offset the menu field menu window left by the width of the checkmark 2413 // so that the text when the menu is closed lines up with the text when 2414 // the menu is open. 2415 if (inMenuField) 2416 frame.OffsetBy(-8.0f, 0.0f); 2417 2418 bool scroll = false; 2419 if (superMenu == NULL || superItem == NULL || inMenuField) { 2420 // just move the window on screen 2421 if (frame.bottom > screenFrame.bottom) 2422 frame.OffsetBy(0, screenFrame.bottom - frame.bottom); 2423 else if (frame.top < screenFrame.top) 2424 frame.OffsetBy(0, -frame.top); 2425 2426 if (frame.right > screenFrame.right) { 2427 frame.OffsetBy(screenFrame.right - frame.right, 0); 2428 fExtraMenuData->frameShiftedLeft = true; 2429 } 2430 else if (frame.left < screenFrame.left) 2431 frame.OffsetBy(-frame.left, 0); 2432 } else if (superMenu->Layout() == B_ITEMS_IN_COLUMN) { 2433 if (frame.right > screenFrame.right 2434 || superMenu->fExtraMenuData->frameShiftedLeft) { 2435 frame.OffsetBy(-superItem->Frame().Width() - frame.Width() - 2, 0); 2436 fExtraMenuData->frameShiftedLeft = true; 2437 } 2438 2439 if (frame.left < 0) 2440 frame.OffsetBy(-frame.left + 6, 0); 2441 2442 if (frame.bottom > screenFrame.bottom) 2443 frame.OffsetBy(0, screenFrame.bottom - frame.bottom); 2444 } else { 2445 if (frame.bottom > screenFrame.bottom) { 2446 frame.OffsetBy(0, -superItem->Frame().Height() 2447 - frame.Height() - 3); 2448 } 2449 2450 if (frame.right > screenFrame.right) 2451 frame.OffsetBy(screenFrame.right - frame.right, 0); 2452 } 2453 2454 if (!scroll) { 2455 // basically, if this returns false, it means 2456 // that the menu frame won't fit completely inside the screen 2457 // TODO: Scrolling will currently only work up/down, 2458 // not left/right 2459 scroll = screenFrame.Height() < frame.Height(); 2460 } 2461 2462 if (scrollOn != NULL) 2463 *scrollOn = scroll; 2464 2465 return frame; 2466 } 2467 2468 2469 void 2470 BMenu::DrawItems(BRect updateRect) 2471 { 2472 int32 itemCount = fItems.CountItems(); 2473 for (int32 i = 0; i < itemCount; i++) { 2474 BMenuItem* item = ItemAt(i); 2475 if (item->Frame().Intersects(updateRect)) 2476 item->Draw(); 2477 } 2478 } 2479 2480 2481 int 2482 BMenu::_State(BMenuItem** item) const 2483 { 2484 if (fState == MENU_STATE_TRACKING || fState == MENU_STATE_CLOSED) 2485 return fState; 2486 2487 if (fSelected != NULL && fSelected->Submenu() != NULL) 2488 return fSelected->Submenu()->_State(item); 2489 2490 return fState; 2491 } 2492 2493 2494 void 2495 BMenu::_InvokeItem(BMenuItem* item, bool now) 2496 { 2497 if (!item->IsEnabled()) 2498 return; 2499 2500 // Do the "selected" animation 2501 // TODO: Doesn't work. This is supposed to highlight 2502 // and dehighlight the item, works on beos but not on haiku. 2503 if (!item->Submenu() && LockLooper()) { 2504 snooze(50000); 2505 item->Select(true); 2506 Window()->UpdateIfNeeded(); 2507 snooze(50000); 2508 item->Select(false); 2509 Window()->UpdateIfNeeded(); 2510 snooze(50000); 2511 item->Select(true); 2512 Window()->UpdateIfNeeded(); 2513 snooze(50000); 2514 item->Select(false); 2515 Window()->UpdateIfNeeded(); 2516 UnlockLooper(); 2517 } 2518 2519 // Lock the root menu window before calling BMenuItem::Invoke() 2520 BMenu* parent = this; 2521 BMenu* rootMenu = NULL; 2522 do { 2523 rootMenu = parent; 2524 parent = rootMenu->Supermenu(); 2525 } while (parent != NULL); 2526 2527 if (rootMenu->LockLooper()) { 2528 item->Invoke(); 2529 rootMenu->UnlockLooper(); 2530 } 2531 } 2532 2533 2534 bool 2535 BMenu::_OverSuper(BPoint location) 2536 { 2537 if (!Supermenu()) 2538 return false; 2539 2540 return fSuperbounds.Contains(location); 2541 } 2542 2543 2544 bool 2545 BMenu::_OverSubmenu(BMenuItem* item, BPoint loc) 2546 { 2547 if (item == NULL) 2548 return false; 2549 2550 BMenu* subMenu = item->Submenu(); 2551 if (subMenu == NULL || subMenu->Window() == NULL) 2552 return false; 2553 2554 // assume that loc is in screen coordinates 2555 if (subMenu->Window()->Frame().Contains(loc)) 2556 return true; 2557 2558 return subMenu->_OverSubmenu(subMenu->fSelected, loc); 2559 } 2560 2561 2562 BMenuWindow* 2563 BMenu::_MenuWindow() 2564 { 2565 #if USE_CACHED_MENUWINDOW 2566 if (fCachedMenuWindow == NULL) { 2567 char windowName[64]; 2568 snprintf(windowName, 64, "%s cached menu", Name()); 2569 fCachedMenuWindow = new (nothrow) BMenuWindow(windowName); 2570 } 2571 #endif 2572 return fCachedMenuWindow; 2573 } 2574 2575 2576 void 2577 BMenu::_DeleteMenuWindow() 2578 { 2579 if (fCachedMenuWindow != NULL) { 2580 fCachedMenuWindow->Lock(); 2581 fCachedMenuWindow->Quit(); 2582 fCachedMenuWindow = NULL; 2583 } 2584 } 2585 2586 2587 BMenuItem* 2588 BMenu::_HitTestItems(BPoint where, BPoint slop) const 2589 { 2590 // TODO: Take "slop" into account ? 2591 2592 // if the point doesn't lie within the menu's 2593 // bounds, bail out immediately 2594 if (!Bounds().Contains(where)) 2595 return NULL; 2596 2597 int32 itemCount = CountItems(); 2598 for (int32 i = 0; i < itemCount; i++) { 2599 BMenuItem* item = ItemAt(i); 2600 if (item->Frame().Contains(where) 2601 && dynamic_cast<BSeparatorItem*>(item) == NULL) { 2602 return item; 2603 } 2604 } 2605 2606 return NULL; 2607 } 2608 2609 2610 BRect 2611 BMenu::_Superbounds() const 2612 { 2613 return fSuperbounds; 2614 } 2615 2616 2617 void 2618 BMenu::_CacheFontInfo() 2619 { 2620 font_height fh; 2621 GetFontHeight(&fh); 2622 fAscent = fh.ascent; 2623 fDescent = fh.descent; 2624 fFontHeight = ceilf(fh.ascent + fh.descent + fh.leading); 2625 } 2626 2627 2628 void 2629 BMenu::_ItemMarked(BMenuItem* item) 2630 { 2631 if (IsRadioMode()) { 2632 for (int32 i = 0; i < CountItems(); i++) { 2633 if (ItemAt(i) != item) 2634 ItemAt(i)->SetMarked(false); 2635 } 2636 } 2637 2638 if (IsLabelFromMarked() && Superitem() != NULL) 2639 Superitem()->SetLabel(item->Label()); 2640 } 2641 2642 2643 void 2644 BMenu::_Install(BWindow* target) 2645 { 2646 for (int32 i = 0; i < CountItems(); i++) 2647 ItemAt(i)->Install(target); 2648 } 2649 2650 2651 void 2652 BMenu::_Uninstall() 2653 { 2654 for (int32 i = 0; i < CountItems(); i++) 2655 ItemAt(i)->Uninstall(); 2656 } 2657 2658 2659 void 2660 BMenu::_SelectItem(BMenuItem* item, bool showSubmenu, bool selectFirstItem, 2661 bool keyDown) 2662 { 2663 // Avoid deselecting and then reselecting the same item 2664 // which would cause flickering 2665 if (item != fSelected) { 2666 if (fSelected != NULL) { 2667 fSelected->Select(false); 2668 BMenu* subMenu = fSelected->Submenu(); 2669 if (subMenu != NULL && subMenu->Window() != NULL) 2670 subMenu->_Hide(); 2671 } 2672 2673 fSelected = item; 2674 if (fSelected != NULL) 2675 fSelected->Select(true); 2676 } 2677 2678 if (fSelected != NULL && showSubmenu) { 2679 BMenu* subMenu = fSelected->Submenu(); 2680 if (subMenu != NULL && subMenu->Window() == NULL) { 2681 if (!subMenu->_Show(selectFirstItem, keyDown)) { 2682 // something went wrong, deselect the item 2683 fSelected->Select(false); 2684 fSelected = NULL; 2685 } 2686 } 2687 } 2688 } 2689 2690 2691 bool 2692 BMenu::_SelectNextItem(BMenuItem* item, bool forward) 2693 { 2694 if (CountItems() == 0) // cannot select next item in an empty menu 2695 return false; 2696 2697 BMenuItem* nextItem = _NextItem(item, forward); 2698 if (nextItem == NULL) 2699 return false; 2700 2701 _SelectItem(nextItem, dynamic_cast<BMenuBar*>(this) != NULL); 2702 2703 if (LockLooper()) { 2704 be_app->ObscureCursor(); 2705 UnlockLooper(); 2706 } 2707 2708 return true; 2709 } 2710 2711 2712 BMenuItem* 2713 BMenu::_NextItem(BMenuItem* item, bool forward) const 2714 { 2715 const int32 numItems = fItems.CountItems(); 2716 if (numItems == 0) 2717 return NULL; 2718 2719 int32 index = fItems.IndexOf(item); 2720 int32 loopCount = numItems; 2721 while (--loopCount) { 2722 // Cycle through menu items in the given direction... 2723 if (forward) 2724 index++; 2725 else 2726 index--; 2727 2728 // ... wrap around... 2729 if (index < 0) 2730 index = numItems - 1; 2731 else if (index >= numItems) 2732 index = 0; 2733 2734 // ... and return the first suitable item found. 2735 BMenuItem* nextItem = ItemAt(index); 2736 if (nextItem->IsEnabled()) 2737 return nextItem; 2738 } 2739 2740 // If no other suitable item was found, return NULL. 2741 return NULL; 2742 } 2743 2744 2745 void 2746 BMenu::_SetStickyMode(bool sticky) 2747 { 2748 if (fStickyMode == sticky) 2749 return; 2750 2751 fStickyMode = sticky; 2752 2753 if (fSuper != NULL) { 2754 // propagate the status to the super menu 2755 fSuper->_SetStickyMode(sticky); 2756 } else { 2757 // TODO: Ugly hack, but it needs to be done in this method 2758 BMenuBar* menuBar = dynamic_cast<BMenuBar*>(this); 2759 if (sticky && menuBar != NULL && menuBar->LockLooper()) { 2760 // If we are switching to sticky mode, 2761 // steal the focus from the current focus view 2762 // (needed to handle keyboard navigation) 2763 menuBar->_StealFocus(); 2764 menuBar->UnlockLooper(); 2765 } 2766 } 2767 } 2768 2769 2770 bool 2771 BMenu::_IsStickyMode() const 2772 { 2773 return fStickyMode; 2774 } 2775 2776 2777 void 2778 BMenu::_GetShiftKey(uint32 &value) const 2779 { 2780 // TODO: Move into init_interface_kit(). 2781 // Currently we can't do that, as get_modifier_key() blocks forever 2782 // when called on input_server initialization, since it tries 2783 // to send a synchronous message to itself (input_server is 2784 // a BApplication) 2785 2786 if (get_modifier_key(B_LEFT_SHIFT_KEY, &value) != B_OK) 2787 value = 0x4b; 2788 } 2789 2790 2791 void 2792 BMenu::_GetControlKey(uint32 &value) const 2793 { 2794 // TODO: Move into init_interface_kit(). 2795 // Currently we can't do that, as get_modifier_key() blocks forever 2796 // when called on input_server initialization, since it tries 2797 // to send a synchronous message to itself (input_server is 2798 // a BApplication) 2799 2800 if (get_modifier_key(B_LEFT_CONTROL_KEY, &value) != B_OK) 2801 value = 0x5c; 2802 } 2803 2804 2805 void 2806 BMenu::_GetCommandKey(uint32 &value) const 2807 { 2808 // TODO: Move into init_interface_kit(). 2809 // Currently we can't do that, as get_modifier_key() blocks forever 2810 // when called on input_server initialization, since it tries 2811 // to send a synchronous message to itself (input_server is 2812 // a BApplication) 2813 2814 if (get_modifier_key(B_LEFT_COMMAND_KEY, &value) != B_OK) 2815 value = 0x66; 2816 } 2817 2818 2819 void 2820 BMenu::_GetOptionKey(uint32 &value) const 2821 { 2822 // TODO: Move into init_interface_kit(). 2823 // Currently we can't do that, as get_modifier_key() blocks forever 2824 // when called on input_server initialization, since it tries 2825 // to send a synchronous message to itself (input_server is 2826 // a BApplication) 2827 2828 if (get_modifier_key(B_LEFT_OPTION_KEY, &value) != B_OK) 2829 value = 0x5d; 2830 } 2831 2832 2833 void 2834 BMenu::_GetMenuKey(uint32 &value) const 2835 { 2836 // TODO: Move into init_interface_kit(). 2837 // Currently we can't do that, as get_modifier_key() blocks forever 2838 // when called on input_server initialization, since it tries 2839 // to send a synchronous message to itself (input_server is 2840 // a BApplication) 2841 2842 if (get_modifier_key(B_MENU_KEY, &value) != B_OK) 2843 value = 0x68; 2844 } 2845 2846 2847 void 2848 BMenu::_CalcTriggers() 2849 { 2850 BPrivate::TriggerList triggerList; 2851 2852 // Gathers the existing triggers set by the user 2853 for (int32 i = 0; i < CountItems(); i++) { 2854 char trigger = ItemAt(i)->Trigger(); 2855 if (trigger != 0) 2856 triggerList.AddTrigger(trigger); 2857 } 2858 2859 // Set triggers for items which don't have one yet 2860 for (int32 i = 0; i < CountItems(); i++) { 2861 BMenuItem* item = ItemAt(i); 2862 if (item->Trigger() == 0) { 2863 uint32 trigger; 2864 int32 index; 2865 if (_ChooseTrigger(item->Label(), index, trigger, triggerList)) 2866 item->SetAutomaticTrigger(index, trigger); 2867 } 2868 } 2869 } 2870 2871 2872 bool 2873 BMenu::_ChooseTrigger(const char* title, int32& index, uint32& trigger, 2874 BPrivate::TriggerList& triggers) 2875 { 2876 if (title == NULL) 2877 return false; 2878 2879 index = 0; 2880 uint32 c; 2881 const char* nextCharacter, *character; 2882 2883 // two runs: first we look out for alphanumeric ASCII characters 2884 nextCharacter = title; 2885 character = nextCharacter; 2886 while ((c = BUnicodeChar::FromUTF8(&nextCharacter)) != 0) { 2887 if (!(c < 128 && BUnicodeChar::IsAlNum(c)) || triggers.HasTrigger(c)) { 2888 character = nextCharacter; 2889 continue; 2890 } 2891 trigger = BUnicodeChar::ToLower(c); 2892 index = (int32)(character - title); 2893 return triggers.AddTrigger(c); 2894 } 2895 2896 // then, if we still haven't found something, we accept anything 2897 nextCharacter = title; 2898 character = nextCharacter; 2899 while ((c = BUnicodeChar::FromUTF8(&nextCharacter)) != 0) { 2900 if (BUnicodeChar::IsSpace(c) || triggers.HasTrigger(c)) { 2901 character = nextCharacter; 2902 continue; 2903 } 2904 trigger = BUnicodeChar::ToLower(c); 2905 index = (int32)(character - title); 2906 return triggers.AddTrigger(c); 2907 } 2908 2909 return false; 2910 } 2911 2912 2913 void 2914 BMenu::_UpdateWindowViewSize(const bool &move) 2915 { 2916 BMenuWindow* window = static_cast<BMenuWindow*>(Window()); 2917 if (window == NULL) 2918 return; 2919 2920 if (dynamic_cast<BMenuBar*>(this) != NULL) 2921 return; 2922 2923 if (!fResizeToFit) 2924 return; 2925 2926 bool scroll = false; 2927 const BPoint screenLocation = move ? ScreenLocation() 2928 : window->Frame().LeftTop(); 2929 BRect frame = _CalcFrame(screenLocation, &scroll); 2930 ResizeTo(frame.Width(), frame.Height()); 2931 2932 if (fItems.CountItems() > 0) { 2933 if (!scroll) { 2934 if (fLayout == B_ITEMS_IN_COLUMN) 2935 window->DetachScrollers(); 2936 2937 window->ResizeTo(Bounds().Width(), Bounds().Height()); 2938 } else { 2939 BScreen screen(window); 2940 2941 // Only scroll on menus not attached to a menubar, or when the 2942 // menu frame is above the visible screen 2943 if (dynamic_cast<BMenuBar*>(Supermenu()) == NULL || frame.top < 0) { 2944 2945 // If we need scrolling, resize the window to fit the screen and 2946 // attach scrollers to our cached BMenuWindow. 2947 window->ResizeTo(Bounds().Width(), screen.Frame().Height()); 2948 frame.top = 0; 2949 2950 // we currently only support scrolling for B_ITEMS_IN_COLUMN 2951 if (fLayout == B_ITEMS_IN_COLUMN) { 2952 window->AttachScrollers(); 2953 2954 BMenuItem* selectedItem = FindMarked(); 2955 if (selectedItem != NULL) { 2956 // scroll to the selected item 2957 if (Supermenu() == NULL) { 2958 window->TryScrollTo(selectedItem->Frame().top); 2959 } else { 2960 BPoint point = selectedItem->Frame().LeftTop(); 2961 BPoint superPoint = Superitem()->Frame().LeftTop(); 2962 Supermenu()->ConvertToScreen(&superPoint); 2963 ConvertToScreen(&point); 2964 window->TryScrollTo(point.y - superPoint.y); 2965 } 2966 } 2967 } 2968 } 2969 } 2970 } else { 2971 _CacheFontInfo(); 2972 window->ResizeTo(StringWidth(BPrivate::kEmptyMenuLabel) 2973 + fPad.left + fPad.right, 2974 fFontHeight + fPad.top + fPad.bottom); 2975 } 2976 2977 if (move) 2978 window->MoveTo(frame.LeftTop()); 2979 } 2980 2981 2982 bool 2983 BMenu::_AddDynamicItems(bool keyDown) 2984 { 2985 bool addAborted = false; 2986 if (AddDynamicItem(B_INITIAL_ADD)) { 2987 BMenuItem* superItem = Superitem(); 2988 BMenu* superMenu = Supermenu(); 2989 do { 2990 if (superMenu != NULL 2991 && !superMenu->_OkToProceed(superItem, keyDown)) { 2992 AddDynamicItem(B_ABORT); 2993 addAborted = true; 2994 break; 2995 } 2996 } while (AddDynamicItem(B_PROCESSING)); 2997 } 2998 2999 return addAborted; 3000 } 3001 3002 3003 bool 3004 BMenu::_OkToProceed(BMenuItem* item, bool keyDown) 3005 { 3006 BPoint where; 3007 uint32 buttons; 3008 GetMouse(&where, &buttons, false); 3009 bool stickyMode = _IsStickyMode(); 3010 // Quit if user clicks the mouse button in sticky mode 3011 // or releases the mouse button in nonsticky mode 3012 // or moves the pointer over another item 3013 // TODO: I added the check for BMenuBar to solve a problem with Deskbar. 3014 // BeOS seems to do something similar. This could also be a bug in 3015 // Deskbar, though. 3016 if ((buttons != 0 && stickyMode) 3017 || ((dynamic_cast<BMenuBar*>(this) == NULL 3018 && (buttons == 0 && !stickyMode)) 3019 || ((_HitTestItems(where) != item) && !keyDown))) { 3020 return false; 3021 } 3022 3023 return true; 3024 } 3025 3026 3027 bool 3028 BMenu::_CustomTrackingWantsToQuit() 3029 { 3030 if (fExtraMenuData != NULL && fExtraMenuData->trackingHook != NULL 3031 && fExtraMenuData->trackingState != NULL) { 3032 return fExtraMenuData->trackingHook(this, 3033 fExtraMenuData->trackingState); 3034 } 3035 3036 return false; 3037 } 3038 3039 3040 void 3041 BMenu::_QuitTracking(bool onlyThis) 3042 { 3043 _SelectItem(NULL); 3044 if (BMenuBar* menuBar = dynamic_cast<BMenuBar*>(this)) 3045 menuBar->_RestoreFocus(); 3046 3047 fState = MENU_STATE_CLOSED; 3048 3049 if (!onlyThis) { 3050 // Close the whole menu hierarchy 3051 if (Supermenu() != NULL) 3052 Supermenu()->fState = MENU_STATE_CLOSED; 3053 3054 if (_IsStickyMode()) 3055 _SetStickyMode(false); 3056 3057 if (LockLooper()) { 3058 be_app->ShowCursor(); 3059 UnlockLooper(); 3060 } 3061 } 3062 3063 _Hide(); 3064 } 3065 3066 3067 // #pragma mark - menu_info functions 3068 3069 3070 // TODO: Maybe the following two methods would fit better into 3071 // InterfaceDefs.cpp 3072 // In R5, they do all the work client side, we let the app_server handle the 3073 // details. 3074 status_t 3075 set_menu_info(menu_info* info) 3076 { 3077 if (!info) 3078 return B_BAD_VALUE; 3079 3080 BPrivate::AppServerLink link; 3081 link.StartMessage(AS_SET_MENU_INFO); 3082 link.Attach<menu_info>(*info); 3083 3084 status_t status = B_ERROR; 3085 if (link.FlushWithReply(status) == B_OK && status == B_OK) 3086 BMenu::sMenuInfo = *info; 3087 // Update also the local copy, in case anyone relies on it 3088 3089 return status; 3090 } 3091 3092 3093 status_t 3094 get_menu_info(menu_info* info) 3095 { 3096 if (!info) 3097 return B_BAD_VALUE; 3098 3099 BPrivate::AppServerLink link; 3100 link.StartMessage(AS_GET_MENU_INFO); 3101 3102 status_t status = B_ERROR; 3103 if (link.FlushWithReply(status) == B_OK && status == B_OK) 3104 link.Read<menu_info>(info); 3105 3106 return status; 3107 } 3108 3109 3110 extern "C" void 3111 B_IF_GCC_2(InvalidateLayout__5BMenub,_ZN5BMenu16InvalidateLayoutEb)( 3112 BMenu* menu, bool descendants) 3113 { 3114 menu->InvalidateLayout(); 3115 } 3116