xref: /haiku/src/apps/charactermap/CharacterView.cpp (revision f2df0cfe93a902842f6f4629ff614f5b3f9bf687)
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 				}
443 
444 				// Draw character
445 				char character[16];
446 				UnicodeToUTF8(c, character, sizeof(character));
447 
448 				DrawString(character,
449 					BPoint(x + (fCharacterWidth - StringWidth(character)) / 2,
450 						y + fCharacterBase));
451 			}
452 
453 			x += fCharacterWidth + fGap;
454 			if (x + fCharacterWidth + kXGap >= fDataRect.right) {
455 				y += fCharacterHeight;
456 				x = kXGap;
457 			}
458 		}
459 
460 		if (x != kXGap)
461 			y += fCharacterHeight;
462 		y += fTitleGap;
463 
464 		SetFont(&font);
465 	}
466 }
467 
468 
469 void
470 CharacterView::DoLayout()
471 {
472 	fHasTopCharacter = _GetTopmostCharacter(fTopCharacter, fTopOffset);
473 	_UpdateSize();
474 }
475 
476 
477 int32
478 CharacterView::_BlockAt(BPoint point) const
479 {
480 	// TODO: use binary search
481 	for (uint32 i = 0; i < kNumUnicodeBlocks; i++) {
482 		if (!IsShowingBlock(i))
483 			continue;
484 
485 		if (fTitleTops[i] <= point.y
486 			&& (i == kNumUnicodeBlocks - 1 || fTitleTops[i + 1] > point.y))
487 			return i;
488 	}
489 
490 	return -1;
491 }
492 
493 
494 bool
495 CharacterView::_GetCharacterAt(BPoint point, uint32& character,
496 	BRect* _frame) const
497 {
498 	int32 i = _BlockAt(point);
499 	if (i == -1)
500 		return false;
501 
502 	int32 y = fTitleTops[i] + fTitleHeight;
503 	if (y > point.y)
504 		return false;
505 
506 	const int32 startX = fGap / 2;
507 	if (startX > point.x)
508 		return false;
509 
510 	int32 endX = startX + fCharactersPerLine * (fCharacterWidth + fGap);
511 	if (endX < point.x)
512 		return false;
513 
514 	for (uint32 c = kUnicodeBlocks[i].start; c <= kUnicodeBlocks[i].end;
515 			c += fCharactersPerLine, y += fCharacterHeight) {
516 		if (y + fCharacterHeight <= point.y)
517 			continue;
518 
519 		int32 pos = (int32)((point.x - startX) / (fCharacterWidth + fGap));
520 		if (c + pos > kUnicodeBlocks[i].end)
521 			return false;
522 
523 		// Found character at position
524 
525 		character = c + pos;
526 
527 		if (_frame != NULL) {
528 			_frame->Set(startX + pos * (fCharacterWidth + fGap),
529 				y, startX + (pos + 1) * (fCharacterWidth + fGap) - 1,
530 				y + fCharacterHeight);
531 		}
532 
533 		return true;
534 	}
535 
536 	return false;
537 }
538 
539 
540 void
541 CharacterView::_UpdateFontSize()
542 {
543 	font_height fontHeight;
544 	GetFontHeight(&fontHeight);
545 	fTitleHeight = (int32)ceilf(fontHeight.ascent + fontHeight.descent
546 		+ fontHeight.leading) + 2;
547 	fTitleBase = (int32)ceilf(fontHeight.ascent);
548 
549 	// Find widest character
550 	fCharacterWidth = (int32)ceilf(fCharacterFont.StringWidth("W") * 1.5f);
551 
552 	if (fCharacterFont.IsFullAndHalfFixed()) {
553 		// TODO: improve this!
554 		fCharacterWidth = (int32)ceilf(fCharacterWidth * 1.4);
555 	}
556 
557 	fCharacterFont.GetHeight(&fontHeight);
558 	fCharacterHeight = (int32)ceilf(fontHeight.ascent + fontHeight.descent
559 		+ fontHeight.leading);
560 	fCharacterBase = (int32)ceilf(fontHeight.ascent);
561 
562 	fGap = (int32)roundf(fCharacterHeight / 8.0);
563 	if (fGap < 3)
564 		fGap = 3;
565 
566 	fCharacterHeight += fGap;
567 	fTitleGap = fGap * 3;
568 }
569 
570 
571 void
572 CharacterView::_UpdateSize()
573 {
574 	// Compute data rect
575 
576 	BRect bounds = Bounds();
577 
578 	_UpdateFontSize();
579 
580 	fDataRect.right = bounds.Width();
581 	fDataRect.bottom = 0;
582 
583 	fCharactersPerLine = int32(bounds.Width() / (fGap + fCharacterWidth));
584 	if (fCharactersPerLine == 0)
585 		fCharactersPerLine = 1;
586 
587 	for (uint32 i = 0; i < kNumUnicodeBlocks; i++) {
588 		fTitleTops[i] = (int32)ceilf(fDataRect.bottom);
589 
590 		if (!IsShowingBlock(i))
591 			continue;
592 
593 		int32 lines = (kUnicodeBlocks[i].Count() + fCharactersPerLine - 1)
594 			/ fCharactersPerLine;
595 		fDataRect.bottom += lines * fCharacterHeight + fTitleHeight + fTitleGap;
596 	}
597 
598 	// Update scroll bars
599 
600 	BScrollBar* scroller = ScrollBar(B_VERTICAL);
601 	if (scroller == NULL)
602 		return;
603 
604 	if (bounds.Height() > fDataRect.Height()) {
605 		// no scrolling
606 		scroller->SetRange(0.0f, 0.0f);
607 		scroller->SetValue(0.0f);
608 	} else {
609 		scroller->SetRange(0.0f, fDataRect.Height() - bounds.Height() - 1.0f);
610 		scroller->SetProportion(bounds.Height () / fDataRect.Height());
611 		scroller->SetSteps(fCharacterHeight,
612 			Bounds().Height() - fCharacterHeight);
613 
614 		// scroll up if there is empty room on bottom
615 		if (fDataRect.Height() < bounds.bottom)
616 			ScrollBy(0.0f, bounds.bottom - fDataRect.Height());
617 	}
618 
619 	Invalidate();
620 }
621 
622 
623 bool
624 CharacterView::_GetTopmostCharacter(uint32& character, int32& offset) const
625 {
626 	int32 top = (int32)Bounds().top;
627 
628 	int32 i = _BlockAt(BPoint(0, top));
629 	if (i == -1)
630 		return false;
631 
632 	int32 characterTop = fTitleTops[i] + fTitleHeight;
633 	if (characterTop > top) {
634 		character = kUnicodeBlocks[i].start;
635 		offset = characterTop - top;
636 		return true;
637 	}
638 
639 	int32 lines = (top - characterTop + fCharacterHeight - 1)
640 		/ fCharacterHeight;
641 
642 	character = kUnicodeBlocks[i].start + lines * fCharactersPerLine;
643 	offset = top - characterTop - lines * fCharacterHeight;
644 	return true;
645 }
646 
647 
648 BRect
649 CharacterView::_FrameFor(uint32 character) const
650 {
651 	// find block containing the character
652 
653 	// TODO: could use binary search here
654 
655 	for (uint32 i = 0; i < kNumUnicodeBlocks; i++) {
656 		if (kUnicodeBlocks[i].end < character)
657 			continue;
658 		if (kUnicodeBlocks[i].start > character) {
659 			// Character is not mapped
660 			return BRect();
661 		}
662 
663 		int32 diff = character - kUnicodeBlocks[i].start;
664 		int32 y = fTitleTops[i] + fTitleHeight
665 			+ (diff / fCharactersPerLine) * fCharacterHeight;
666 		int32 x = fGap / 2 + diff % fCharactersPerLine;
667 
668 		return BRect(x, y, x + fCharacterWidth + fGap, y + fCharacterHeight);
669 	}
670 
671 	return BRect();
672 }
673 
674 
675 void
676 CharacterView::_CopyToClipboard(const char* text)
677 {
678 	if (!be_clipboard->Lock())
679 		return;
680 
681 	be_clipboard->Clear();
682 
683 	BMessage* clip = be_clipboard->Data();
684 	if (clip != NULL) {
685 		clip->AddData("text/plain", B_MIME_TYPE, text, strlen(text));
686 		be_clipboard->Commit();
687 	}
688 
689 	be_clipboard->Unlock();
690 }
691