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