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