xref: /haiku/src/apps/charactermap/CharacterWindow.cpp (revision fc7456e9b1ec38c941134ed6d01c438cf289381e)
1 /*
2  * Copyright 2009-2015, Axel Dörfler, axeld@pinc-software.de.
3  * Copyright 2011, Philippe Saint-Pierre, stpere@gmail.com.
4  * Distributed under the terms of the MIT License.
5  */
6 
7 
8 #include "CharacterWindow.h"
9 
10 #include <stdio.h>
11 #include <string.h>
12 
13 #include <Application.h>
14 #include <Button.h>
15 #include <Catalog.h>
16 #include <File.h>
17 #include <FindDirectory.h>
18 #include <Font.h>
19 #include <LayoutBuilder.h>
20 #include <ListView.h>
21 #include <Menu.h>
22 #include <MenuBar.h>
23 #include <MenuItem.h>
24 #include <MessageFilter.h>
25 #include <Path.h>
26 #include <Roster.h>
27 #include <ScrollView.h>
28 #include <Slider.h>
29 #include <StringView.h>
30 #include <TextControl.h>
31 #include <UnicodeChar.h>
32 
33 #include "CharacterView.h"
34 #include "UnicodeBlockView.h"
35 
36 
37 #undef B_TRANSLATION_CONTEXT
38 #define B_TRANSLATION_CONTEXT "CharacterWindow"
39 
40 
41 static const uint32 kMsgUnicodeBlockSelected = 'unbs';
42 static const uint32 kMsgCharacterChanged = 'chch';
43 static const uint32 kMsgFontSelected = 'fnts';
44 static const uint32 kMsgFontSizeChanged = 'fsch';
45 static const uint32 kMsgPrivateBlocks = 'prbl';
46 static const uint32 kMsgContainedBlocks = 'cnbl';
47 static const uint32 kMsgFilterChanged = 'fltr';
48 static const uint32 kMsgClearFilter = 'clrf';
49 
50 static const int32 kMinFontSize = 10;
51 static const int32 kMaxFontSize = 72;
52 
53 
54 class FontSizeSlider : public BSlider {
55 public:
56 	FontSizeSlider(const char* name, const char* label, BMessage* message,
57 			int32 min, int32 max)
58 		: BSlider(name, label, NULL, min, max, B_HORIZONTAL)
59 	{
60 		SetModificationMessage(message);
61 	}
62 
63 protected:
64 	const char* UpdateText() const
65 	{
66 		snprintf(fText, sizeof(fText), "%" B_PRId32 "pt", Value());
67 		return fText;
68 	}
69 
70 private:
71 	mutable char	fText[32];
72 };
73 
74 
75 class RedirectUpAndDownFilter : public BMessageFilter {
76 public:
77 	RedirectUpAndDownFilter(BHandler* target)
78 		: BMessageFilter(B_ANY_DELIVERY, B_ANY_SOURCE, B_KEY_DOWN),
79 		fTarget(target)
80 	{
81 	}
82 
83 	virtual filter_result Filter(BMessage* message, BHandler** _target)
84 	{
85 		const char* bytes;
86 		if (message->FindString("bytes", &bytes) != B_OK)
87 			return B_DISPATCH_MESSAGE;
88 
89 		if (bytes[0] == B_UP_ARROW
90 			|| bytes[0] == B_DOWN_ARROW)
91 			*_target = fTarget;
92 
93 		return B_DISPATCH_MESSAGE;
94 	}
95 
96 private:
97 	BHandler*	fTarget;
98 };
99 
100 
101 class EscapeMessageFilter : public BMessageFilter {
102 public:
103 	EscapeMessageFilter(uint32 command)
104 		: BMessageFilter(B_ANY_DELIVERY, B_ANY_SOURCE, B_KEY_DOWN),
105 		fCommand(command)
106 	{
107 	}
108 
109 	virtual filter_result Filter(BMessage* message, BHandler** /*_target*/)
110 	{
111 		const char* bytes;
112 		if (message->what != B_KEY_DOWN
113 			|| message->FindString("bytes", &bytes) != B_OK
114 			|| bytes[0] != B_ESCAPE)
115 			return B_DISPATCH_MESSAGE;
116 
117 		Looper()->PostMessage(fCommand);
118 		return B_SKIP_MESSAGE;
119 	}
120 
121 private:
122 	uint32	fCommand;
123 };
124 
125 
126 CharacterWindow::CharacterWindow()
127 	:
128 	BWindow(BRect(100, 100, 700, 550), B_TRANSLATE_SYSTEM_NAME("CharacterMap"),
129 		B_TITLED_WINDOW, B_ASYNCHRONOUS_CONTROLS | B_QUIT_ON_WINDOW_CLOSE
130 			| B_AUTO_UPDATE_SIZE_LIMITS)
131 {
132 	BMessage settings;
133 	_LoadSettings(settings);
134 
135 	BRect frame;
136 	if (settings.FindRect("window frame", &frame) == B_OK) {
137 		MoveTo(frame.LeftTop());
138 		ResizeTo(frame.Width(), frame.Height());
139 		MoveOnScreen(B_MOVE_IF_PARTIALLY_OFFSCREEN);
140 	} else {
141 		float scaling = be_plain_font->Size() / 12.0f;
142 		ResizeTo(Frame().Width() * scaling, Frame().Height() * scaling);
143 		CenterOnScreen();
144 	}
145 
146 	// create GUI
147 	BMenuBar* menuBar = new BMenuBar("menu");
148 
149 	fFilterControl = new BTextControl(B_TRANSLATE("Filter:"), NULL, NULL);
150 	fFilterControl->SetModificationMessage(new BMessage(kMsgFilterChanged));
151 
152 	BButton* clearButton = new BButton("clear", B_TRANSLATE("Clear"),
153 		new BMessage(kMsgClearFilter));
154 
155 	fUnicodeBlockView = new UnicodeBlockView("unicodeBlocks");
156 	fUnicodeBlockView->SetSelectionMessage(
157 		new BMessage(kMsgUnicodeBlockSelected));
158 
159 	BScrollView* unicodeScroller = new BScrollView("unicodeScroller",
160 		fUnicodeBlockView, 0, false, true);
161 
162 	fCharacterView = new CharacterView("characters");
163 	fCharacterView->SetTarget(this, kMsgCharacterChanged);
164 
165 	fGlyphView = new BStringView("glyph", "");
166 	fGlyphView->SetExplicitMaxSize(BSize(B_SIZE_UNSET,
167 		fGlyphView->PreferredSize().Height()));
168 
169 	// TODO: have a context object shared by CharacterView/UnicodeBlockView
170 	bool show;
171 	if (settings.FindBool("show private blocks", &show) == B_OK) {
172 		fCharacterView->ShowPrivateBlocks(show);
173 		fUnicodeBlockView->ShowPrivateBlocks(show);
174 	}
175 	if (settings.FindBool("show contained blocks only", &show) == B_OK) {
176 		fCharacterView->ShowContainedBlocksOnly(show);
177 		fUnicodeBlockView->ShowContainedBlocksOnly(show);
178 	}
179 
180 	const char* family;
181 	const char* style;
182 	BString displayName;
183 
184 	if (settings.FindString("font family", &family) == B_OK
185 		&& settings.FindString("font style", &style) == B_OK) {
186 		_SetFont(family, style);
187 		displayName << family << " " << style;
188 	} else {
189 		font_family currentFontFamily;
190 		font_style currentFontStyle;
191 		fCharacterView->CharacterFont().GetFamilyAndStyle(&currentFontFamily,
192 			&currentFontStyle);
193 		displayName << currentFontFamily << " " << currentFontStyle;
194 	}
195 
196 	int32 fontSize;
197 	if (settings.FindInt32("font size", &fontSize) == B_OK) {
198 		BFont font = fCharacterView->CharacterFont();
199 		if (fontSize < kMinFontSize)
200 			fontSize = kMinFontSize;
201 		else if (fontSize > kMaxFontSize)
202 			fontSize = kMaxFontSize;
203 		font.SetSize(fontSize);
204 
205 		fCharacterView->SetCharacterFont(font);
206 		fUnicodeBlockView->SetCharacterFont(font);
207 	} else
208 		fontSize = (int32)fCharacterView->CharacterFont().Size();
209 
210 	BScrollView* characterScroller = new BScrollView("characterScroller",
211 		fCharacterView, 0, false, true);
212 
213 	fFontSizeSlider = new FontSizeSlider("fontSizeSlider",
214 		displayName,
215 		new BMessage(kMsgFontSizeChanged), kMinFontSize, kMaxFontSize);
216 	fFontSizeSlider->SetValue(fontSize);
217 
218 	fCodeView = new BStringView("code", "-");
219 	fCodeView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED,
220 		fCodeView->PreferredSize().Height()));
221 
222 	// Set minimum width for character pane to prevent UI
223 	// from jumping when longer code strings are displayed.
224 	// use 'w' character for sizing as it's likely the widest
225 	// character for a Latin font.  40 characters is a little
226 	// wider than needed so hopefully this covers other
227 	// non-Latin fonts that may be wider.
228 	BFont viewFont;
229 	fCodeView->GetFont(&viewFont);
230 	fCharacterView->SetExplicitMinSize(BSize(viewFont.StringWidth("w") * 40,
231 		B_SIZE_UNSET));
232 
233 	BLayoutBuilder::Group<>(this, B_VERTICAL, 0)
234 		.Add(menuBar)
235 		.AddGroup(B_HORIZONTAL)
236 			.SetInsets(B_USE_WINDOW_SPACING)
237 			.AddGroup(B_VERTICAL)
238 				.AddGroup(B_HORIZONTAL)
239 					.Add(fFilterControl)
240 					.Add(clearButton)
241 				.End()
242 				.Add(unicodeScroller)
243 			.End()
244 			.AddGroup(B_VERTICAL)
245 				.Add(characterScroller)
246 				.Add(fFontSizeSlider)
247 				.AddGroup(B_HORIZONTAL)
248 					.Add(fGlyphView)
249 					.Add(fCodeView);
250 
251 	// Add menu
252 
253 	// "File" menu
254 	BMenu* menu = new BMenu(B_TRANSLATE("File"));
255 	BMenuItem* item;
256 
257 	menu->AddItem(new BMenuItem(B_TRANSLATE("Quit"),
258 		new BMessage(B_QUIT_REQUESTED), 'Q'));
259 	menu->SetTargetForItems(this);
260 	menuBar->AddItem(menu);
261 
262 	menu = new BMenu(B_TRANSLATE("View"));
263 	menu->AddItem(item = new BMenuItem(B_TRANSLATE("Show private blocks"),
264 		new BMessage(kMsgPrivateBlocks)));
265 	item->SetMarked(fCharacterView->IsShowingPrivateBlocks());
266 
267 	menu->AddItem(item = new BMenuItem(
268 		B_TRANSLATE("Only show blocks contained in font"),
269 		new BMessage(kMsgContainedBlocks)));
270 	item->SetMarked(fCharacterView->IsShowingContainedBlocksOnly());
271 	menuBar->AddItem(menu);
272 
273 	fFontMenu = _CreateFontMenu();
274 	menuBar->AddItem(fFontMenu);
275 
276 	AddCommonFilter(new EscapeMessageFilter(kMsgClearFilter));
277 	AddCommonFilter(new RedirectUpAndDownFilter(fUnicodeBlockView));
278 
279 	// TODO: why is this needed?
280 	fUnicodeBlockView->SetTarget(this);
281 
282 	fFilterControl->MakeFocus();
283 
284 	fUnicodeBlockView->SelectBlockForCharacter(0);
285 }
286 
287 
288 CharacterWindow::~CharacterWindow()
289 {
290 }
291 
292 
293 void
294 CharacterWindow::MessageReceived(BMessage* message)
295 {
296 	if (message->WasDropped()) {
297 		const char* text;
298 		ssize_t size;
299 		uint32 c;
300 		if (message->FindInt32("character", (int32*)&c) == B_OK) {
301 			fCharacterView->ScrollToCharacter(c);
302 			return;
303 		} else if (message->FindData("text/plain", B_MIME_TYPE,
304 				(const void**)&text, &size) == B_OK) {
305 			fCharacterView->ScrollToCharacter(BUnicodeChar::FromUTF8(text));
306 			return;
307 		}
308 	}
309 
310 	switch (message->what) {
311 		case B_COPY:
312 			PostMessage(message, fCharacterView);
313 			break;
314 
315 		case kMsgUnicodeBlockSelected:
316 		{
317 			int32 index;
318 			if (message->FindInt32("index", &index) != B_OK
319 				|| index < 0)
320 				break;
321 
322 			BlockListItem* item
323 				= static_cast<BlockListItem*>(fUnicodeBlockView->ItemAt(index));
324 			fCharacterView->ScrollToBlock(item->BlockIndex());
325 
326 			fFilterControl->MakeFocus();
327 			break;
328 		}
329 
330 		case kMsgCharacterChanged:
331 		{
332 			uint32 character;
333 			if (message->FindInt32("character", (int32*)&character) != B_OK)
334 				break;
335 
336 			char utf8[16];
337 			CharacterView::UnicodeToUTF8(character, utf8, sizeof(utf8));
338 
339 			char utf8Hex[32];
340 			CharacterView::UnicodeToUTF8Hex(character, utf8Hex,
341 				sizeof(utf8Hex));
342 
343 			char text[128];
344 			snprintf(text, sizeof(text), " %s: %#" B_PRIx32 " (%" B_PRId32 "), UTF-8: %s",
345 				B_TRANSLATE("Code"), character, character, utf8Hex);
346 
347 			char glyph[20];
348 			snprintf(glyph, sizeof(glyph), "'%s'", utf8);
349 
350 			fGlyphView->SetText(glyph);
351 			fCodeView->SetText(text);
352 
353 			fUnicodeBlockView->SelectBlockForCharacter(character);
354 			break;
355 		}
356 
357 		case kMsgFontSelected:
358 		{
359 			BMenuItem* item;
360 
361 			if (message->FindPointer("source", (void**)&item) != B_OK)
362 				break;
363 
364 			fSelectedFontItem->SetMarked(false);
365 
366 			// If it's the family menu, just select the first style
367 			if (item->Submenu() != NULL) {
368 				item->SetMarked(true);
369 				item = item->Submenu()->ItemAt(0);
370 			}
371 
372 			if (item != NULL) {
373 				item->SetMarked(true);
374 				fSelectedFontItem = item;
375 
376 				_SetFont(item->Menu()->Name(), item->Label());
377 
378 				BString displayName;
379 				displayName << item->Menu()->Name() << " " << item->Label();
380 
381 				fFontSizeSlider->SetLabel(displayName);
382 
383 				item = item->Menu()->Superitem();
384 				item->SetMarked(true);
385 			}
386 			break;
387 		}
388 
389 		case kMsgFontSizeChanged:
390 		{
391 			int32 size = fFontSizeSlider->Value();
392 			if (size < kMinFontSize)
393 				size = kMinFontSize;
394 			else if (size > kMaxFontSize)
395 				size = kMaxFontSize;
396 
397 			BFont font = fCharacterView->CharacterFont();
398 			font.SetSize(size);
399 			fCharacterView->SetCharacterFont(font);
400 			fUnicodeBlockView->SetCharacterFont(font);
401 			break;
402 		}
403 
404 		case kMsgPrivateBlocks:
405 		{
406 			BMenuItem* item;
407 			if (message->FindPointer("source", (void**)&item) != B_OK
408 				|| item == NULL)
409 				break;
410 
411 			item->SetMarked(!item->IsMarked());
412 
413 			fCharacterView->ShowPrivateBlocks(item->IsMarked());
414 			fUnicodeBlockView->ShowPrivateBlocks(item->IsMarked());
415 			break;
416 		}
417 
418 		case kMsgContainedBlocks:
419 		{
420 			BMenuItem* item;
421 			if (message->FindPointer("source", (void**)&item) != B_OK
422 				|| item == NULL)
423 				break;
424 
425 			item->SetMarked(!item->IsMarked());
426 
427 			fCharacterView->ShowContainedBlocksOnly(item->IsMarked());
428 			fUnicodeBlockView->ShowContainedBlocksOnly(item->IsMarked());
429 			break;
430 		}
431 
432 		case kMsgFilterChanged:
433 			fUnicodeBlockView->SetFilter(fFilterControl->Text());
434 			fUnicodeBlockView->Select(0);
435 			break;
436 
437 		case kMsgClearFilter:
438 			fFilterControl->SetText("");
439 			fFilterControl->MakeFocus();
440 			break;
441 
442 		default:
443 			BWindow::MessageReceived(message);
444 			break;
445 	}
446 }
447 
448 
449 bool
450 CharacterWindow::QuitRequested()
451 {
452 	_SaveSettings();
453 	be_app->PostMessage(B_QUIT_REQUESTED);
454 	return true;
455 }
456 
457 
458 status_t
459 CharacterWindow::_OpenSettings(BFile& file, uint32 mode)
460 {
461 	BPath path;
462 	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK)
463 		return B_ERROR;
464 
465 	path.Append("CharacterMap settings");
466 
467 	return file.SetTo(path.Path(), mode);
468 }
469 
470 
471 status_t
472 CharacterWindow::_LoadSettings(BMessage& settings)
473 {
474 	BFile file;
475 	status_t status = _OpenSettings(file, B_READ_ONLY);
476 	if (status != B_OK)
477 		return status;
478 
479 	return settings.Unflatten(&file);
480 }
481 
482 
483 status_t
484 CharacterWindow::_SaveSettings()
485 {
486 	BFile file;
487 	status_t status = _OpenSettings(file, B_WRITE_ONLY | B_CREATE_FILE
488 		| B_ERASE_FILE);
489 	if (status < B_OK)
490 		return status;
491 
492 	BMessage settings('chrm');
493 	status = settings.AddRect("window frame", Frame());
494 	if (status != B_OK)
495 		return status;
496 
497 	if (status == B_OK) {
498 		status = settings.AddBool("show private blocks",
499 			fCharacterView->IsShowingPrivateBlocks());
500 	}
501 	if (status == B_OK) {
502 		status = settings.AddBool("show contained blocks only",
503 			fCharacterView->IsShowingContainedBlocksOnly());
504 	}
505 
506 	if (status == B_OK) {
507 		BFont font = fCharacterView->CharacterFont();
508 		status = settings.AddInt32("font size", font.Size());
509 
510 		font_family family;
511 		font_style style;
512 		if (status == B_OK)
513 			font.GetFamilyAndStyle(&family, &style);
514 		if (status == B_OK)
515 			status = settings.AddString("font family", family);
516 		if (status == B_OK)
517 			status = settings.AddString("font style", style);
518 	}
519 
520 	if (status == B_OK)
521 		status = settings.Flatten(&file);
522 
523 	return status;
524 }
525 
526 
527 void
528 CharacterWindow::_SetFont(const char* family, const char* style)
529 {
530 	BFont font = fCharacterView->CharacterFont();
531 	font.SetFamilyAndStyle(family, style);
532 
533 	fCharacterView->SetCharacterFont(font);
534 	fUnicodeBlockView->SetCharacterFont(font);
535 	fGlyphView->SetFont(&font, B_FONT_FAMILY_AND_STYLE);
536 }
537 
538 
539 BMenu*
540 CharacterWindow::_CreateFontMenu()
541 {
542 	BMenu* menu = new BMenu(B_TRANSLATE("Font"));
543 	_UpdateFontMenu(menu);
544 
545 	return menu;
546 }
547 
548 
549 void
550 CharacterWindow::_UpdateFontMenu(BMenu* menu)
551 {
552 	BMenuItem* item;
553 
554 	while (menu->CountItems() > 0) {
555 		item = menu->RemoveItem(static_cast<int32>(0));
556 		delete(item);
557 	}
558 
559 	font_family currentFamily;
560 	font_style currentStyle;
561 	fCharacterView->CharacterFont().GetFamilyAndStyle(&currentFamily,
562 		&currentStyle);
563 
564 	int32 numFamilies = count_font_families();
565 
566 	menu->SetRadioMode(true);
567 
568 	for (int32 i = 0; i < numFamilies; i++) {
569 		font_family family;
570 		if (get_font_family(i, &family) == B_OK) {
571 			BMenu* subMenu = new BMenu(family);
572 			menu->AddItem(new BMenuItem(subMenu,
573 				new BMessage(kMsgFontSelected)));
574 
575 			int numStyles = count_font_styles(family);
576 			for (int32 j = 0; j < numStyles; j++) {
577 				font_style style;
578 				uint32 flags;
579 				if (get_font_style(family, j, &style, &flags) == B_OK) {
580 					item = new BMenuItem(style, new BMessage(kMsgFontSelected));
581 					subMenu->AddItem(item);
582 
583 					if (!strcmp(family, currentFamily)
584 						&& !strcmp(style, currentStyle)) {
585 						fSelectedFontItem = item;
586 						item->SetMarked(true);
587 					}
588 				}
589 			}
590 		}
591 	}
592 
593 	item = menu->FindItem(currentFamily);
594 	item->SetMarked(true);
595 }
596 
597 
598 void
599 CharacterWindow::MenusBeginning()
600 {
601 	if (update_font_families(false) == true)
602 		_UpdateFontMenu(fFontMenu);
603 }
604