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(false); 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(false); 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(false); 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(true); 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 (releasedOnce) 1659 _UpdateStateClose(item, location, buttons); 1660 1661 if (fState != MENU_STATE_CLOSED) { 1662 bigtime_t snoozeAmount = 50000; 1663 1664 BPoint newLocation = location; 1665 uint32 newButtons = buttons; 1666 1667 // If user doesn't move the mouse, loop here, 1668 // so we don't interfer with keyboard menu navigation 1669 do { 1670 snooze(snoozeAmount); 1671 if (!LockLooper()) 1672 break; 1673 GetMouse(&newLocation, &newButtons, true); 1674 UnlockLooper(); 1675 } while (newLocation == location && newButtons == buttons 1676 && !(item && item->Submenu() != NULL)); 1677 bigtime_t newPollTime = system_time(); 1678 1679 // mouseSpeed in px per ms 1680 // (actually point_distance returns the square of the distance, 1681 // so it's more px^2 per ms) 1682 mouseSpeed = (int32)(point_distance(newLocation, location) * 1000 1683 / (newPollTime - pollTime)); 1684 pollTime = newPollTime; 1685 1686 if (newLocation != location || newButtons != buttons) { 1687 if (!releasedOnce && newButtons == 0 && buttons != 0) 1688 releasedOnce = true; 1689 location = newLocation; 1690 buttons = newButtons; 1691 } 1692 1693 if (releasedOnce) 1694 _UpdateStateClose(item, location, buttons); 1695 } 1696 } 1697 1698 if (action != NULL) 1699 *action = fState; 1700 1701 if (fSelected != NULL && LockLooper()) { 1702 _SelectItem(NULL); 1703 UnlockLooper(); 1704 } 1705 1706 // delete the menu window recycled for all the child menus 1707 _DeleteMenuWindow(); 1708 1709 return item; 1710 } 1711 1712 1713 void 1714 BMenu::_UpdateNavigationArea(BPoint position, BRect& navAreaRectAbove, 1715 BRect& navAreaRectBelow) 1716 { 1717 #define NAV_AREA_THRESHOLD 8 1718 1719 // The navigation area is a region in which mouse-overs won't select 1720 // the item under the cursor. This makes it easier to navigate to 1721 // submenus, as the cursor can be moved to submenu items directly instead 1722 // of having to move it horizontally into the submenu first. The concept 1723 // is illustrated below: 1724 // 1725 // +-------+----+---------+ 1726 // | | /| | 1727 // | | /*| | 1728 // |[2]--> | /**| | 1729 // | |/[4]| | 1730 // |------------| | 1731 // | [1] | [6] | 1732 // |------------| | 1733 // | |\[5]| | 1734 // |[3]--> | \**| | 1735 // | | \*| | 1736 // | | \| | 1737 // | +----|---------+ 1738 // | | 1739 // +------------+ 1740 // 1741 // [1] Selected item, cursor position ('position') 1742 // [2] Upper navigation area rectangle ('navAreaRectAbove') 1743 // [3] Lower navigation area rectangle ('navAreaRectBelow') 1744 // [4] Upper navigation area 1745 // [5] Lower navigation area 1746 // [6] Submenu 1747 // 1748 // The rectangles are used to calculate if the cursor is in the actual 1749 // navigation area (see _UpdateStateOpenSelect()). 1750 1751 if (fSelected == NULL) 1752 return; 1753 1754 BMenu* submenu = fSelected->Submenu(); 1755 1756 if (submenu != NULL) { 1757 BRect menuBounds = ConvertToScreen(Bounds()); 1758 1759 fSelected->Submenu()->LockLooper(); 1760 BRect submenuBounds = fSelected->Submenu()->ConvertToScreen( 1761 fSelected->Submenu()->Bounds()); 1762 fSelected->Submenu()->UnlockLooper(); 1763 1764 if (menuBounds.left < submenuBounds.left) { 1765 navAreaRectAbove.Set(position.x + NAV_AREA_THRESHOLD, 1766 submenuBounds.top, menuBounds.right, 1767 position.y); 1768 navAreaRectBelow.Set(position.x + NAV_AREA_THRESHOLD, 1769 position.y, menuBounds.right, 1770 submenuBounds.bottom); 1771 } else { 1772 navAreaRectAbove.Set(menuBounds.left, 1773 submenuBounds.top, position.x - NAV_AREA_THRESHOLD, 1774 position.y); 1775 navAreaRectBelow.Set(menuBounds.left, 1776 position.y, position.x - NAV_AREA_THRESHOLD, 1777 submenuBounds.bottom); 1778 } 1779 } else { 1780 navAreaRectAbove = BRect(); 1781 navAreaRectBelow = BRect(); 1782 } 1783 } 1784 1785 1786 void 1787 BMenu::_UpdateStateOpenSelect(BMenuItem* item, BPoint position, 1788 BRect& navAreaRectAbove, BRect& navAreaRectBelow, bigtime_t& selectedTime, 1789 bigtime_t& navigationAreaTime) 1790 { 1791 if (fState == MENU_STATE_CLOSED) 1792 return; 1793 1794 if (item != fSelected) { 1795 if (navigationAreaTime == 0) 1796 navigationAreaTime = system_time(); 1797 1798 position = ConvertToScreen(position); 1799 1800 bool inNavAreaRectAbove = navAreaRectAbove.Contains(position); 1801 bool inNavAreaRectBelow = navAreaRectBelow.Contains(position); 1802 1803 if (!inNavAreaRectAbove && !inNavAreaRectBelow) { 1804 _SelectItem(item, false); 1805 navAreaRectAbove = BRect(); 1806 navAreaRectBelow = BRect(); 1807 selectedTime = system_time(); 1808 navigationAreaTime = 0; 1809 return; 1810 } 1811 1812 BRect menuBounds = ConvertToScreen(Bounds()); 1813 1814 fSelected->Submenu()->LockLooper(); 1815 BRect submenuBounds = fSelected->Submenu()->ConvertToScreen( 1816 fSelected->Submenu()->Bounds()); 1817 fSelected->Submenu()->UnlockLooper(); 1818 1819 float xOffset; 1820 1821 // navAreaRectAbove and navAreaRectBelow have the same X 1822 // position and width, so it doesn't matter which one we use to 1823 // calculate the X offset 1824 if (menuBounds.left < submenuBounds.left) 1825 xOffset = position.x - navAreaRectAbove.left; 1826 else 1827 xOffset = navAreaRectAbove.right - position.x; 1828 1829 bool inNavArea; 1830 1831 if (inNavAreaRectAbove) { 1832 float yOffset = navAreaRectAbove.bottom - position.y; 1833 float ratio = navAreaRectAbove.Width() / navAreaRectAbove.Height(); 1834 1835 inNavArea = yOffset <= xOffset / ratio; 1836 } else { 1837 float yOffset = navAreaRectBelow.bottom - position.y; 1838 float ratio = navAreaRectBelow.Width() / navAreaRectBelow.Height(); 1839 1840 inNavArea = yOffset >= (navAreaRectBelow.Height() - xOffset 1841 / ratio); 1842 } 1843 1844 bigtime_t systime = system_time(); 1845 1846 if (!inNavArea || (navigationAreaTime > 0 && systime - 1847 navigationAreaTime > kNavigationAreaTimeout)) { 1848 // Don't delay opening of submenu if the user had 1849 // to wait for the navigation area timeout anyway 1850 _SelectItem(item, inNavArea); 1851 1852 if (inNavArea) { 1853 _UpdateNavigationArea(position, navAreaRectAbove, 1854 navAreaRectBelow); 1855 } else { 1856 navAreaRectAbove = BRect(); 1857 navAreaRectBelow = BRect(); 1858 } 1859 1860 selectedTime = system_time(); 1861 navigationAreaTime = 0; 1862 } 1863 } else if (fSelected->Submenu() != NULL && 1864 system_time() - selectedTime > kOpenSubmenuDelay) { 1865 _SelectItem(fSelected, true); 1866 1867 if (!navAreaRectAbove.IsValid() && !navAreaRectBelow.IsValid()) { 1868 position = ConvertToScreen(position); 1869 _UpdateNavigationArea(position, navAreaRectAbove, 1870 navAreaRectBelow); 1871 } 1872 } 1873 1874 if (fState != MENU_STATE_TRACKING) 1875 fState = MENU_STATE_TRACKING; 1876 } 1877 1878 1879 void 1880 BMenu::_UpdateStateClose(BMenuItem* item, const BPoint& where, 1881 const uint32& buttons) 1882 { 1883 if (fState == MENU_STATE_CLOSED) 1884 return; 1885 1886 if (buttons != 0 && _IsStickyMode()) { 1887 if (item == NULL) { 1888 if (item != fSelected) { 1889 LockLooper(); 1890 _SelectItem(item, false); 1891 UnlockLooper(); 1892 } 1893 fState = MENU_STATE_CLOSED; 1894 } else 1895 _SetStickyMode(false); 1896 } else if (buttons == 0 && !_IsStickyMode()) { 1897 if (fExtraRect != NULL && fExtraRect->Contains(where)) { 1898 _SetStickyMode(true); 1899 fExtraRect = NULL; 1900 // Setting this to NULL will prevent this code 1901 // to be executed next time 1902 } else { 1903 if (item != fSelected) { 1904 LockLooper(); 1905 _SelectItem(item, false); 1906 UnlockLooper(); 1907 } 1908 fState = MENU_STATE_CLOSED; 1909 } 1910 } 1911 } 1912 1913 1914 bool 1915 BMenu::_AddItem(BMenuItem* item, int32 index) 1916 { 1917 ASSERT(item != NULL); 1918 if (index < 0 || index > fItems.CountItems()) 1919 return false; 1920 1921 if (item->IsMarked()) 1922 _ItemMarked(item); 1923 1924 if (!fItems.AddItem(item, index)) 1925 return false; 1926 1927 // install the item on the supermenu's window 1928 // or onto our window, if we are a root menu 1929 BWindow* window = NULL; 1930 if (Superitem() != NULL) 1931 window = Superitem()->fWindow; 1932 else 1933 window = Window(); 1934 if (window != NULL) 1935 item->Install(window); 1936 1937 item->SetSuper(this); 1938 return true; 1939 } 1940 1941 1942 bool 1943 BMenu::_RemoveItems(int32 index, int32 count, BMenuItem* item, 1944 bool deleteItems) 1945 { 1946 bool success = false; 1947 bool invalidateLayout = false; 1948 1949 bool locked = LockLooper(); 1950 BWindow* window = Window(); 1951 1952 // The plan is simple: If we're given a BMenuItem directly, we use it 1953 // and ignore index and count. Otherwise, we use them instead. 1954 if (item != NULL) { 1955 if (fItems.RemoveItem(item)) { 1956 if (item == fSelected && window != NULL) 1957 _SelectItem(NULL); 1958 item->Uninstall(); 1959 item->SetSuper(NULL); 1960 if (deleteItems) 1961 delete item; 1962 success = invalidateLayout = true; 1963 } 1964 } else { 1965 // We iterate backwards because it's simpler 1966 int32 i = min_c(index + count - 1, fItems.CountItems() - 1); 1967 // NOTE: the range check for "index" is done after 1968 // calculating the last index to be removed, so 1969 // that the range is not "shifted" unintentionally 1970 index = max_c(0, index); 1971 for (; i >= index; i--) { 1972 item = static_cast<BMenuItem*>(fItems.ItemAt(i)); 1973 if (item != NULL) { 1974 if (fItems.RemoveItem(item)) { 1975 if (item == fSelected && window != NULL) 1976 _SelectItem(NULL); 1977 item->Uninstall(); 1978 item->SetSuper(NULL); 1979 if (deleteItems) 1980 delete item; 1981 success = true; 1982 invalidateLayout = true; 1983 } else { 1984 // operation not entirely successful 1985 success = false; 1986 break; 1987 } 1988 } 1989 } 1990 } 1991 1992 if (invalidateLayout) { 1993 InvalidateLayout(); 1994 if (locked && window != NULL) { 1995 _LayoutItems(0); 1996 _UpdateWindowViewSize(false); 1997 Invalidate(); 1998 } 1999 } 2000 2001 if (locked) 2002 UnlockLooper(); 2003 2004 return success; 2005 } 2006 2007 2008 bool 2009 BMenu::_RelayoutIfNeeded() 2010 { 2011 if (!fUseCachedMenuLayout) { 2012 fUseCachedMenuLayout = true; 2013 _CacheFontInfo(); 2014 _LayoutItems(0); 2015 return true; 2016 } 2017 return false; 2018 } 2019 2020 2021 void 2022 BMenu::_LayoutItems(int32 index) 2023 { 2024 _CalcTriggers(); 2025 2026 float width, height; 2027 _ComputeLayout(index, fResizeToFit, true, &width, &height); 2028 2029 if (fResizeToFit) 2030 ResizeTo(width, height); 2031 } 2032 2033 2034 BSize 2035 BMenu::_ValidatePreferredSize() 2036 { 2037 if (!fLayoutData->preferred.IsWidthSet() || ResizingMode() 2038 != fLayoutData->lastResizingMode) { 2039 _ComputeLayout(0, true, false, NULL, NULL); 2040 ResetLayoutInvalidation(); 2041 } 2042 2043 return fLayoutData->preferred; 2044 } 2045 2046 2047 void 2048 BMenu::_ComputeLayout(int32 index, bool bestFit, bool moveItems, 2049 float* _width, float* _height) 2050 { 2051 // TODO: Take "bestFit", "moveItems", "index" into account, 2052 // Recalculate only the needed items, 2053 // not the whole layout every time 2054 2055 fLayoutData->lastResizingMode = ResizingMode(); 2056 2057 BRect frame; 2058 2059 switch (fLayout) { 2060 case B_ITEMS_IN_COLUMN: 2061 _ComputeColumnLayout(index, bestFit, moveItems, frame); 2062 break; 2063 2064 case B_ITEMS_IN_ROW: 2065 _ComputeRowLayout(index, bestFit, moveItems, frame); 2066 break; 2067 2068 case B_ITEMS_IN_MATRIX: 2069 _ComputeMatrixLayout(frame); 2070 break; 2071 2072 default: 2073 break; 2074 } 2075 2076 // change width depending on resize mode 2077 BSize size; 2078 if ((ResizingMode() & B_FOLLOW_LEFT_RIGHT) == B_FOLLOW_LEFT_RIGHT) { 2079 if (Parent()) 2080 size.width = Parent()->Frame().Width() + 1; 2081 else if (Window()) 2082 size.width = Window()->Frame().Width() + 1; 2083 else 2084 size.width = Bounds().Width(); 2085 } else 2086 size.width = frame.Width(); 2087 2088 size.height = frame.Height(); 2089 2090 if (_width) 2091 *_width = size.width; 2092 2093 if (_height) 2094 *_height = size.height; 2095 2096 if (bestFit) 2097 fLayoutData->preferred = size; 2098 2099 if (moveItems) 2100 fUseCachedMenuLayout = true; 2101 } 2102 2103 2104 void 2105 BMenu::_ComputeColumnLayout(int32 index, bool bestFit, bool moveItems, 2106 BRect& frame) 2107 { 2108 BFont font; 2109 GetFont(&font); 2110 bool command = false; 2111 bool control = false; 2112 bool shift = false; 2113 bool option = false; 2114 if (index > 0) 2115 frame = ItemAt(index - 1)->Frame(); 2116 else 2117 frame.Set(0, 0, 0, -1); 2118 2119 for (; index < fItems.CountItems(); index++) { 2120 BMenuItem* item = ItemAt(index); 2121 2122 float width, height; 2123 item->GetContentSize(&width, &height); 2124 2125 if (item->fModifiers && item->fShortcutChar) { 2126 width += font.Size(); 2127 if (item->fModifiers & B_COMMAND_KEY) 2128 command = true; 2129 if (item->fModifiers & B_CONTROL_KEY) 2130 control = true; 2131 if (item->fModifiers & B_SHIFT_KEY) 2132 shift = true; 2133 if (item->fModifiers & B_OPTION_KEY) 2134 option = true; 2135 } 2136 2137 item->fBounds.left = 0.0f; 2138 item->fBounds.top = frame.bottom + 1.0f; 2139 item->fBounds.bottom = item->fBounds.top + height + fPad.top 2140 + fPad.bottom; 2141 2142 if (item->fSubmenu != NULL) 2143 width += item->Frame().Height(); 2144 2145 frame.right = max_c(frame.right, width + fPad.left + fPad.right); 2146 frame.bottom = item->fBounds.bottom; 2147 } 2148 2149 if (command) 2150 frame.right += BPrivate::MenuPrivate::MenuItemCommand()->Bounds().Width() + 1; 2151 if (control) 2152 frame.right += BPrivate::MenuPrivate::MenuItemControl()->Bounds().Width() + 1; 2153 if (option) 2154 frame.right += BPrivate::MenuPrivate::MenuItemOption()->Bounds().Width() + 1; 2155 if (shift) 2156 frame.right += BPrivate::MenuPrivate::MenuItemShift()->Bounds().Width() + 1; 2157 2158 if (fMaxContentWidth > 0) 2159 frame.right = min_c(frame.right, fMaxContentWidth); 2160 2161 if (moveItems) { 2162 for (int32 i = 0; i < fItems.CountItems(); i++) 2163 ItemAt(i)->fBounds.right = frame.right; 2164 } 2165 2166 frame.top = 0; 2167 frame.right = ceilf(frame.right); 2168 } 2169 2170 2171 void 2172 BMenu::_ComputeRowLayout(int32 index, bool bestFit, bool moveItems, 2173 BRect& frame) 2174 { 2175 font_height fh; 2176 GetFontHeight(&fh); 2177 frame.Set(0.0f, 0.0f, 0.0f, ceilf(fh.ascent + fh.descent + fPad.top 2178 + fPad.bottom)); 2179 2180 for (int32 i = 0; i < fItems.CountItems(); i++) { 2181 BMenuItem* item = ItemAt(i); 2182 2183 float width, height; 2184 item->GetContentSize(&width, &height); 2185 2186 item->fBounds.left = frame.right; 2187 item->fBounds.top = 0.0f; 2188 item->fBounds.right = item->fBounds.left + width + fPad.left 2189 + fPad.right; 2190 2191 frame.right = item->Frame().right + 1.0f; 2192 frame.bottom = max_c(frame.bottom, height + fPad.top + fPad.bottom); 2193 } 2194 2195 if (moveItems) { 2196 for (int32 i = 0; i < fItems.CountItems(); i++) 2197 ItemAt(i)->fBounds.bottom = frame.bottom; 2198 } 2199 2200 if (bestFit) 2201 frame.right = ceilf(frame.right); 2202 else 2203 frame.right = Bounds().right; 2204 } 2205 2206 2207 void 2208 BMenu::_ComputeMatrixLayout(BRect &frame) 2209 { 2210 frame.Set(0, 0, 0, 0); 2211 for (int32 i = 0; i < CountItems(); i++) { 2212 BMenuItem* item = ItemAt(i); 2213 if (item != NULL) { 2214 frame.left = min_c(frame.left, item->Frame().left); 2215 frame.right = max_c(frame.right, item->Frame().right); 2216 frame.top = min_c(frame.top, item->Frame().top); 2217 frame.bottom = max_c(frame.bottom, item->Frame().bottom); 2218 } 2219 } 2220 } 2221 2222 2223 // Assumes the SuperMenu to be locked (due to calling ConvertToScreen()) 2224 BPoint 2225 BMenu::ScreenLocation() 2226 { 2227 BMenu* superMenu = Supermenu(); 2228 BMenuItem* superItem = Superitem(); 2229 2230 if (superMenu == NULL || superItem == NULL) { 2231 debugger("BMenu can't determine where to draw." 2232 "Override BMenu::ScreenLocation() to determine location."); 2233 } 2234 2235 BPoint point; 2236 if (superMenu->Layout() == B_ITEMS_IN_COLUMN) 2237 point = superItem->Frame().RightTop() + BPoint(1.0f, 1.0f); 2238 else 2239 point = superItem->Frame().LeftBottom() + BPoint(1.0f, 1.0f); 2240 2241 superMenu->ConvertToScreen(&point); 2242 2243 return point; 2244 } 2245 2246 2247 BRect 2248 BMenu::_CalcFrame(BPoint where, bool* scrollOn) 2249 { 2250 // TODO: Improve me 2251 BRect bounds = Bounds(); 2252 BRect frame = bounds.OffsetToCopy(where); 2253 2254 BScreen screen(Window()); 2255 BRect screenFrame = screen.Frame(); 2256 2257 BMenu* superMenu = Supermenu(); 2258 BMenuItem* superItem = Superitem(); 2259 2260 bool scroll = false; 2261 2262 // TODO: Horrible hack: 2263 // When added to a BMenuField, a BPopUpMenu is the child of 2264 // a _BMCMenuBar_ to "fake" the menu hierarchy 2265 if (superMenu == NULL || superItem == NULL 2266 || dynamic_cast<_BMCMenuBar_*>(superMenu) != NULL) { 2267 // just move the window on screen 2268 2269 if (frame.bottom > screenFrame.bottom) 2270 frame.OffsetBy(0, screenFrame.bottom - frame.bottom); 2271 else if (frame.top < screenFrame.top) 2272 frame.OffsetBy(0, -frame.top); 2273 2274 if (frame.right > screenFrame.right) 2275 frame.OffsetBy(screenFrame.right - frame.right, 0); 2276 else if (frame.left < screenFrame.left) 2277 frame.OffsetBy(-frame.left, 0); 2278 } else if (superMenu->Layout() == B_ITEMS_IN_COLUMN) { 2279 if (frame.right > screenFrame.right) 2280 frame.OffsetBy(-superItem->Frame().Width() - frame.Width() - 2, 0); 2281 2282 if (frame.left < 0) 2283 frame.OffsetBy(-frame.left + 6, 0); 2284 2285 if (frame.bottom > screenFrame.bottom) 2286 frame.OffsetBy(0, screenFrame.bottom - frame.bottom); 2287 } else { 2288 if (frame.bottom > screenFrame.bottom) { 2289 if (scrollOn != NULL && superMenu != NULL 2290 && dynamic_cast<BMenuBar*>(superMenu) != NULL 2291 && frame.top < (screenFrame.bottom - 80)) { 2292 scroll = true; 2293 } else { 2294 frame.OffsetBy(0, -superItem->Frame().Height() 2295 - frame.Height() - 3); 2296 } 2297 } 2298 2299 if (frame.right > screenFrame.right) 2300 frame.OffsetBy(screenFrame.right - frame.right, 0); 2301 } 2302 2303 if (!scroll) { 2304 // basically, if this returns false, it means 2305 // that the menu frame won't fit completely inside the screen 2306 // TODO: Scrolling will currently only work up/down, 2307 // not left/right 2308 scroll = screenFrame.Height() < frame.Height(); 2309 } 2310 2311 if (scrollOn != NULL) 2312 *scrollOn = scroll; 2313 2314 return frame; 2315 } 2316 2317 2318 void 2319 BMenu::_DrawItems(BRect updateRect) 2320 { 2321 int32 itemCount = fItems.CountItems(); 2322 for (int32 i = 0; i < itemCount; i++) { 2323 BMenuItem* item = ItemAt(i); 2324 if (item->Frame().Intersects(updateRect)) 2325 item->Draw(); 2326 } 2327 } 2328 2329 2330 int 2331 BMenu::_State(BMenuItem** item) const 2332 { 2333 if (fState == MENU_STATE_TRACKING || fState == MENU_STATE_CLOSED) 2334 return fState; 2335 2336 if (fSelected != NULL && fSelected->Submenu() != NULL) 2337 return fSelected->Submenu()->_State(item); 2338 2339 return fState; 2340 } 2341 2342 2343 void 2344 BMenu::_InvokeItem(BMenuItem* item, bool now) 2345 { 2346 if (!item->IsEnabled()) 2347 return; 2348 2349 // Do the "selected" animation 2350 // TODO: Doesn't work. This is supposed to highlight 2351 // and dehighlight the item, works on beos but not on haiku. 2352 if (!item->Submenu() && LockLooper()) { 2353 snooze(50000); 2354 item->Select(true); 2355 Window()->UpdateIfNeeded(); 2356 snooze(50000); 2357 item->Select(false); 2358 Window()->UpdateIfNeeded(); 2359 snooze(50000); 2360 item->Select(true); 2361 Window()->UpdateIfNeeded(); 2362 snooze(50000); 2363 item->Select(false); 2364 Window()->UpdateIfNeeded(); 2365 UnlockLooper(); 2366 } 2367 2368 // Lock the root menu window before calling BMenuItem::Invoke() 2369 BMenu* parent = this; 2370 BMenu* rootMenu = NULL; 2371 do { 2372 rootMenu = parent; 2373 parent = rootMenu->Supermenu(); 2374 } while (parent != NULL); 2375 2376 if (rootMenu->LockLooper()) { 2377 item->Invoke(); 2378 rootMenu->UnlockLooper(); 2379 } 2380 } 2381 2382 2383 bool 2384 BMenu::_OverSuper(BPoint location) 2385 { 2386 if (!Supermenu()) 2387 return false; 2388 2389 return fSuperbounds.Contains(location); 2390 } 2391 2392 2393 bool 2394 BMenu::_OverSubmenu(BMenuItem* item, BPoint loc) 2395 { 2396 if (item == NULL) 2397 return false; 2398 2399 BMenu* subMenu = item->Submenu(); 2400 if (subMenu == NULL || subMenu->Window() == NULL) 2401 return false; 2402 2403 // we assume that loc is in screen coords { 2404 if (subMenu->Window()->Frame().Contains(loc)) 2405 return true; 2406 2407 return subMenu->_OverSubmenu(subMenu->fSelected, loc); 2408 } 2409 2410 2411 BMenuWindow* 2412 BMenu::_MenuWindow() 2413 { 2414 #if USE_CACHED_MENUWINDOW 2415 if (fCachedMenuWindow == NULL) { 2416 char windowName[64]; 2417 snprintf(windowName, 64, "%s cached menu", Name()); 2418 fCachedMenuWindow = new (nothrow) BMenuWindow(windowName); 2419 } 2420 #endif 2421 return fCachedMenuWindow; 2422 } 2423 2424 2425 void 2426 BMenu::_DeleteMenuWindow() 2427 { 2428 if (fCachedMenuWindow != NULL) { 2429 fCachedMenuWindow->Lock(); 2430 fCachedMenuWindow->Quit(); 2431 fCachedMenuWindow = NULL; 2432 } 2433 } 2434 2435 2436 BMenuItem* 2437 BMenu::_HitTestItems(BPoint where, BPoint slop) const 2438 { 2439 // TODO: Take "slop" into account ? 2440 2441 // if the point doesn't lie within the menu's 2442 // bounds, bail out immediately 2443 if (!Bounds().Contains(where)) 2444 return NULL; 2445 2446 int32 itemCount = CountItems(); 2447 for (int32 i = 0; i < itemCount; i++) { 2448 BMenuItem* item = ItemAt(i); 2449 if (item->IsEnabled() && item->Frame().Contains(where)) 2450 return item; 2451 } 2452 2453 return NULL; 2454 } 2455 2456 2457 BRect 2458 BMenu::_Superbounds() const 2459 { 2460 return fSuperbounds; 2461 } 2462 2463 2464 void 2465 BMenu::_CacheFontInfo() 2466 { 2467 font_height fh; 2468 GetFontHeight(&fh); 2469 fAscent = fh.ascent; 2470 fDescent = fh.descent; 2471 fFontHeight = ceilf(fh.ascent + fh.descent + fh.leading); 2472 } 2473 2474 2475 void 2476 BMenu::_ItemMarked(BMenuItem* item) 2477 { 2478 if (IsRadioMode()) { 2479 for (int32 i = 0; i < CountItems(); i++) { 2480 if (ItemAt(i) != item) 2481 ItemAt(i)->SetMarked(false); 2482 } 2483 InvalidateLayout(); 2484 } 2485 2486 if (IsLabelFromMarked() && Superitem()) 2487 Superitem()->SetLabel(item->Label()); 2488 } 2489 2490 2491 void 2492 BMenu::_Install(BWindow* target) 2493 { 2494 for (int32 i = 0; i < CountItems(); i++) 2495 ItemAt(i)->Install(target); 2496 } 2497 2498 2499 void 2500 BMenu::_Uninstall() 2501 { 2502 for (int32 i = 0; i < CountItems(); i++) 2503 ItemAt(i)->Uninstall(); 2504 } 2505 2506 2507 void 2508 BMenu::_SelectItem(BMenuItem* menuItem, bool showSubmenu, bool selectFirstItem) 2509 { 2510 // Avoid deselecting and then reselecting the same item 2511 // which would cause flickering 2512 if (menuItem != fSelected) { 2513 if (fSelected != NULL) { 2514 fSelected->Select(false); 2515 BMenu* subMenu = fSelected->Submenu(); 2516 if (subMenu != NULL && subMenu->Window() != NULL) 2517 subMenu->_Hide(); 2518 } 2519 2520 fSelected = menuItem; 2521 if (fSelected != NULL) 2522 fSelected->Select(true); 2523 } 2524 2525 if (fSelected != NULL && showSubmenu) { 2526 BMenu* subMenu = fSelected->Submenu(); 2527 if (subMenu != NULL && subMenu->Window() == NULL) { 2528 if (!subMenu->_Show(selectFirstItem)) { 2529 // something went wrong, deselect the item 2530 fSelected->Select(false); 2531 fSelected = NULL; 2532 } 2533 } 2534 } 2535 } 2536 2537 2538 bool 2539 BMenu::_SelectNextItem(BMenuItem* item, bool forward) 2540 { 2541 if (CountItems() == 0) // cannot select next item in an empty menu 2542 return false; 2543 2544 BMenuItem* nextItem = _NextItem(item, forward); 2545 if (nextItem == NULL) 2546 return false; 2547 2548 bool openMenu = false; 2549 if (dynamic_cast<BMenuBar*>(this) != NULL) 2550 openMenu = true; 2551 _SelectItem(nextItem, openMenu); 2552 return true; 2553 } 2554 2555 2556 BMenuItem* 2557 BMenu::_NextItem(BMenuItem* item, bool forward) const 2558 { 2559 // go to next item, and skip over disabled items such as separators 2560 int32 index = fItems.IndexOf(item); 2561 const int32 numItems = fItems.CountItems(); 2562 if (index < 0) { 2563 if (forward) 2564 index = -1; 2565 else 2566 index = numItems; 2567 } 2568 int32 startIndex = index; 2569 do { 2570 if (forward) 2571 index++; 2572 else 2573 index--; 2574 2575 // cycle through menu items 2576 if (index < 0) 2577 index = numItems - 1; 2578 else if (index >= numItems) 2579 index = 0; 2580 } while (!ItemAt(index)->IsEnabled() && index != startIndex); 2581 2582 if (index == startIndex) { 2583 // We are back where we started and no item was enabled. 2584 return NULL; 2585 } 2586 2587 return ItemAt(index); 2588 } 2589 2590 2591 void 2592 BMenu::_SetIgnoreHidden(bool on) 2593 { 2594 fIgnoreHidden = on; 2595 } 2596 2597 2598 void 2599 BMenu::_SetStickyMode(bool on) 2600 { 2601 if (fStickyMode == on) 2602 return; 2603 2604 fStickyMode = on; 2605 2606 // If we are switching to sticky mode, propagate the status 2607 // back to the super menu 2608 if (fSuper != NULL) 2609 fSuper->_SetStickyMode(on); 2610 else { 2611 // TODO: Ugly hack, but it needs to be done right here in this method 2612 BMenuBar* menuBar = dynamic_cast<BMenuBar*>(this); 2613 if (on && menuBar != NULL && menuBar->LockLooper()) { 2614 // Steal the focus from the current focus view 2615 // (needed to handle keyboard navigation) 2616 menuBar->_StealFocus(); 2617 menuBar->UnlockLooper(); 2618 } 2619 } 2620 } 2621 2622 2623 bool 2624 BMenu::_IsStickyMode() const 2625 { 2626 return fStickyMode; 2627 } 2628 2629 2630 void 2631 BMenu::_CalcTriggers() 2632 { 2633 BPrivate::TriggerList triggerList; 2634 2635 // Gathers the existing triggers set by the user 2636 for (int32 i = 0; i < CountItems(); i++) { 2637 char trigger = ItemAt(i)->Trigger(); 2638 if (trigger != 0) 2639 triggerList.AddTrigger(trigger); 2640 } 2641 2642 // Set triggers for items which don't have one yet 2643 for (int32 i = 0; i < CountItems(); i++) { 2644 BMenuItem* item = ItemAt(i); 2645 if (item->Trigger() == 0) { 2646 uint32 trigger; 2647 int32 index; 2648 if (_ChooseTrigger(item->Label(), index, trigger, triggerList)) 2649 item->SetAutomaticTrigger(index, trigger); 2650 } 2651 } 2652 } 2653 2654 2655 bool 2656 BMenu::_ChooseTrigger(const char* title, int32& index, uint32& trigger, 2657 BPrivate::TriggerList& triggers) 2658 { 2659 if (title == NULL) 2660 return false; 2661 2662 uint32 c; 2663 2664 // two runs: first we look out for uppercase letters 2665 // TODO: support Unicode characters correctly! 2666 for (uint32 i = 0; (c = title[i]) != '\0'; i++) { 2667 if (!IsInsideGlyph(c) && isupper(c) && !triggers.HasTrigger(c)) { 2668 index = i; 2669 trigger = tolower(c); 2670 return triggers.AddTrigger(c); 2671 } 2672 } 2673 2674 // then, if we still haven't found anything, we accept them all 2675 index = 0; 2676 while ((c = UTF8ToCharCode(&title)) != 0) { 2677 if (!isspace(c) && !triggers.HasTrigger(c)) { 2678 trigger = tolower(c); 2679 return triggers.AddTrigger(c); 2680 } 2681 2682 index++; 2683 } 2684 2685 return false; 2686 } 2687 2688 2689 void 2690 BMenu::_UpdateWindowViewSize(const bool &move) 2691 { 2692 BMenuWindow* window = static_cast<BMenuWindow*>(Window()); 2693 if (window == NULL) 2694 return; 2695 2696 if (dynamic_cast<BMenuBar*>(this) != NULL) 2697 return; 2698 2699 if (!fResizeToFit) 2700 return; 2701 2702 bool scroll = false; 2703 const BPoint screenLocation = move ? ScreenLocation() : window->Frame().LeftTop(); 2704 BRect frame = _CalcFrame(screenLocation, &scroll); 2705 ResizeTo(frame.Width(), frame.Height()); 2706 2707 if (fItems.CountItems() > 0) { 2708 if (!scroll) { 2709 window->ResizeTo(Bounds().Width(), Bounds().Height()); 2710 } else { 2711 BScreen screen(window); 2712 2713 // If we need scrolling, resize the window to fit the screen and 2714 // attach scrollers to our cached BMenuWindow. 2715 if (dynamic_cast<BMenuBar*>(Supermenu()) == NULL || frame.top < 0) { 2716 window->ResizeTo(Bounds().Width(), screen.Frame().Height()); 2717 frame.top = 0; 2718 } else { 2719 // Or, in case our parent was a BMenuBar enable scrolling with 2720 // normal size. 2721 window->ResizeTo(Bounds().Width(), 2722 screen.Frame().bottom - frame.top); 2723 } 2724 2725 window->AttachScrollers(); 2726 } 2727 } else { 2728 _CacheFontInfo(); 2729 window->ResizeTo(StringWidth(BPrivate::kEmptyMenuLabel) 2730 + fPad.left + fPad.right, 2731 fFontHeight + fPad.top + fPad.bottom); 2732 } 2733 2734 if (move) 2735 window->MoveTo(frame.LeftTop()); 2736 } 2737 2738 2739 bool 2740 BMenu::_OkToProceed(BMenuItem* item) 2741 { 2742 BPoint where; 2743 ulong buttons; 2744 GetMouse(&where, &buttons, false); 2745 bool stickyMode = _IsStickyMode(); 2746 // Quit if user clicks the mouse button in sticky mode 2747 // or releases the mouse button in nonsticky mode 2748 // or moves the pointer over another item 2749 // TODO: I added the check for BMenuBar to solve a problem with Deskbar. 2750 // BeOS seems to do something similar. This could also be a bug in 2751 // Deskbar, though. 2752 if ((buttons != 0 && stickyMode) 2753 || ((dynamic_cast<BMenuBar*>(this) == NULL 2754 && (buttons == 0 && !stickyMode)) || _HitTestItems(where) != item)) 2755 return false; 2756 2757 return true; 2758 } 2759 2760 2761 bool 2762 BMenu::_CustomTrackingWantsToQuit() 2763 { 2764 if (fExtraMenuData != NULL && fExtraMenuData->trackingHook != NULL 2765 && fExtraMenuData->trackingState != NULL) { 2766 return fExtraMenuData->trackingHook(this, 2767 fExtraMenuData->trackingState); 2768 } 2769 2770 return false; 2771 } 2772 2773 2774 void 2775 BMenu::_QuitTracking(bool onlyThis) 2776 { 2777 _SelectItem(NULL); 2778 if (BMenuBar* menuBar = dynamic_cast<BMenuBar*>(this)) 2779 menuBar->_RestoreFocus(); 2780 2781 fChosenItem = NULL; 2782 fState = MENU_STATE_CLOSED; 2783 2784 // Close the whole menu hierarchy 2785 if (!onlyThis && _IsStickyMode()) 2786 _SetStickyMode(false); 2787 2788 _Hide(); 2789 } 2790 2791 2792 // #pragma mark - 2793 2794 2795 // TODO: Maybe the following two methods would fit better into 2796 // InterfaceDefs.cpp 2797 // In R5, they do all the work client side, we let the app_server handle the 2798 // details. 2799 status_t 2800 set_menu_info(menu_info* info) 2801 { 2802 if (!info) 2803 return B_BAD_VALUE; 2804 2805 BPrivate::AppServerLink link; 2806 link.StartMessage(AS_SET_MENU_INFO); 2807 link.Attach<menu_info>(*info); 2808 2809 status_t status = B_ERROR; 2810 if (link.FlushWithReply(status) == B_OK && status == B_OK) 2811 BMenu::sMenuInfo = *info; 2812 // Update also the local copy, in case anyone relies on it 2813 2814 return status; 2815 } 2816 2817 2818 status_t 2819 get_menu_info(menu_info* info) 2820 { 2821 if (!info) 2822 return B_BAD_VALUE; 2823 2824 BPrivate::AppServerLink link; 2825 link.StartMessage(AS_GET_MENU_INFO); 2826 2827 status_t status = B_ERROR; 2828 if (link.FlushWithReply(status) == B_OK && status == B_OK) 2829 link.Read<menu_info>(info); 2830 2831 return status; 2832 } 2833