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