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