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