xref: /haiku/src/apps/charactermap/CharacterView.cpp (revision b671e9bbdbd10268a042b4f4cc4317ccd03d105e)
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 		_GetCharacterAt(fClickPoint, character, &frame);
307 
308 		BPoint offset = fClickPoint - frame.LeftTop();
309 		frame.OffsetTo(B_ORIGIN);
310 
311 		BBitmap* bitmap = new BBitmap(frame, B_BITMAP_ACCEPTS_VIEWS, B_RGBA32);
312 		bitmap->Lock();
313 
314 		BView* view = new BView(frame, "drag", 0, 0);
315 		bitmap->AddChild(view);
316 
317 		view->SetLowColor(B_TRANSPARENT_COLOR);
318 		view->FillRect(frame, B_SOLID_LOW);
319 
320 		// Draw character
321 		char text[16];
322 		UnicodeToUTF8(character, text, sizeof(text));
323 
324 		view->SetDrawingMode(B_OP_ALPHA);
325 		view->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_COMPOSITE);
326 		view->SetFont(&fCharacterFont);
327 		view->DrawString(text,
328 			BPoint((fCharacterWidth - view->StringWidth(text)) / 2,
329 				fCharacterBase));
330 
331 		view->Sync();
332 		bitmap->RemoveChild(view);
333 		bitmap->Unlock();
334 
335 		BMessage drag(B_MIME_DATA);
336 		if ((modifiers() & (B_SHIFT_KEY | B_OPTION_KEY)) != 0) {
337 			// paste UTF-8 hex string
338 			CharacterView::UnicodeToUTF8Hex(character, text, sizeof(text));
339 		}
340 		drag.AddData("text/plain", B_MIME_DATA, text, strlen(text));
341 
342 		DragMessage(&drag, bitmap, B_OP_ALPHA, offset);
343 		fClickPoint.x = -1;
344 
345 		fHasCharacter = false;
346 		Invalidate(fCurrentCharacterFrame);
347 	}
348 }
349 
350 
351 void
352 CharacterView::Draw(BRect updateRect)
353 {
354 	const int32 kXGap = fGap / 2;
355 
356 	BFont font;
357 	GetFont(&font);
358 
359 	for (int32 i = _BlockAt(updateRect.LeftTop()); i < (int32)kNumUnicodeBlocks;
360 			i++) {
361 		if (!IsShowingBlock(i))
362 			continue;
363 
364 		int32 y = fTitleTops[i];
365 		if (y > updateRect.bottom)
366 			break;
367 
368  		SetHighColor(0, 0, 0);
369 		DrawString(kUnicodeBlocks[i].name, BPoint(3, y + fTitleBase));
370 
371 		y += fTitleHeight;
372 		int32 x = kXGap;
373 		SetFont(&fCharacterFont);
374 
375 		for (uint32 c = kUnicodeBlocks[i].start; c <= kUnicodeBlocks[i].end;
376 				c++) {
377 			if (y + fCharacterHeight > updateRect.top
378 				&& y < updateRect.bottom) {
379 				// Stroke frame around the active character
380 				if (fHasCharacter && fCurrentCharacter == c) {
381 					SetHighColor(tint_color(ViewColor(), 1.05f));
382 					FillRect(BRect(x, y, x + fCharacterWidth,
383 						y + fCharacterHeight - fGap));
384 
385 					SetHighColor(0, 0, 0);
386 				}
387 
388 				// Draw character
389 				char character[16];
390 				UnicodeToUTF8(c, character, sizeof(character));
391 
392 				DrawString(character,
393 					BPoint(x + (fCharacterWidth - StringWidth(character)) / 2,
394 						y + fCharacterBase));
395 			}
396 
397 			x += fCharacterWidth + fGap;
398 			if (x + fCharacterWidth + kXGap >= fDataRect.right) {
399 				y += fCharacterHeight;
400 				x = kXGap;
401 			}
402 		}
403 
404 		if (x != kXGap)
405 			y += fCharacterHeight;
406 		y += fTitleGap;
407 
408 		SetFont(&font);
409 	}
410 }
411 
412 
413 void
414 CharacterView::DoLayout()
415 {
416 	fHasTopCharacter = _GetTopmostCharacter(fTopCharacter, fTopOffset);
417 	_UpdateSize();
418 }
419 
420 
421 int32
422 CharacterView::_BlockAt(BPoint point)
423 {
424 	// TODO: use binary search
425 	for (uint32 i = 0; i < kNumUnicodeBlocks; i++) {
426 		if (!IsShowingBlock(i))
427 			continue;
428 
429 		if (fTitleTops[i] <= point.y
430 			&& (i == kNumUnicodeBlocks - 1 || fTitleTops[i + 1] > point.y))
431 			return i;
432 	}
433 
434 	return -1;
435 }
436 
437 
438 bool
439 CharacterView::_GetCharacterAt(BPoint point, uint32& character, BRect* _frame)
440 {
441 	int32 i = _BlockAt(point);
442 	if (i == -1)
443 		return false;
444 
445 	int32 y = fTitleTops[i] + fTitleHeight;
446 	if (y > point.y)
447 		return false;
448 
449 	const int32 startX = fGap / 2;
450 	if (startX > point.x)
451 		return false;
452 
453 	int32 endX = startX + fCharactersPerLine * (fCharacterWidth + fGap);
454 	if (endX < point.x)
455 		return false;
456 
457 	for (uint32 c = kUnicodeBlocks[i].start; c <= kUnicodeBlocks[i].end;
458 			c += fCharactersPerLine, y += fCharacterHeight) {
459 		if (y + fCharacterHeight <= point.y)
460 			continue;
461 
462 		int32 pos = (int32)((point.x - startX) / (fCharacterWidth + fGap));
463 		if (c + pos > kUnicodeBlocks[i].end)
464 			return false;
465 
466 		// Found character at position
467 
468 		character = c + pos;
469 
470 		if (_frame != NULL) {
471 			_frame->Set(startX + pos * (fCharacterWidth + fGap),
472 				y, startX + (pos + 1) * (fCharacterWidth + fGap) - 1,
473 				y + fCharacterHeight);
474 		}
475 
476 		return true;
477 	}
478 
479 	return false;
480 }
481 
482 
483 void
484 CharacterView::_UpdateFontSize()
485 {
486 	font_height fontHeight;
487 	GetFontHeight(&fontHeight);
488 	fTitleHeight = (int32)ceilf(fontHeight.ascent + fontHeight.descent
489 		+ fontHeight.leading) + 2;
490 	fTitleBase = (int32)ceilf(fontHeight.ascent);
491 
492 	// Find widest character
493 	fCharacterWidth = (int32)ceilf(fCharacterFont.StringWidth("W") * 1.5f);
494 
495 	if (fCharacterFont.IsFullAndHalfFixed()) {
496 		// TODO: improve this!
497 		fCharacterWidth = (int32)ceilf(fCharacterWidth * 1.4);
498 	}
499 
500 	fCharacterFont.GetHeight(&fontHeight);
501 	fCharacterHeight = (int32)ceilf(fontHeight.ascent + fontHeight.descent
502 		+ fontHeight.leading);
503 	fCharacterBase = (int32)ceilf(fontHeight.ascent);
504 
505 	fGap = (int32)roundf(fCharacterHeight / 8.0);
506 	if (fGap < 3)
507 		fGap = 3;
508 
509 	fCharacterHeight += fGap;
510 	fTitleGap = fGap * 3;
511 }
512 
513 
514 void
515 CharacterView::_UpdateSize()
516 {
517 	// Compute data rect
518 
519 	BRect bounds = Bounds();
520 
521 	_UpdateFontSize();
522 
523 	fDataRect.right = bounds.Width();
524 	fDataRect.bottom = 0;
525 
526 	fCharactersPerLine = int32(bounds.Width() / (fGap + fCharacterWidth));
527 	if (fCharactersPerLine == 0)
528 		fCharactersPerLine = 1;
529 
530 	for (uint32 i = 0; i < kNumUnicodeBlocks; i++) {
531 		fTitleTops[i] = (int32)ceilf(fDataRect.bottom);
532 
533 		if (!IsShowingBlock(i))
534 			continue;
535 
536 		int32 lines = (kUnicodeBlocks[i].Count() + fCharactersPerLine - 1)
537 			/ fCharactersPerLine;
538 		fDataRect.bottom += lines * fCharacterHeight + fTitleHeight + fTitleGap;
539 	}
540 
541 	// Update scroll bars
542 
543 	BScrollBar* scroller = ScrollBar(B_VERTICAL);
544 	if (scroller == NULL)
545 		return;
546 
547 	if (bounds.Height() > fDataRect.Height()) {
548 		// no scrolling
549 		scroller->SetRange(0.0f, 0.0f);
550 		scroller->SetValue(0.0f);
551 	} else {
552 		scroller->SetRange(0.0f, fDataRect.Height() - bounds.Height() - 1.0f);
553 		scroller->SetProportion(bounds.Height () / fDataRect.Height());
554 		scroller->SetSteps(fCharacterHeight,
555 			Bounds().Height() - fCharacterHeight);
556 
557 		// scroll up if there is empty room on bottom
558 		if (fDataRect.Height() < bounds.bottom)
559 			ScrollBy(0.0f, bounds.bottom - fDataRect.Height());
560 	}
561 
562 	Invalidate();
563 }
564 
565 
566 bool
567 CharacterView::_GetTopmostCharacter(uint32& character, int32& offset)
568 {
569 	int32 top = (int32)Bounds().top;
570 
571 	int32 i = _BlockAt(BPoint(0, top));
572 	if (i == -1)
573 		return false;
574 
575 	int32 characterTop = fTitleTops[i] + fTitleHeight;
576 	if (characterTop > top) {
577 		character = kUnicodeBlocks[i].start;
578 		offset = characterTop - top;
579 		return true;
580 	}
581 
582 	int32 lines = (top - characterTop + fCharacterHeight - 1)
583 		/ fCharacterHeight;
584 
585 	character = kUnicodeBlocks[i].start + lines * fCharactersPerLine;
586 	offset = top - characterTop - lines * fCharacterHeight;
587 	return true;
588 }
589 
590 
591 BRect
592 CharacterView::_FrameFor(uint32 character)
593 {
594 	// find block containing the character
595 
596 	// TODO: could use binary search here
597 
598 	for (uint32 i = 0; i < kNumUnicodeBlocks; i++) {
599 		if (kUnicodeBlocks[i].end < character)
600 			continue;
601 		if (kUnicodeBlocks[i].start > character) {
602 			// Character is not mapped
603 			return BRect();
604 		}
605 
606 		int32 diff = character - kUnicodeBlocks[i].start;
607 		int32 y = fTitleTops[i] + fTitleHeight + diff / fCharactersPerLine;
608 		int32 x = fGap / 2 + diff % fCharactersPerLine;
609 
610 		return BRect(x, y, x + fCharacterWidth + fGap, y + fCharacterHeight);
611 	}
612 
613 	return BRect();
614 }
615 
616 
617 void
618 CharacterView::_CopyToClipboard(const char* text)
619 {
620 	if (!be_clipboard->Lock())
621 		return;
622 
623 	be_clipboard->Clear();
624 
625 	BMessage* clip = be_clipboard->Data();
626 	if (clip != NULL) {
627 		clip->AddData("text/plain", B_MIME_TYPE, text, strlen(text));
628 		be_clipboard->Commit();
629 	}
630 
631 	be_clipboard->Unlock();
632 }
633