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