1 /* 2 * Copyright 2001-2015 Haiku, Inc. All rights reserved. 3 * Distributed under the terms of the MIT License. 4 * 5 * Authors: 6 * Stefano Ceccherini, stefano.ceccherini@gmail.com 7 * Marc Flerackers, mflerackers@androme.be 8 * Bill Hayden, haydentech@users.sourceforge.net 9 * Olivier Milla 10 * John Scipione, jscipione@gmail.com 11 */ 12 13 14 #include <ctype.h> 15 #include <stdlib.h> 16 #include <string.h> 17 18 #include <algorithm> 19 20 #include <Bitmap.h> 21 #include <ControlLook.h> 22 #include <MenuItem.h> 23 #include <Shape.h> 24 #include <String.h> 25 #include <Window.h> 26 27 #include <MenuPrivate.h> 28 29 #include "utf8_functions.h" 30 31 32 static const float kMarkTint = 0.75f; 33 34 // map control key shortcuts to drawable Unicode characters 35 // cf. http://unicode.org/charts/PDF/U2190.pdf 36 const char* kUTF8ControlMap[] = { 37 NULL, 38 "\xe2\x86\xb8", /* B_HOME U+21B8 */ 39 NULL, NULL, 40 NULL, /* B_END */ 41 NULL, /* B_INSERT */ 42 NULL, NULL, 43 NULL, /* B_BACKSPACE */ 44 "\xe2\x86\xb9", /* B_TAB U+21B9 */ 45 "\xe2\x8f\x8e", /* B_ENTER, U+23CE */ 46 NULL, /* B_PAGE_UP */ 47 NULL, /* B_PAGE_DOWN */ 48 NULL, NULL, NULL, 49 NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 50 NULL, NULL, NULL, NULL, 51 "\xe2\x86\x90", /* B_LEFT_ARROW */ 52 "\xe2\x86\x92", /* B_RIGHT_ARROW */ 53 "\xe2\x86\x91", /* B_UP_ARROW */ 54 "\xe2\x86\x93", /* B_DOWN_ARROW */ 55 "\xe2\x90\xA3" /* B_SPACE */ 56 }; 57 58 59 using BPrivate::MenuPrivate; 60 61 BMenuItem::BMenuItem(const char* label, BMessage* message, char shortcut, 62 uint32 modifiers) 63 { 64 _InitData(); 65 if (label != NULL) 66 fLabel = strdup(label); 67 68 SetMessage(message); 69 70 fShortcutChar = shortcut; 71 72 if (shortcut != 0) 73 fModifiers = modifiers | B_COMMAND_KEY; 74 else 75 fModifiers = 0; 76 } 77 78 79 BMenuItem::BMenuItem(BMenu* menu, BMessage* message) 80 { 81 _InitData(); 82 SetMessage(message); 83 _InitMenuData(menu); 84 } 85 86 87 BMenuItem::BMenuItem(BMessage* data) 88 { 89 _InitData(); 90 91 if (data->HasString("_label")) { 92 const char* string; 93 94 data->FindString("_label", &string); 95 SetLabel(string); 96 } 97 98 bool disable; 99 if (data->FindBool("_disable", &disable) == B_OK) 100 SetEnabled(!disable); 101 102 bool marked; 103 if (data->FindBool("_marked", &marked) == B_OK) 104 SetMarked(marked); 105 106 int32 userTrigger; 107 if (data->FindInt32("_user_trig", &userTrigger) == B_OK) 108 SetTrigger(userTrigger); 109 110 if (data->HasInt32("_shortcut")) { 111 int32 shortcut, mods; 112 113 data->FindInt32("_shortcut", &shortcut); 114 data->FindInt32("_mods", &mods); 115 116 SetShortcut(shortcut, mods); 117 } 118 119 if (data->HasMessage("_msg")) { 120 BMessage* message = new BMessage; 121 data->FindMessage("_msg", message); 122 SetMessage(message); 123 } 124 125 BMessage subMessage; 126 if (data->FindMessage("_submenu", &subMessage) == B_OK) { 127 BArchivable* object = instantiate_object(&subMessage); 128 if (object != NULL) { 129 BMenu* menu = dynamic_cast<BMenu*>(object); 130 if (menu != NULL) 131 _InitMenuData(menu); 132 } 133 } 134 } 135 136 137 BArchivable* 138 BMenuItem::Instantiate(BMessage* data) 139 { 140 if (validate_instantiation(data, "BMenuItem")) 141 return new BMenuItem(data); 142 143 return NULL; 144 } 145 146 147 status_t 148 BMenuItem::Archive(BMessage* data, bool deep) const 149 { 150 status_t status = BArchivable::Archive(data, deep); 151 152 if (status == B_OK && fLabel) 153 status = data->AddString("_label", Label()); 154 155 if (status == B_OK && !IsEnabled()) 156 status = data->AddBool("_disable", true); 157 158 if (status == B_OK && IsMarked()) 159 status = data->AddBool("_marked", true); 160 161 if (status == B_OK && fUserTrigger) 162 status = data->AddInt32("_user_trig", fUserTrigger); 163 164 if (status == B_OK && fShortcutChar) { 165 status = data->AddInt32("_shortcut", fShortcutChar); 166 if (status == B_OK) 167 status = data->AddInt32("_mods", fModifiers); 168 } 169 170 if (status == B_OK && Message() != NULL) 171 status = data->AddMessage("_msg", Message()); 172 173 if (status == B_OK && deep && fSubmenu) { 174 BMessage submenu; 175 if (fSubmenu->Archive(&submenu, true) == B_OK) 176 status = data->AddMessage("_submenu", &submenu); 177 } 178 179 return status; 180 } 181 182 183 BMenuItem::~BMenuItem() 184 { 185 if (fSuper != NULL) 186 fSuper->RemoveItem(this); 187 188 free(fLabel); 189 delete fSubmenu; 190 } 191 192 193 void 194 BMenuItem::SetLabel(const char* string) 195 { 196 if (fLabel != NULL) { 197 free(fLabel); 198 fLabel = NULL; 199 } 200 201 if (string != NULL) 202 fLabel = strdup(string); 203 204 if (fSuper != NULL) { 205 fSuper->InvalidateLayout(); 206 207 if (fSuper->LockLooper()) { 208 fSuper->Invalidate(); 209 fSuper->UnlockLooper(); 210 } 211 } 212 } 213 214 215 void 216 BMenuItem::SetEnabled(bool enable) 217 { 218 if (fEnabled == enable) 219 return; 220 221 fEnabled = enable; 222 223 if (fSubmenu != NULL) 224 fSubmenu->SetEnabled(enable); 225 226 BMenu* menu = fSuper; 227 if (menu != NULL && menu->LockLooper()) { 228 menu->Invalidate(fBounds); 229 menu->UnlockLooper(); 230 } 231 } 232 233 234 void 235 BMenuItem::SetMarked(bool mark) 236 { 237 fMark = mark; 238 239 if (mark && fSuper != NULL) { 240 MenuPrivate priv(fSuper); 241 priv.ItemMarked(this); 242 } 243 } 244 245 246 void 247 BMenuItem::SetTrigger(char trigger) 248 { 249 fUserTrigger = trigger; 250 251 // try uppercase letters first 252 253 const char* pos = strchr(Label(), toupper(trigger)); 254 trigger = tolower(trigger); 255 256 if (pos == NULL) { 257 // take lowercase, too 258 pos = strchr(Label(), trigger); 259 } 260 261 if (pos != NULL) { 262 fTriggerIndex = UTF8CountChars(Label(), pos - Label()); 263 fTrigger = trigger; 264 } else { 265 fTrigger = 0; 266 fTriggerIndex = -1; 267 } 268 269 if (fSuper != NULL) 270 fSuper->InvalidateLayout(); 271 } 272 273 274 void 275 BMenuItem::SetShortcut(char shortcut, uint32 modifiers) 276 { 277 if (fShortcutChar != 0 && (fModifiers & B_COMMAND_KEY) != 0 278 && fWindow != NULL) { 279 fWindow->RemoveShortcut(fShortcutChar, fModifiers); 280 } 281 282 fShortcutChar = shortcut; 283 284 if (shortcut != 0) 285 fModifiers = modifiers | B_COMMAND_KEY; 286 else 287 fModifiers = 0; 288 289 if (fShortcutChar != 0 && (fModifiers & B_COMMAND_KEY) && fWindow) 290 fWindow->AddShortcut(fShortcutChar, fModifiers, this); 291 292 if (fSuper != NULL) { 293 fSuper->InvalidateLayout(); 294 295 if (fSuper->LockLooper()) { 296 fSuper->Invalidate(); 297 fSuper->UnlockLooper(); 298 } 299 } 300 } 301 302 303 const char* 304 BMenuItem::Label() const 305 { 306 return fLabel; 307 } 308 309 310 bool 311 BMenuItem::IsEnabled() const 312 { 313 if (fSubmenu) 314 return fSubmenu->IsEnabled(); 315 316 if (!fEnabled) 317 return false; 318 319 return fSuper != NULL ? fSuper->IsEnabled() : true; 320 } 321 322 323 bool 324 BMenuItem::IsMarked() const 325 { 326 return fMark; 327 } 328 329 330 char 331 BMenuItem::Trigger() const 332 { 333 return fUserTrigger; 334 } 335 336 337 char 338 BMenuItem::Shortcut(uint32* modifiers) const 339 { 340 if (modifiers) 341 *modifiers = fModifiers; 342 343 return fShortcutChar; 344 } 345 346 347 BMenu* 348 BMenuItem::Submenu() const 349 { 350 return fSubmenu; 351 } 352 353 354 BMenu* 355 BMenuItem::Menu() const 356 { 357 return fSuper; 358 } 359 360 361 BRect 362 BMenuItem::Frame() const 363 { 364 return fBounds; 365 } 366 367 368 void 369 BMenuItem::GetContentSize(float* _width, float* _height) 370 { 371 // TODO: Get rid of this. BMenu should handle this 372 // automatically. Maybe it's not even needed, since our 373 // BFont::Height() caches the value locally 374 MenuPrivate(fSuper).CacheFontInfo(); 375 376 fCachedWidth = fSuper->StringWidth(fLabel); 377 378 if (_width) 379 *_width = (float)ceil(fCachedWidth); 380 if (_height) 381 *_height = MenuPrivate(fSuper).FontHeight(); 382 } 383 384 385 void 386 BMenuItem::TruncateLabel(float maxWidth, char* newLabel) 387 { 388 BFont font; 389 fSuper->GetFont(&font); 390 391 BString string(fLabel); 392 393 font.TruncateString(&string, B_TRUNCATE_MIDDLE, maxWidth); 394 395 string.CopyInto(newLabel, 0, string.Length()); 396 newLabel[string.Length()] = '\0'; 397 } 398 399 400 void 401 BMenuItem::DrawContent() 402 { 403 MenuPrivate menuPrivate(fSuper); 404 menuPrivate.CacheFontInfo(); 405 406 fSuper->MovePenBy(0, menuPrivate.Ascent()); 407 BPoint lineStart = fSuper->PenLocation(); 408 409 fSuper->SetDrawingMode(B_OP_OVER); 410 411 float labelWidth; 412 float labelHeight; 413 GetContentSize(&labelWidth, &labelHeight); 414 415 const BRect& padding = menuPrivate.Padding(); 416 float maxContentWidth = fSuper->MaxContentWidth(); 417 float frameWidth = maxContentWidth > 0 ? maxContentWidth 418 : fSuper->Frame().Width() - padding.left - padding.right; 419 420 if (roundf(frameWidth) >= roundf(labelWidth)) 421 fSuper->DrawString(fLabel); 422 else { 423 // truncate label to fit 424 char* truncatedLabel = new char[strlen(fLabel) + 4]; 425 TruncateLabel(frameWidth, truncatedLabel); 426 fSuper->DrawString(truncatedLabel); 427 delete[] truncatedLabel; 428 } 429 430 if (fSuper->AreTriggersEnabled() && fTriggerIndex != -1) { 431 float escapements[fTriggerIndex + 1]; 432 BFont font; 433 fSuper->GetFont(&font); 434 435 font.GetEscapements(fLabel, fTriggerIndex + 1, escapements); 436 437 for (int32 i = 0; i < fTriggerIndex; i++) 438 lineStart.x += escapements[i] * font.Size(); 439 440 lineStart.x--; 441 lineStart.y++; 442 443 BPoint lineEnd(lineStart); 444 lineEnd.x += escapements[fTriggerIndex] * font.Size(); 445 446 fSuper->StrokeLine(lineStart, lineEnd); 447 } 448 } 449 450 451 void 452 BMenuItem::Draw() 453 { 454 const color_which lowColor = fSuper->LowUIColor(); 455 const color_which highColor = fSuper->HighUIColor(); 456 457 fSuper->SetLowColor(_LowColor()); 458 fSuper->SetHighColor(_HighColor()); 459 460 if (_IsActivated()) { 461 // fill in the background 462 BRect frame(Frame()); 463 be_control_look->DrawMenuItemBackground(fSuper, frame, frame, 464 fSuper->LowColor(), BControlLook::B_ACTIVATED); 465 } 466 467 // draw content 468 fSuper->MovePenTo(ContentLocation()); 469 DrawContent(); 470 471 // draw extra symbols 472 MenuPrivate privateAccessor(fSuper); 473 const menu_layout layout = privateAccessor.Layout(); 474 if (layout == B_ITEMS_IN_COLUMN) { 475 if (IsMarked()) 476 _DrawMarkSymbol(); 477 478 if (fShortcutChar) 479 _DrawShortcutSymbol(privateAccessor.HasSubmenus()); 480 481 if (Submenu() != NULL) 482 _DrawSubmenuSymbol(); 483 } 484 485 // restore the parent menu's low color and high color 486 fSuper->SetLowUIColor(lowColor); 487 fSuper->SetHighUIColor(highColor); 488 } 489 490 491 void 492 BMenuItem::Highlight(bool highlight) 493 { 494 fSuper->Invalidate(Frame()); 495 } 496 497 498 bool 499 BMenuItem::IsSelected() const 500 { 501 return fSelected; 502 } 503 504 505 BPoint 506 BMenuItem::ContentLocation() const 507 { 508 const BRect& padding = MenuPrivate(fSuper).Padding(); 509 510 return BPoint(fBounds.left + padding.left, fBounds.top + padding.top); 511 } 512 513 514 void BMenuItem::_ReservedMenuItem1() {} 515 void BMenuItem::_ReservedMenuItem2() {} 516 void BMenuItem::_ReservedMenuItem3() {} 517 void BMenuItem::_ReservedMenuItem4() {} 518 519 520 BMenuItem::BMenuItem(const BMenuItem &) 521 { 522 } 523 524 525 BMenuItem& 526 BMenuItem::operator=(const BMenuItem &) 527 { 528 return *this; 529 } 530 531 532 void 533 BMenuItem::_InitData() 534 { 535 fLabel = NULL; 536 fSubmenu = NULL; 537 fWindow = NULL; 538 fSuper = NULL; 539 fModifiers = 0; 540 fCachedWidth = 0; 541 fTriggerIndex = -1; 542 fUserTrigger = 0; 543 fTrigger = 0; 544 fShortcutChar = 0; 545 fMark = false; 546 fEnabled = true; 547 fSelected = false; 548 } 549 550 551 void 552 BMenuItem::_InitMenuData(BMenu* menu) 553 { 554 fSubmenu = menu; 555 556 MenuPrivate(fSubmenu).SetSuperItem(this); 557 558 BMenuItem* item = menu->FindMarked(); 559 560 if (menu->IsRadioMode() && menu->IsLabelFromMarked() && item != NULL) 561 SetLabel(item->Label()); 562 else 563 SetLabel(menu->Name()); 564 } 565 566 567 void 568 BMenuItem::Install(BWindow* window) 569 { 570 if (fSubmenu != NULL) 571 MenuPrivate(fSubmenu).Install(window); 572 573 fWindow = window; 574 575 if (fShortcutChar != 0 && (fModifiers & B_COMMAND_KEY) && fWindow) 576 window->AddShortcut(fShortcutChar, fModifiers, this); 577 578 if (!Messenger().IsValid()) 579 SetTarget(window); 580 } 581 582 583 status_t 584 BMenuItem::Invoke(BMessage* message) 585 { 586 if (!IsEnabled()) 587 return B_ERROR; 588 589 if (fSuper->IsRadioMode()) 590 SetMarked(true); 591 592 bool notify = false; 593 uint32 kind = InvokeKind(¬ify); 594 595 BMessage clone(kind); 596 status_t err = B_BAD_VALUE; 597 598 if (message == NULL && !notify) 599 message = Message(); 600 601 if (message == NULL) { 602 if (!fSuper->IsWatched()) 603 return err; 604 } else 605 clone = *message; 606 607 clone.AddInt32("index", fSuper->IndexOf(this)); 608 clone.AddInt64("when", (int64)system_time()); 609 clone.AddPointer("source", this); 610 clone.AddMessenger("be:sender", BMessenger(fSuper)); 611 612 if (message != NULL) 613 err = BInvoker::Invoke(&clone); 614 615 // TODO: assynchronous messaging 616 // SendNotices(kind, &clone); 617 618 return err; 619 } 620 621 622 void 623 BMenuItem::Uninstall() 624 { 625 if (fSubmenu != NULL) 626 MenuPrivate(fSubmenu).Uninstall(); 627 628 if (Target() == fWindow) 629 SetTarget(BMessenger()); 630 631 if (fShortcutChar != 0 && (fModifiers & B_COMMAND_KEY) != 0 632 && fWindow != NULL) { 633 fWindow->RemoveShortcut(fShortcutChar, fModifiers); 634 } 635 636 fWindow = NULL; 637 } 638 639 640 void 641 BMenuItem::SetSuper(BMenu* super) 642 { 643 if (fSuper != NULL && super != NULL) { 644 debugger("Error - can't add menu or menu item to more than 1 container" 645 " (either menu or menubar)."); 646 } 647 648 if (fSubmenu != NULL) 649 MenuPrivate(fSubmenu).SetSuper(super); 650 651 fSuper = super; 652 } 653 654 655 void 656 BMenuItem::Select(bool selected) 657 { 658 if (fSelected == selected) 659 return; 660 661 if (Submenu() != NULL || IsEnabled()) { 662 fSelected = selected; 663 Highlight(selected); 664 } 665 } 666 667 668 bool 669 BMenuItem::_IsActivated() 670 { 671 return IsSelected() && (IsEnabled() || fSubmenu != NULL); 672 } 673 674 675 rgb_color 676 BMenuItem::_LowColor() 677 { 678 return _IsActivated() ? ui_color(B_MENU_SELECTED_BACKGROUND_COLOR) 679 : ui_color(B_MENU_BACKGROUND_COLOR); 680 } 681 682 683 rgb_color 684 BMenuItem::_HighColor() 685 { 686 rgb_color highColor; 687 688 bool isEnabled = IsEnabled(); 689 bool isSelected = IsSelected(); 690 691 if (isEnabled && isSelected) 692 highColor = ui_color(B_MENU_SELECTED_ITEM_TEXT_COLOR); 693 else if (isEnabled) 694 highColor = ui_color(B_MENU_ITEM_TEXT_COLOR); 695 else { 696 rgb_color bgColor = fSuper->LowColor(); 697 if (bgColor.red + bgColor.green + bgColor.blue > 128 * 3) 698 highColor = tint_color(bgColor, B_DISABLED_LABEL_TINT); 699 else 700 highColor = tint_color(bgColor, B_LIGHTEN_2_TINT); 701 } 702 703 return highColor; 704 } 705 706 707 void 708 BMenuItem::_DrawMarkSymbol() 709 { 710 fSuper->PushState(); 711 712 BRect r(fBounds); 713 float leftMargin; 714 MenuPrivate(fSuper).GetItemMargins(&leftMargin, NULL, NULL, NULL); 715 float gap = leftMargin / 4; 716 r.right = r.left + leftMargin - gap; 717 r.left += gap / 3; 718 719 BPoint center(floorf((r.left + r.right) / 2.0), 720 floorf((r.top + r.bottom) / 2.0)); 721 722 float size = std::min(r.Height() - 2, r.Width()); 723 r.top = floorf(center.y - size / 2 + 0.5); 724 r.bottom = floorf(center.y + size / 2 + 0.5); 725 r.left = floorf(center.x - size / 2 + 0.5); 726 r.right = floorf(center.x + size / 2 + 0.5); 727 728 BShape arrowShape; 729 center.x += 0.5; 730 center.y += 0.5; 731 size *= 0.3; 732 arrowShape.MoveTo(BPoint(center.x - size, center.y - size * 0.25)); 733 arrowShape.LineTo(BPoint(center.x - size * 0.25, center.y + size)); 734 arrowShape.LineTo(BPoint(center.x + size, center.y - size)); 735 736 fSuper->SetHighColor(tint_color(_HighColor(), kMarkTint)); 737 fSuper->SetDrawingMode(B_OP_OVER); 738 fSuper->SetPenSize(2.0); 739 // NOTE: StrokeShape() offsets the shape by the current pen position, 740 // it is not documented in the BeBook, but it is true! 741 fSuper->MovePenTo(B_ORIGIN); 742 fSuper->StrokeShape(&arrowShape); 743 744 fSuper->PopState(); 745 } 746 747 748 void 749 BMenuItem::_DrawShortcutSymbol(bool submenus) 750 { 751 BMenu* menu = fSuper; 752 BFont font; 753 menu->GetFont(&font); 754 BPoint where = ContentLocation(); 755 // Start from the right and walk our way back 756 where.x = fBounds.right - font.Size(); 757 758 // Leave space for the submenu arrow if any item in the menu has a submenu 759 if (submenus) 760 where.x -= fBounds.Height() / 2; 761 762 const float ascent = MenuPrivate(fSuper).Ascent(); 763 if (fShortcutChar <= B_SPACE && kUTF8ControlMap[(int)fShortcutChar]) 764 _DrawControlChar(fShortcutChar, where + BPoint(0, ascent)); 765 else 766 fSuper->DrawChar(fShortcutChar, where + BPoint(0, ascent)); 767 768 where.y += (fBounds.Height() - 11) / 2 - 1; 769 where.x -= 4; 770 771 // TODO: It would be nice to draw these taking into account the text (low) 772 // color. 773 if ((fModifiers & B_COMMAND_KEY) != 0) { 774 const BBitmap* command = MenuPrivate::MenuItemCommand(); 775 const BRect &rect = command->Bounds(); 776 where.x -= rect.Width() + 1; 777 fSuper->DrawBitmap(command, where); 778 } 779 780 if ((fModifiers & B_CONTROL_KEY) != 0) { 781 const BBitmap* control = MenuPrivate::MenuItemControl(); 782 const BRect &rect = control->Bounds(); 783 where.x -= rect.Width() + 1; 784 fSuper->DrawBitmap(control, where); 785 } 786 787 if ((fModifiers & B_OPTION_KEY) != 0) { 788 const BBitmap* option = MenuPrivate::MenuItemOption(); 789 const BRect &rect = option->Bounds(); 790 where.x -= rect.Width() + 1; 791 fSuper->DrawBitmap(option, where); 792 } 793 794 if ((fModifiers & B_SHIFT_KEY) != 0) { 795 const BBitmap* shift = MenuPrivate::MenuItemShift(); 796 const BRect &rect = shift->Bounds(); 797 where.x -= rect.Width() + 1; 798 fSuper->DrawBitmap(shift, where); 799 } 800 } 801 802 803 void 804 BMenuItem::_DrawSubmenuSymbol() 805 { 806 fSuper->PushState(); 807 808 float symbolSize = roundf(Frame().Height() * 2 / 3); 809 810 BRect rect(fBounds); 811 rect.left = rect.right - symbolSize; 812 813 // 14px by default, scaled with font size up to right margin - padding 814 BRect symbolRect(0, 0, symbolSize, symbolSize); 815 symbolRect.OffsetTo(BPoint(rect.left, 816 fBounds.top + (fBounds.Height() - symbolSize) / 2)); 817 818 be_control_look->DrawArrowShape(Menu(), symbolRect, symbolRect, 819 _HighColor(), BControlLook::B_RIGHT_ARROW, 0, kMarkTint); 820 821 fSuper->PopState(); 822 } 823 824 825 void 826 BMenuItem::_DrawControlChar(char shortcut, BPoint where) 827 { 828 // TODO: If needed, take another font for the control characters 829 // (or have font overlays in the app_server!) 830 const char* symbol = " "; 831 if (kUTF8ControlMap[(int)fShortcutChar]) 832 symbol = kUTF8ControlMap[(int)fShortcutChar]; 833 834 fSuper->DrawString(symbol, where); 835 } 836 837 838 void 839 BMenuItem::SetAutomaticTrigger(int32 index, uint32 trigger) 840 { 841 fTriggerIndex = index; 842 fTrigger = trigger; 843 } 844