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