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