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