xref: /haiku/src/apps/charactermap/CharacterView.cpp (revision b08627f310bb2e80bca50176e7a758182384735a)
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