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