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
26static const uint32 kMsgCopyAsEscapedString = 'cesc';
27
28
29CharacterView::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
44CharacterView::~CharacterView()
45{
46	delete[] fTitleTops;
47}
48
49
50void
51CharacterView::SetTarget(BMessenger target, uint32 command)
52{
53	fTarget = target;
54	fTargetCommand = command;
55}
56
57
58void
59CharacterView::SetCharacterFont(const BFont& font)
60{
61	fCharacterFont = font;
62
63	InvalidateLayout();
64}
65
66
67void
68CharacterView::ShowPrivateBlocks(bool show)
69{
70	if (fShowPrivateBlocks == show)
71		return;
72
73	fShowPrivateBlocks = show;
74	InvalidateLayout();
75}
76
77
78void
79CharacterView::ShowContainedBlocksOnly(bool show)
80{
81	if (fShowContainedBlocksOnly == show)
82		return;
83
84	fShowContainedBlocksOnly = show;
85	InvalidateLayout();
86}
87
88
89bool
90CharacterView::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
108void
109CharacterView::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
120void
121CharacterView::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
131bool
132CharacterView::IsCharacterVisible(uint32 c) const
133{
134	return Bounds().Contains(_FrameFor(c));
135}
136
137
138/*static*/ void
139CharacterView::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
170CharacterView::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
183void
184CharacterView::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
215void
216CharacterView::AttachedToWindow()
217{
218	Window()->AddShortcut('C', B_SHIFT_KEY,
219		new BMessage(kMsgCopyAsEscapedString), this);
220}
221
222
223void
224CharacterView::DetachedFromWindow()
225{
226}
227
228
229BSize
230CharacterView::MinSize()
231{
232	return BLayoutUtils::ComposeSize(ExplicitMinSize(),
233		BSize(fCharacterHeight, fCharacterHeight + fTitleHeight));
234}
235
236
237void
238CharacterView::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
254void
255CharacterView::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
288void
289CharacterView::MouseUp(BPoint where)
290{
291	fClickPoint.x = -1;
292}
293
294
295void
296CharacterView::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
377void
378CharacterView::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
439void
440CharacterView::DoLayout()
441{
442	fHasTopCharacter = _GetTopmostCharacter(fTopCharacter, fTopOffset);
443	_UpdateSize();
444}
445
446
447int32
448CharacterView::_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
464bool
465CharacterView::_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
510void
511CharacterView::_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
541void
542CharacterView::_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
593bool
594CharacterView::_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
618BRect
619CharacterView::_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
645void
646CharacterView::_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