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