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