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