1 /* 2 * Copyright 2001-2013 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 //! Display item for BMenu class 14 15 #include <ctype.h> 16 #include <stdlib.h> 17 #include <string.h> 18 19 #include <Bitmap.h> 20 #include <ControlLook.h> 21 #include <MenuItem.h> 22 #include <Shape.h> 23 #include <String.h> 24 #include <Window.h> 25 26 #include <MenuPrivate.h> 27 28 #include "utf8_functions.h" 29 30 31 const float kLightBGTint = (B_LIGHTEN_1_TINT + B_LIGHTEN_1_TINT + B_NO_TINT) / 3.0; 32 33 // map control key shortcuts to drawable Unicode characters 34 // cf. http://unicode.org/charts/PDF/U2190.pdf 35 const char *kUTF8ControlMap[] = { 36 NULL, 37 "\xe2\x86\xb8", /* B_HOME U+21B8 */ 38 NULL, NULL, 39 NULL, /* B_END */ 40 NULL, /* B_INSERT */ 41 NULL, NULL, 42 NULL, /* B_BACKSPACE */ 43 "\xe2\x86\xb9", /* B_TAB U+21B9 */ 44 "\xe2\x86\xb5", /* B_ENTER, U+21B5 */ 45 //"\xe2\x8f\x8e", /* B_ENTER, U+23CE it's the official one */ 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 using BPrivate::MenuPrivate; 58 59 BMenuItem::BMenuItem(const char* label, BMessage* message, char shortcut, 60 uint32 modifiers) 61 { 62 _InitData(); 63 if (label != NULL) 64 fLabel = strdup(label); 65 66 SetMessage(message); 67 68 fShortcutChar = shortcut; 69 70 if (shortcut != 0) 71 fModifiers = modifiers | B_COMMAND_KEY; 72 else 73 fModifiers = 0; 74 } 75 76 77 BMenuItem::BMenuItem(BMenu* menu, BMessage* message) 78 { 79 _InitData(); 80 SetMessage(message); 81 _InitMenuData(menu); 82 } 83 84 85 BMenuItem::BMenuItem(BMessage* data) 86 { 87 _InitData(); 88 89 if (data->HasString("_label")) { 90 const char *string; 91 92 data->FindString("_label", &string); 93 SetLabel(string); 94 } 95 96 bool disable; 97 if (data->FindBool("_disable", &disable) == B_OK) 98 SetEnabled(!disable); 99 100 bool marked; 101 if (data->FindBool("_marked", &marked) == B_OK) 102 SetMarked(marked); 103 104 int32 userTrigger; 105 if (data->FindInt32("_user_trig", &userTrigger) == B_OK) 106 SetTrigger(userTrigger); 107 108 if (data->HasInt32("_shortcut")) { 109 int32 shortcut, mods; 110 111 data->FindInt32("_shortcut", &shortcut); 112 data->FindInt32("_mods", &mods); 113 114 SetShortcut(shortcut, mods); 115 } 116 117 if (data->HasMessage("_msg")) { 118 BMessage *msg = new BMessage; 119 data->FindMessage("_msg", msg); 120 SetMessage(msg); 121 } 122 123 BMessage subMessage; 124 if (data->FindMessage("_submenu", &subMessage) == B_OK) { 125 BArchivable* object = instantiate_object(&subMessage); 126 if (object != NULL) { 127 BMenu* menu = dynamic_cast<BMenu *>(object); 128 if (menu != NULL) 129 _InitMenuData(menu); 130 } 131 } 132 } 133 134 135 BArchivable* 136 BMenuItem::Instantiate(BMessage* data) 137 { 138 if (validate_instantiation(data, "BMenuItem")) 139 return new BMenuItem(data); 140 141 return NULL; 142 } 143 144 145 status_t 146 BMenuItem::Archive(BMessage* data, bool deep) const 147 { 148 status_t ret = BArchivable::Archive(data, deep); 149 150 if (ret == B_OK && fLabel) 151 ret = data->AddString("_label", Label()); 152 153 if (ret == B_OK && !IsEnabled()) 154 ret = data->AddBool("_disable", true); 155 156 if (ret == B_OK && IsMarked()) 157 ret = data->AddBool("_marked", true); 158 159 if (ret == B_OK && fUserTrigger) 160 ret = data->AddInt32("_user_trig", fUserTrigger); 161 162 if (ret == B_OK && fShortcutChar) { 163 ret = data->AddInt32("_shortcut", fShortcutChar); 164 if (ret == B_OK) 165 ret = data->AddInt32("_mods", fModifiers); 166 } 167 168 if (ret == B_OK && Message()) 169 ret = data->AddMessage("_msg", Message()); 170 171 if (ret == B_OK && deep && fSubmenu) { 172 BMessage submenu; 173 if (fSubmenu->Archive(&submenu, true) == B_OK) 174 ret = data->AddMessage("_submenu", &submenu); 175 } 176 177 return ret; 178 } 179 180 181 BMenuItem::~BMenuItem() 182 { 183 free(fLabel); 184 delete fSubmenu; 185 } 186 187 188 void 189 BMenuItem::SetLabel(const char *string) 190 { 191 if (fLabel != NULL) { 192 free(fLabel); 193 fLabel = NULL; 194 } 195 196 if (string != NULL) 197 fLabel = strdup(string); 198 199 if (fSuper != NULL) { 200 fSuper->InvalidateLayout(); 201 202 if (fSuper->LockLooper()) { 203 fSuper->Invalidate(); 204 fSuper->UnlockLooper(); 205 } 206 } 207 } 208 209 210 void 211 BMenuItem::SetEnabled(bool state) 212 { 213 if (fEnabled == state) 214 return; 215 216 fEnabled = state; 217 218 if (fSubmenu != NULL) 219 fSubmenu->SetEnabled(state); 220 221 BMenu* menu = fSuper; 222 if (menu != NULL && menu->LockLooper()) { 223 menu->Invalidate(fBounds); 224 menu->UnlockLooper(); 225 } 226 } 227 228 229 void 230 BMenuItem::SetMarked(bool state) 231 { 232 fMark = state; 233 234 if (state && fSuper != NULL) { 235 MenuPrivate priv(fSuper); 236 priv.ItemMarked(this); 237 } 238 } 239 240 241 void 242 BMenuItem::SetTrigger(char trigger) 243 { 244 fUserTrigger = trigger; 245 246 // try uppercase letters first 247 248 const char* pos = strchr(Label(), toupper(trigger)); 249 trigger = tolower(trigger); 250 251 if (pos == NULL) { 252 // take lowercase, too 253 pos = strchr(Label(), trigger); 254 } 255 256 if (pos != NULL) { 257 fTriggerIndex = UTF8CountChars(Label(), pos - Label()); 258 fTrigger = trigger; 259 } else { 260 fTrigger = 0; 261 fTriggerIndex = -1; 262 } 263 264 if (fSuper != NULL) 265 fSuper->InvalidateLayout(); 266 } 267 268 269 void 270 BMenuItem::SetShortcut(char ch, uint32 modifiers) 271 { 272 if (fShortcutChar != 0 && (fModifiers & B_COMMAND_KEY) && fWindow) 273 fWindow->RemoveShortcut(fShortcutChar, fModifiers); 274 275 fShortcutChar = ch; 276 277 if (ch != 0) 278 fModifiers = modifiers | B_COMMAND_KEY; 279 else 280 fModifiers = 0; 281 282 if (fShortcutChar != 0 && (fModifiers & B_COMMAND_KEY) && fWindow) 283 fWindow->AddShortcut(fShortcutChar, fModifiers, this); 284 285 if (fSuper) { 286 fSuper->InvalidateLayout(); 287 288 if (fSuper->LockLooper()) { 289 fSuper->Invalidate(); 290 fSuper->UnlockLooper(); 291 } 292 } 293 } 294 295 296 const char* 297 BMenuItem::Label() const 298 { 299 return fLabel; 300 } 301 302 303 bool 304 BMenuItem::IsEnabled() const 305 { 306 if (fSubmenu) 307 return fSubmenu->IsEnabled(); 308 309 if (!fEnabled) 310 return false; 311 312 return fSuper != NULL ? fSuper->IsEnabled() : true; 313 } 314 315 316 bool 317 BMenuItem::IsMarked() const 318 { 319 return fMark; 320 } 321 322 323 char 324 BMenuItem::Trigger() const 325 { 326 return fUserTrigger; 327 } 328 329 330 char 331 BMenuItem::Shortcut(uint32* modifiers) const 332 { 333 if (modifiers) 334 *modifiers = fModifiers; 335 336 return fShortcutChar; 337 } 338 339 340 BMenu* 341 BMenuItem::Submenu() const 342 { 343 return fSubmenu; 344 } 345 346 347 BMenu* 348 BMenuItem::Menu() const 349 { 350 return fSuper; 351 } 352 353 354 BRect 355 BMenuItem::Frame() const 356 { 357 return fBounds; 358 } 359 360 361 void 362 BMenuItem::GetContentSize(float* width, float* height) 363 { 364 // TODO: Get rid of this. BMenu should handle this 365 // automatically. Maybe it's not even needed, since our 366 // BFont::Height() caches the value locally 367 MenuPrivate(fSuper).CacheFontInfo(); 368 369 fCachedWidth = fSuper->StringWidth(fLabel); 370 371 if (width) 372 *width = (float)ceil(fCachedWidth); 373 if (height) 374 *height = MenuPrivate(fSuper).FontHeight(); 375 } 376 377 378 void 379 BMenuItem::TruncateLabel(float maxWidth, char* newLabel) 380 { 381 BFont font; 382 fSuper->GetFont(&font); 383 384 BString string(fLabel); 385 386 font.TruncateString(&string, B_TRUNCATE_MIDDLE, maxWidth); 387 388 string.CopyInto(newLabel, 0, string.Length()); 389 newLabel[string.Length()] = '\0'; 390 } 391 392 393 void 394 BMenuItem::DrawContent() 395 { 396 MenuPrivate menuPrivate(fSuper); 397 menuPrivate.CacheFontInfo(); 398 399 fSuper->MovePenBy(0, menuPrivate.Ascent()); 400 BPoint lineStart = fSuper->PenLocation(); 401 402 fSuper->SetDrawingMode(B_OP_OVER); 403 404 float labelWidth; 405 float labelHeight; 406 GetContentSize(&labelWidth, &labelHeight); 407 408 const BRect& padding = menuPrivate.Padding(); 409 float frameWidth = fSuper->Frame().Width() - padding.left - padding.right; 410 411 if (roundf(frameWidth) >= roundf(labelWidth)) 412 fSuper->DrawString(fLabel); 413 else { 414 // truncate label to fit 415 char* truncatedLabel = new char[strlen(fLabel) + 4]; 416 TruncateLabel(frameWidth, truncatedLabel); 417 fSuper->DrawString(truncatedLabel); 418 delete[] truncatedLabel; 419 } 420 421 if (fSuper->AreTriggersEnabled() && fTriggerIndex != -1) { 422 float escapements[fTriggerIndex + 1]; 423 BFont font; 424 fSuper->GetFont(&font); 425 426 font.GetEscapements(fLabel, fTriggerIndex + 1, escapements); 427 428 for (int32 i = 0; i < fTriggerIndex; i++) 429 lineStart.x += escapements[i] * font.Size(); 430 431 lineStart.x--; 432 lineStart.y++; 433 434 BPoint lineEnd(lineStart); 435 lineEnd.x += escapements[fTriggerIndex] * font.Size(); 436 437 fSuper->StrokeLine(lineStart, lineEnd); 438 } 439 } 440 441 442 void 443 BMenuItem::Draw() 444 { 445 rgb_color lowColor = fSuper->LowColor(); 446 447 bool enabled = IsEnabled(); 448 bool selected = IsSelected(); 449 450 // set low color and fill background if selected 451 bool activated = selected && (enabled || Submenu()); 452 if (activated) { 453 BRect rect = Frame(); 454 be_control_look->DrawMenuItemBackground(fSuper, rect, rect, 455 ui_color(B_MENU_SELECTED_BACKGROUND_COLOR), 456 BControlLook::B_ACTIVATED); 457 } 458 459 // set high color 460 if (activated) 461 fSuper->SetHighColor(ui_color(B_MENU_SELECTED_ITEM_TEXT_COLOR)); 462 else if (enabled) 463 fSuper->SetHighColor(ui_color(B_MENU_ITEM_TEXT_COLOR)); 464 else { 465 // TODO: Use a lighten tint if the menu uses a dark background 466 fSuper->SetHighColor(tint_color(lowColor, B_DISABLED_LABEL_TINT)); 467 } 468 469 // draw content 470 fSuper->MovePenTo(ContentLocation()); 471 DrawContent(); 472 473 // draw extra symbols 474 const menu_layout layout = MenuPrivate(fSuper).Layout(); 475 if (layout == B_ITEMS_IN_COLUMN) { 476 if (IsMarked()) 477 _DrawMarkSymbol(); 478 479 if (fShortcutChar) 480 _DrawShortcutSymbol(); 481 482 if (Submenu()) 483 _DrawSubmenuSymbol(); 484 } 485 486 fSuper->SetLowColor(lowColor); 487 } 488 489 490 void 491 BMenuItem::Highlight(bool flag) 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 void 668 BMenuItem::_DrawMarkSymbol() 669 { 670 fSuper->PushState(); 671 672 BRect r(fBounds); 673 float leftMargin; 674 MenuPrivate(fSuper).GetItemMargins(&leftMargin, NULL, NULL, NULL); 675 r.right = r.left + leftMargin - 3; 676 r.left += 1; 677 678 BPoint center(floorf((r.left + r.right) / 2.0), 679 floorf((r.top + r.bottom) / 2.0)); 680 681 float size = min_c(r.Height() - 2, r.Width()); 682 r.top = floorf(center.y - size / 2 + 0.5); 683 r.bottom = floorf(center.y + size / 2 + 0.5); 684 r.left = floorf(center.x - size / 2 + 0.5); 685 r.right = floorf(center.x + size / 2 + 0.5); 686 687 BShape arrowShape; 688 center.x += 0.5; 689 center.y += 0.5; 690 size *= 0.3; 691 arrowShape.MoveTo(BPoint(center.x - size, center.y - size * 0.25)); 692 arrowShape.LineTo(BPoint(center.x - size * 0.25, center.y + size)); 693 arrowShape.LineTo(BPoint(center.x + size, center.y - size)); 694 695 fSuper->SetDrawingMode(B_OP_OVER); 696 fSuper->SetPenSize(2.0); 697 // NOTE: StrokeShape() offsets the shape by the current pen position, 698 // it is not documented in the BeBook, but it is true! 699 fSuper->MovePenTo(B_ORIGIN); 700 fSuper->StrokeShape(&arrowShape); 701 702 fSuper->PopState(); 703 } 704 705 706 void 707 BMenuItem::_DrawShortcutSymbol() 708 { 709 BMenu* menu = fSuper; 710 BFont font; 711 menu->GetFont(&font); 712 BPoint where = ContentLocation(); 713 where.x = fBounds.right - font.Size(); 714 715 if (fSubmenu) 716 where.x -= fBounds.Height() - 3; 717 718 const float ascent = MenuPrivate(fSuper).Ascent(); 719 if (fShortcutChar < B_SPACE && kUTF8ControlMap[(int)fShortcutChar]) 720 _DrawControlChar(fShortcutChar, where + BPoint(0, ascent)); 721 else 722 fSuper->DrawChar(fShortcutChar, where + BPoint(0, ascent)); 723 724 where.y += (fBounds.Height() - 11) / 2 - 1; 725 where.x -= 4; 726 727 // TODO: It would be nice to draw these taking into account the text (low) 728 // color. 729 if (fModifiers & B_COMMAND_KEY) { 730 const BBitmap *command = MenuPrivate::MenuItemCommand(); 731 const BRect &rect = command->Bounds(); 732 where.x -= rect.Width() + 1; 733 fSuper->DrawBitmap(command, where); 734 } 735 736 if (fModifiers & B_CONTROL_KEY) { 737 const BBitmap *control = MenuPrivate::MenuItemControl(); 738 const BRect &rect = control->Bounds(); 739 where.x -= rect.Width() + 1; 740 fSuper->DrawBitmap(control, where); 741 } 742 743 if (fModifiers & B_OPTION_KEY) { 744 const BBitmap *option = MenuPrivate::MenuItemOption(); 745 const BRect &rect = option->Bounds(); 746 where.x -= rect.Width() + 1; 747 fSuper->DrawBitmap(option, where); 748 } 749 750 if (fModifiers & B_SHIFT_KEY) { 751 const BBitmap *shift = MenuPrivate::MenuItemShift(); 752 const BRect &rect = shift->Bounds(); 753 where.x -= rect.Width() + 1; 754 fSuper->DrawBitmap(shift, where); 755 } 756 } 757 758 759 void 760 BMenuItem::_DrawSubmenuSymbol() 761 { 762 fSuper->PushState(); 763 764 BRect r(fBounds); 765 float rightMargin; 766 MenuPrivate(fSuper).GetItemMargins(NULL, NULL, &rightMargin, NULL); 767 r.left = r.right - rightMargin + 3; 768 r.right -= 1; 769 770 BPoint center(floorf((r.left + r.right) / 2.0), 771 floorf((r.top + r.bottom) / 2.0)); 772 773 float size = min_c(r.Height() - 2, r.Width()); 774 r.top = floorf(center.y - size / 2 + 0.5); 775 r.bottom = floorf(center.y + size / 2 + 0.5); 776 r.left = floorf(center.x - size / 2 + 0.5); 777 r.right = floorf(center.x + size / 2 + 0.5); 778 779 BShape arrowShape; 780 center.x += 0.5; 781 center.y += 0.5; 782 size *= 0.25; 783 float hSize = size * 0.7; 784 arrowShape.MoveTo(BPoint(center.x - hSize, center.y - size)); 785 arrowShape.LineTo(BPoint(center.x + hSize, center.y)); 786 arrowShape.LineTo(BPoint(center.x - hSize, center.y + size)); 787 788 fSuper->SetDrawingMode(B_OP_OVER); 789 fSuper->SetPenSize(ceilf(size * 0.4)); 790 // NOTE: StrokeShape() offsets the shape by the current pen position, 791 // it is not documented in the BeBook, but it is true! 792 fSuper->MovePenTo(B_ORIGIN); 793 fSuper->StrokeShape(&arrowShape); 794 795 fSuper->PopState(); 796 } 797 798 799 void 800 BMenuItem::_DrawControlChar(char shortcut, BPoint where) 801 { 802 // TODO: If needed, take another font for the control characters 803 // (or have font overlays in the app_server!) 804 const char* symbol = " "; 805 if (kUTF8ControlMap[(int)fShortcutChar]) 806 symbol = kUTF8ControlMap[(int)fShortcutChar]; 807 808 fSuper->DrawString(symbol, where); 809 } 810 811 812 void 813 BMenuItem::SetAutomaticTrigger(int32 index, uint32 trigger) 814 { 815 fTriggerIndex = index; 816 fTrigger = trigger; 817 } 818