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