1 /* 2 * Copyright 2009-2010, Axel Dörfler, axeld@pinc-software.de. 3 * Distributed under the terms of the MIT License. 4 */ 5 6 7 #include "CharacterView.h" 8 9 #include <stdio.h> 10 #include <string.h> 11 12 #include <Bitmap.h> 13 #include <Catalog.h> 14 #include <Clipboard.h> 15 #include <LayoutUtils.h> 16 #include <MenuItem.h> 17 #include <PopUpMenu.h> 18 #include <ScrollBar.h> 19 #include <Window.h> 20 21 #include "UnicodeBlocks.h" 22 23 #undef B_TRANSLATION_CONTEXT 24 #define B_TRANSLATION_CONTEXT "CharacterView" 25 26 static const uint32 kMsgCopyAsEscapedString = 'cesc'; 27 28 29 CharacterView::CharacterView(const char* name) 30 : BView(name, B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE | B_FRAME_EVENTS 31 | B_SCROLL_VIEW_AWARE), 32 fTargetCommand(0), 33 fClickPoint(-1, 0), 34 fHasCharacter(false), 35 fShowPrivateBlocks(false), 36 fShowContainedBlocksOnly(false) 37 { 38 fTitleTops = new int32[kNumUnicodeBlocks]; 39 fCharacterFont.SetSize(fCharacterFont.Size() * 1.5f); 40 41 _UpdateFontSize(); 42 DoLayout(); 43 } 44 45 46 CharacterView::~CharacterView() 47 { 48 delete[] fTitleTops; 49 } 50 51 52 void 53 CharacterView::SetTarget(BMessenger target, uint32 command) 54 { 55 fTarget = target; 56 fTargetCommand = command; 57 } 58 59 60 void 61 CharacterView::SetCharacterFont(const BFont& font) 62 { 63 fCharacterFont = font; 64 fUnicodeBlocks = fCharacterFont.Blocks(); 65 InvalidateLayout(); 66 } 67 68 69 void 70 CharacterView::ShowPrivateBlocks(bool show) 71 { 72 if (fShowPrivateBlocks == show) 73 return; 74 75 fShowPrivateBlocks = show; 76 InvalidateLayout(); 77 } 78 79 80 void 81 CharacterView::ShowContainedBlocksOnly(bool show) 82 { 83 if (fShowContainedBlocksOnly == show) 84 return; 85 86 fShowContainedBlocksOnly = show; 87 InvalidateLayout(); 88 } 89 90 91 bool 92 CharacterView::IsShowingBlock(int32 blockIndex) const 93 { 94 if (blockIndex < 0 || blockIndex >= (int32)kNumUnicodeBlocks) 95 return false; 96 97 if (!fShowPrivateBlocks && kUnicodeBlocks[blockIndex].private_block) 98 return false; 99 100 // The reason for two checks is BeOS compatibility. 101 // The first one checks for unicode blocks as defined by Be, 102 // but there are only 71 such blocks. 103 // The rest of the blocks (denoted by kNoBlock) need to 104 // be queried by searching for the start and end codepoints 105 // via the IncludesBlock method. 106 if (fShowContainedBlocksOnly) { 107 if (kUnicodeBlocks[blockIndex].block != kNoBlock) 108 return (fUnicodeBlocks & kUnicodeBlocks[blockIndex].block) != kNoBlock; 109 110 if (!fCharacterFont.IncludesBlock( 111 kUnicodeBlocks[blockIndex].start, 112 kUnicodeBlocks[blockIndex].end)) 113 return false; 114 } 115 116 return true; 117 } 118 119 120 void 121 CharacterView::ScrollToBlock(int32 blockIndex) 122 { 123 // don't scroll if the selected block is already in view. 124 // this prevents distracting jumps when crossing a block 125 // boundary in the character view. 126 if (IsBlockVisible(blockIndex)) 127 return; 128 129 if (blockIndex < 0) 130 blockIndex = 0; 131 else if (blockIndex >= (int32)kNumUnicodeBlocks) 132 blockIndex = kNumUnicodeBlocks - 1; 133 134 BView::ScrollTo(0.0f, fTitleTops[blockIndex]); 135 } 136 137 138 void 139 CharacterView::ScrollToCharacter(uint32 c) 140 { 141 if (IsCharacterVisible(c)) 142 return; 143 144 BRect frame = _FrameFor(c); 145 BView::ScrollTo(0.0f, frame.top); 146 } 147 148 149 bool 150 CharacterView::IsCharacterVisible(uint32 c) const 151 { 152 return Bounds().Contains(_FrameFor(c)); 153 } 154 155 156 bool 157 CharacterView::IsBlockVisible(int32 block) const 158 { 159 int32 topBlock = _BlockAt(BPoint(Bounds().left, Bounds().top)); 160 int32 bottomBlock = _BlockAt(BPoint(Bounds().right, Bounds().bottom)); 161 162 if (block >= topBlock && block <= bottomBlock) 163 return true; 164 165 return false; 166 } 167 168 169 /*static*/ void 170 CharacterView::UnicodeToUTF8(uint32 c, char* text, size_t textSize) 171 { 172 if (textSize < 5) { 173 if (textSize > 0) 174 text[0] = '\0'; 175 return; 176 } 177 178 char* s = text; 179 180 if (c < 0x80) 181 *(s++) = c; 182 else if (c < 0x800) { 183 *(s++) = 0xc0 | (c >> 6); 184 *(s++) = 0x80 | (c & 0x3f); 185 } else if (c < 0x10000) { 186 *(s++) = 0xe0 | (c >> 12); 187 *(s++) = 0x80 | ((c >> 6) & 0x3f); 188 *(s++) = 0x80 | (c & 0x3f); 189 } else if (c <= 0x10ffff) { 190 *(s++) = 0xf0 | (c >> 18); 191 *(s++) = 0x80 | ((c >> 12) & 0x3f); 192 *(s++) = 0x80 | ((c >> 6) & 0x3f); 193 *(s++) = 0x80 | (c & 0x3f); 194 } 195 196 s[0] = '\0'; 197 } 198 199 200 /*static*/ void 201 CharacterView::UnicodeToUTF8Hex(uint32 c, char* text, size_t textSize) 202 { 203 if (c == 0) { 204 snprintf(text, textSize, "\\x00"); 205 return; 206 } 207 208 char character[16]; 209 CharacterView::UnicodeToUTF8(c, character, sizeof(character)); 210 211 int size = 0; 212 for (int32 i = 0; character[i] && size < (int)textSize; i++) { 213 size += snprintf(text + size, textSize - size, "\\x%02x", 214 (uint8)character[i]); 215 } 216 } 217 218 219 void 220 CharacterView::MessageReceived(BMessage* message) 221 { 222 switch (message->what) { 223 case kMsgCopyAsEscapedString: 224 case B_COPY: 225 { 226 uint32 character; 227 if (message->FindInt32("character", (int32*)&character) != B_OK) { 228 if (!fHasCharacter) 229 break; 230 231 character = fCurrentCharacter; 232 } 233 234 char text[17]; 235 if (message->what == kMsgCopyAsEscapedString) 236 UnicodeToUTF8Hex(character, text, sizeof(text)); 237 else 238 UnicodeToUTF8(character, text, sizeof(text)); 239 240 _CopyToClipboard(text); 241 break; 242 } 243 244 default: 245 BView::MessageReceived(message); 246 break; 247 } 248 } 249 250 251 void 252 CharacterView::AttachedToWindow() 253 { 254 Window()->AddShortcut('C', B_SHIFT_KEY, 255 new BMessage(kMsgCopyAsEscapedString), this); 256 SetViewColor(255, 255, 255, 255); 257 SetLowColor(ViewColor()); 258 } 259 260 261 void 262 CharacterView::DetachedFromWindow() 263 { 264 } 265 266 267 BSize 268 CharacterView::MinSize() 269 { 270 return BLayoutUtils::ComposeSize(ExplicitMinSize(), 271 BSize(fCharacterHeight, fCharacterHeight + fTitleHeight)); 272 } 273 274 275 void 276 CharacterView::FrameResized(float width, float height) 277 { 278 // Scroll to character 279 280 if (!fHasTopCharacter) 281 return; 282 283 BRect frame = _FrameFor(fTopCharacter); 284 if (!frame.IsValid()) 285 return; 286 287 BView::ScrollTo(0, frame.top - fTopOffset); 288 fHasTopCharacter = false; 289 } 290 291 292 class PreviewItem: public BMenuItem 293 { 294 public: 295 PreviewItem(const char* text, float width, float height) 296 : BMenuItem(text, NULL), 297 fWidth(width * 2), 298 fHeight(height * 2) 299 { 300 } 301 302 void GetContentSize(float* width, float* height) 303 { 304 *width = fWidth; 305 *height = fHeight; 306 } 307 308 void Draw() 309 { 310 BMenu* menu = Menu(); 311 BRect box = Frame(); 312 313 menu->PushState(); 314 menu->SetLowUIColor(B_DOCUMENT_BACKGROUND_COLOR); 315 menu->SetViewUIColor(B_DOCUMENT_BACKGROUND_COLOR); 316 menu->SetHighUIColor(B_DOCUMENT_TEXT_COLOR); 317 menu->FillRect(box, B_SOLID_LOW); 318 319 // Draw the character in the center of the menu 320 float charWidth = menu->StringWidth(Label()); 321 font_height fontHeight; 322 menu->GetFontHeight(&fontHeight); 323 324 box.left += (box.Width() - charWidth) / 2; 325 box.bottom -= (box.Height() - fontHeight.ascent 326 + fontHeight.descent) / 2; 327 328 menu->DrawString(Label(), BPoint(box.left, box.bottom)); 329 330 menu->PopState(); 331 } 332 333 private: 334 float fWidth; 335 float fHeight; 336 }; 337 338 339 class NoMarginMenu: public BPopUpMenu 340 { 341 public: 342 NoMarginMenu() 343 : BPopUpMenu(B_EMPTY_STRING, false, false) 344 { 345 // Try to have the size right (should be exactly 2x the cell width) 346 // and the item text centered in it. 347 float left, top, bottom, right; 348 GetItemMargins(&left, &top, &bottom, &right); 349 SetItemMargins(left, top, bottom, left); 350 } 351 }; 352 353 354 void 355 CharacterView::MouseDown(BPoint where) 356 { 357 if (!fHasCharacter 358 || Window()->CurrentMessage() == NULL) 359 return; 360 361 int32 buttons; 362 if (Window()->CurrentMessage()->FindInt32("buttons", &buttons) == B_OK) { 363 if ((buttons & B_PRIMARY_MOUSE_BUTTON) != 0) { 364 // Memorize click point for dragging 365 fClickPoint = where; 366 367 char text[16]; 368 UnicodeToUTF8(fCurrentCharacter, text, sizeof(text)); 369 370 fMenu = new NoMarginMenu(); 371 fMenu->AddItem(new PreviewItem(text, fCharacterWidth, 372 fCharacterHeight)); 373 fMenu->SetFont(&fCharacterFont); 374 fMenu->SetFontSize(fCharacterFont.Size() * 2.5); 375 376 uint32 character; 377 BRect rect; 378 379 // Position the menu exactly above the character 380 _GetCharacterAt(where, character, &rect); 381 fMenu->DoLayout(); 382 where = rect.LeftTop(); 383 where.x += (rect.Width() - fMenu->Frame().Width()) / 2; 384 where.y += (rect.Height() - fMenu->Frame().Height()) / 2; 385 386 ConvertToScreen(&where); 387 fMenu->Go(where, true, true, true); 388 } else { 389 // Show context menu 390 BPopUpMenu* menu = new BPopUpMenu(B_EMPTY_STRING, false, false); 391 menu->SetFont(be_plain_font); 392 393 BMessage* message = new BMessage(B_COPY); 394 message->AddInt32("character", fCurrentCharacter); 395 menu->AddItem(new BMenuItem(B_TRANSLATE("Copy character"), message, 396 'C')); 397 398 message = new BMessage(kMsgCopyAsEscapedString); 399 message->AddInt32("character", fCurrentCharacter); 400 menu->AddItem(new BMenuItem( 401 B_TRANSLATE("Copy as escaped byte string"), 402 message, 'C', B_SHIFT_KEY)); 403 404 menu->SetTargetForItems(this); 405 406 ConvertToScreen(&where); 407 menu->Go(where, true, true, true); 408 } 409 } 410 } 411 412 413 void 414 CharacterView::MouseUp(BPoint where) 415 { 416 fClickPoint.x = -1; 417 } 418 419 420 void 421 CharacterView::MouseMoved(BPoint where, uint32 transit, 422 const BMessage* dragMessage) 423 { 424 if (dragMessage != NULL) 425 return; 426 427 BRect frame; 428 uint32 character; 429 bool hasCharacter = _GetCharacterAt(where, character, &frame); 430 431 if (fHasCharacter && (character != fCurrentCharacter || !hasCharacter)) 432 Invalidate(fCurrentCharacterFrame); 433 434 if (hasCharacter && (character != fCurrentCharacter || !fHasCharacter)) { 435 BMessage update(fTargetCommand); 436 update.AddInt32("character", character); 437 fTarget.SendMessage(&update); 438 439 Invalidate(frame); 440 } 441 442 fHasCharacter = hasCharacter; 443 fCurrentCharacter = character; 444 fCurrentCharacterFrame = frame; 445 446 if (fClickPoint.x >= 0 && (fabs(where.x - fClickPoint.x) > 4 447 || fabs(where.y - fClickPoint.y) > 4)) { 448 // Start dragging 449 450 // Update character - we want to drag the one we originally clicked 451 // on, not the one the mouse might be over now. 452 if (!_GetCharacterAt(fClickPoint, character, &frame)) 453 return; 454 455 BPoint offset = fClickPoint - frame.LeftTop(); 456 frame.OffsetTo(B_ORIGIN); 457 458 BBitmap* bitmap = new BBitmap(frame, B_BITMAP_ACCEPTS_VIEWS, B_RGBA32); 459 if (bitmap->InitCheck() != B_OK) { 460 delete bitmap; 461 return; 462 } 463 bitmap->Lock(); 464 465 BView* view = new BView(frame, "drag", 0, 0); 466 bitmap->AddChild(view); 467 468 view->SetLowColor(B_TRANSPARENT_COLOR); 469 view->FillRect(frame, B_SOLID_LOW); 470 471 // Draw character 472 char text[17]; 473 UnicodeToUTF8(character, text, sizeof(text)); 474 475 view->SetDrawingMode(B_OP_ALPHA); 476 view->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_COMPOSITE); 477 view->SetFont(&fCharacterFont); 478 view->DrawString(text, 479 BPoint((fCharacterWidth - view->StringWidth(text)) / 2, 480 fCharacterBase)); 481 482 view->Sync(); 483 bitmap->RemoveChild(view); 484 bitmap->Unlock(); 485 486 BMessage drag(B_MIME_DATA); 487 if ((modifiers() & (B_SHIFT_KEY | B_OPTION_KEY)) != 0) { 488 // paste UTF-8 hex string 489 CharacterView::UnicodeToUTF8Hex(character, text, sizeof(text)); 490 } 491 drag.AddData("text/plain", B_MIME_DATA, text, strlen(text)); 492 493 DragMessage(&drag, bitmap, B_OP_ALPHA, offset); 494 fClickPoint.x = -1; 495 496 fHasCharacter = false; 497 Invalidate(fCurrentCharacterFrame); 498 } 499 } 500 501 502 void 503 CharacterView::Draw(BRect updateRect) 504 { 505 const int32 kXGap = fGap / 2; 506 507 BFont font; 508 GetFont(&font); 509 510 rgb_color color = (rgb_color){0, 0, 0, 255}; 511 rgb_color highlight = (rgb_color){220, 220, 220, 255}; 512 rgb_color enclose = mix_color(highlight, 513 ui_color(B_CONTROL_HIGHLIGHT_COLOR), 128); 514 515 for (int32 i = _BlockAt(updateRect.LeftTop()); i < (int32)kNumUnicodeBlocks; 516 i++) { 517 if (!IsShowingBlock(i)) 518 continue; 519 520 int32 y = fTitleTops[i]; 521 if (y > updateRect.bottom) 522 break; 523 524 SetHighColor(color); 525 DrawString(kUnicodeBlocks[i].name, BPoint(3, y + fTitleBase)); 526 527 y += fTitleHeight; 528 int32 x = kXGap; 529 SetFont(&fCharacterFont); 530 531 for (uint32 c = kUnicodeBlocks[i].start; c <= kUnicodeBlocks[i].end; 532 c++) { 533 if (y + fCharacterHeight > updateRect.top 534 && y < updateRect.bottom) { 535 // Stroke frame around the active character 536 if (fHasCharacter && fCurrentCharacter == c) { 537 SetHighColor(highlight); 538 FillRect(BRect(x, y, x + fCharacterWidth, 539 y + fCharacterHeight - fGap)); 540 SetHighColor(enclose); 541 StrokeRect(BRect(x, y, x + fCharacterWidth, 542 y + fCharacterHeight - fGap)); 543 544 SetHighColor(color); 545 SetLowColor(highlight); 546 } 547 548 // Draw character 549 char character[16]; 550 UnicodeToUTF8(c, character, sizeof(character)); 551 552 DrawString(character, 553 BPoint(x + (fCharacterWidth - StringWidth(character)) / 2, 554 y + fCharacterBase)); 555 } 556 557 x += fCharacterWidth + fGap; 558 if (x + fCharacterWidth + kXGap >= fDataRect.right) { 559 y += fCharacterHeight; 560 x = kXGap; 561 } 562 } 563 564 if (x != kXGap) 565 y += fCharacterHeight; 566 y += fTitleGap; 567 568 SetFont(&font); 569 } 570 } 571 572 573 void 574 CharacterView::DoLayout() 575 { 576 fHasTopCharacter = _GetTopmostCharacter(fTopCharacter, fTopOffset); 577 _UpdateSize(); 578 } 579 580 581 int32 582 CharacterView::_BlockAt(BPoint point) const 583 { 584 uint32 min = 0; 585 uint32 max = kNumUnicodeBlocks; 586 uint32 guess = (max + min) / 2; 587 588 while ((max >= min) && (guess < kNumUnicodeBlocks - 1 )) { 589 if (fTitleTops[guess] <= point.y && fTitleTops[guess + 1] >= point.y) { 590 if (!IsShowingBlock(guess)) 591 return -1; 592 else 593 return guess; 594 } 595 596 if (fTitleTops[guess + 1] < point.y) { 597 min = guess + 1; 598 } else { 599 max = guess - 1; 600 } 601 602 guess = (max + min) / 2; 603 } 604 605 return -1; 606 } 607 608 609 bool 610 CharacterView::_GetCharacterAt(BPoint point, uint32& character, 611 BRect* _frame) const 612 { 613 int32 i = _BlockAt(point); 614 if (i == -1) 615 return false; 616 617 int32 y = fTitleTops[i] + fTitleHeight; 618 if (y > point.y) 619 return false; 620 621 const int32 startX = fGap / 2; 622 if (startX > point.x) 623 return false; 624 625 int32 endX = startX + fCharactersPerLine * (fCharacterWidth + fGap); 626 if (endX < point.x) 627 return false; 628 629 for (uint32 c = kUnicodeBlocks[i].start; c <= kUnicodeBlocks[i].end; 630 c += fCharactersPerLine, y += fCharacterHeight) { 631 if (y + fCharacterHeight <= point.y) 632 continue; 633 634 int32 pos = (int32)((point.x - startX) / (fCharacterWidth + fGap)); 635 if (c + pos > kUnicodeBlocks[i].end) 636 return false; 637 638 // Found character at position 639 640 character = c + pos; 641 642 if (_frame != NULL) { 643 _frame->Set(startX + pos * (fCharacterWidth + fGap), 644 y, startX + (pos + 1) * (fCharacterWidth + fGap) - 1, 645 y + fCharacterHeight); 646 } 647 648 return true; 649 } 650 651 return false; 652 } 653 654 655 void 656 CharacterView::_UpdateFontSize() 657 { 658 font_height fontHeight; 659 GetFontHeight(&fontHeight); 660 fTitleHeight = (int32)ceilf(fontHeight.ascent + fontHeight.descent 661 + fontHeight.leading) + 2; 662 fTitleBase = (int32)ceilf(fontHeight.ascent); 663 664 // Find widest character 665 fCharacterWidth = (int32)ceilf(fCharacterFont.StringWidth("W") * 1.5f); 666 667 if (fCharacterFont.IsFullAndHalfFixed()) { 668 // TODO: improve this! 669 fCharacterWidth = (int32)ceilf(fCharacterWidth * 1.4); 670 } 671 672 fCharacterFont.GetHeight(&fontHeight); 673 fCharacterHeight = (int32)ceilf(fontHeight.ascent + fontHeight.descent 674 + fontHeight.leading); 675 fCharacterBase = (int32)ceilf(fontHeight.ascent); 676 677 fGap = (int32)roundf(fCharacterHeight / 8.0); 678 if (fGap < 3) 679 fGap = 3; 680 681 fCharacterHeight += fGap; 682 fTitleGap = fGap * 3; 683 } 684 685 686 void 687 CharacterView::_UpdateSize() 688 { 689 // Compute data rect 690 691 BRect bounds = Bounds(); 692 693 _UpdateFontSize(); 694 695 fDataRect.right = bounds.Width(); 696 fDataRect.bottom = 0; 697 698 fCharactersPerLine = int32(bounds.Width() / (fGap + fCharacterWidth)); 699 if (fCharactersPerLine == 0) 700 fCharactersPerLine = 1; 701 702 for (uint32 i = 0; i < kNumUnicodeBlocks; i++) { 703 fTitleTops[i] = (int32)ceilf(fDataRect.bottom); 704 705 if (!IsShowingBlock(i)) 706 continue; 707 708 int32 lines = (kUnicodeBlocks[i].Count() + fCharactersPerLine - 1) 709 / fCharactersPerLine; 710 fDataRect.bottom += lines * fCharacterHeight + fTitleHeight + fTitleGap; 711 } 712 713 // Update scroll bars 714 715 BScrollBar* scroller = ScrollBar(B_VERTICAL); 716 if (scroller == NULL) 717 return; 718 719 if (bounds.Height() > fDataRect.Height()) { 720 // no scrolling 721 scroller->SetRange(0.0f, 0.0f); 722 scroller->SetValue(0.0f); 723 } else { 724 scroller->SetRange(0.0f, fDataRect.Height() - bounds.Height() - 1.0f); 725 scroller->SetProportion(bounds.Height () / fDataRect.Height()); 726 scroller->SetSteps(fCharacterHeight, 727 Bounds().Height() - fCharacterHeight); 728 729 // scroll up if there is empty room on bottom 730 if (fDataRect.Height() < bounds.bottom) 731 ScrollBy(0.0f, bounds.bottom - fDataRect.Height()); 732 } 733 734 Invalidate(); 735 } 736 737 738 bool 739 CharacterView::_GetTopmostCharacter(uint32& character, int32& offset) const 740 { 741 int32 top = (int32)Bounds().top; 742 743 int32 i = _BlockAt(BPoint(0, top)); 744 if (i == -1) 745 return false; 746 747 int32 characterTop = fTitleTops[i] + fTitleHeight; 748 if (characterTop > top) { 749 character = kUnicodeBlocks[i].start; 750 offset = characterTop - top; 751 return true; 752 } 753 754 int32 lines = (top - characterTop + fCharacterHeight - 1) 755 / fCharacterHeight; 756 757 character = kUnicodeBlocks[i].start + lines * fCharactersPerLine; 758 offset = top - characterTop - lines * fCharacterHeight; 759 return true; 760 } 761 762 763 BRect 764 CharacterView::_FrameFor(uint32 character) const 765 { 766 // find block containing the character 767 int32 blockNumber = BlockForCharacter(character); 768 769 if (blockNumber > 0) { 770 int32 diff = character - kUnicodeBlocks[blockNumber].start; 771 int32 y = fTitleTops[blockNumber] + fTitleHeight 772 + (diff / fCharactersPerLine) * fCharacterHeight; 773 int32 x = fGap / 2 + diff % fCharactersPerLine; 774 775 return BRect(x, y, x + fCharacterWidth + fGap, y + fCharacterHeight); 776 } 777 778 return BRect(); 779 } 780 781 782 void 783 CharacterView::_CopyToClipboard(const char* text) 784 { 785 if (!be_clipboard->Lock()) 786 return; 787 788 be_clipboard->Clear(); 789 790 BMessage* clip = be_clipboard->Data(); 791 if (clip != NULL) { 792 clip->AddData("text/plain", B_MIME_TYPE, text, strlen(text)); 793 be_clipboard->Commit(); 794 } 795 796 be_clipboard->Unlock(); 797 } 798