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