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