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->ShowPrivateBlocks(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	} else
206		fontSize = (int32)fCharacterView->CharacterFont().Size();
207
208	BScrollView* characterScroller = new BScrollView("characterScroller",
209		fCharacterView, 0, false, true);
210
211	fFontSizeSlider = new FontSizeSlider("fontSizeSlider",
212		displayName,
213		new BMessage(kMsgFontSizeChanged), kMinFontSize, kMaxFontSize);
214	fFontSizeSlider->SetValue(fontSize);
215
216	fCodeView = new BStringView("code", "-");
217	fCodeView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED,
218		fCodeView->PreferredSize().Height()));
219
220	// Set minimum width for character pane to prevent UI
221	// from jumping when longer code strings are displayed.
222	// use 'w' character for sizing as it's likely the widest
223	// character for a Latin font.  40 characters is a little
224	// wider than needed so hopefully this covers other
225	// non-Latin fonts that may be wider.
226	BFont viewFont;
227	fCodeView->GetFont(&viewFont);
228	fCharacterView->SetExplicitMinSize(BSize(viewFont.StringWidth("w") * 40,
229		B_SIZE_UNSET));
230
231	BLayoutBuilder::Group<>(this, B_VERTICAL, 0)
232		.Add(menuBar)
233		.AddGroup(B_HORIZONTAL)
234			.SetInsets(B_USE_WINDOW_SPACING)
235			.AddGroup(B_VERTICAL)
236				.AddGroup(B_HORIZONTAL)
237					.Add(fFilterControl)
238					.Add(clearButton)
239				.End()
240				.Add(unicodeScroller)
241			.End()
242			.AddGroup(B_VERTICAL)
243				.Add(characterScroller)
244				.Add(fFontSizeSlider)
245				.AddGroup(B_HORIZONTAL)
246					.Add(fGlyphView)
247					.Add(fCodeView);
248
249	// Add menu
250
251	// "File" menu
252	BMenu* menu = new BMenu(B_TRANSLATE("File"));
253	BMenuItem* item;
254
255	menu->AddItem(new BMenuItem(B_TRANSLATE("Quit"),
256		new BMessage(B_QUIT_REQUESTED), 'Q'));
257	menu->SetTargetForItems(this);
258	menuBar->AddItem(menu);
259
260	menu = new BMenu(B_TRANSLATE("View"));
261	menu->AddItem(item = new BMenuItem(B_TRANSLATE("Show private blocks"),
262		new BMessage(kMsgPrivateBlocks)));
263	item->SetMarked(fCharacterView->IsShowingPrivateBlocks());
264// TODO: this feature is not yet supported by Haiku!
265#if 0
266	menu->AddItem(item = new BMenuItem("Only show blocks contained in font",
267		new BMessage(kMsgContainedBlocks)));
268	item->SetMarked(fCharacterView->IsShowingContainedBlocksOnly());
269#endif
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			break;
400		}
401
402		case kMsgPrivateBlocks:
403		{
404			BMenuItem* item;
405			if (message->FindPointer("source", (void**)&item) != B_OK
406				|| item == NULL)
407				break;
408
409			item->SetMarked(!item->IsMarked());
410
411			fCharacterView->ShowPrivateBlocks(item->IsMarked());
412			fUnicodeBlockView->ShowPrivateBlocks(item->IsMarked());
413			break;
414		}
415
416		case kMsgContainedBlocks:
417		{
418			BMenuItem* item;
419			if (message->FindPointer("source", (void**)&item) != B_OK
420				|| item == NULL)
421				break;
422
423			item->SetMarked(!item->IsMarked());
424
425			fCharacterView->ShowContainedBlocksOnly(item->IsMarked());
426			fUnicodeBlockView->ShowContainedBlocksOnly(item->IsMarked());
427			break;
428		}
429
430		case kMsgFilterChanged:
431			fUnicodeBlockView->SetFilter(fFilterControl->Text());
432			fUnicodeBlockView->Select(0);
433			break;
434
435		case kMsgClearFilter:
436			fFilterControl->SetText("");
437			fFilterControl->MakeFocus();
438			break;
439
440		default:
441			BWindow::MessageReceived(message);
442			break;
443	}
444}
445
446
447bool
448CharacterWindow::QuitRequested()
449{
450	_SaveSettings();
451	be_app->PostMessage(B_QUIT_REQUESTED);
452	return true;
453}
454
455
456status_t
457CharacterWindow::_OpenSettings(BFile& file, uint32 mode)
458{
459	BPath path;
460	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK)
461		return B_ERROR;
462
463	path.Append("CharacterMap settings");
464
465	return file.SetTo(path.Path(), mode);
466}
467
468
469status_t
470CharacterWindow::_LoadSettings(BMessage& settings)
471{
472	BFile file;
473	status_t status = _OpenSettings(file, B_READ_ONLY);
474	if (status != B_OK)
475		return status;
476
477	return settings.Unflatten(&file);
478}
479
480
481status_t
482CharacterWindow::_SaveSettings()
483{
484	BFile file;
485	status_t status = _OpenSettings(file, B_WRITE_ONLY | B_CREATE_FILE
486		| B_ERASE_FILE);
487	if (status < B_OK)
488		return status;
489
490	BMessage settings('chrm');
491	status = settings.AddRect("window frame", Frame());
492	if (status != B_OK)
493		return status;
494
495	if (status == B_OK) {
496		status = settings.AddBool("show private blocks",
497			fCharacterView->IsShowingPrivateBlocks());
498	}
499	if (status == B_OK) {
500		status = settings.AddBool("show contained blocks only",
501			fCharacterView->IsShowingContainedBlocksOnly());
502	}
503
504	if (status == B_OK) {
505		BFont font = fCharacterView->CharacterFont();
506		status = settings.AddInt32("font size", font.Size());
507
508		font_family family;
509		font_style style;
510		if (status == B_OK)
511			font.GetFamilyAndStyle(&family, &style);
512		if (status == B_OK)
513			status = settings.AddString("font family", family);
514		if (status == B_OK)
515			status = settings.AddString("font style", style);
516	}
517
518	if (status == B_OK)
519		status = settings.Flatten(&file);
520
521	return status;
522}
523
524
525void
526CharacterWindow::_SetFont(const char* family, const char* style)
527{
528	BFont font = fCharacterView->CharacterFont();
529	font.SetFamilyAndStyle(family, style);
530
531	fCharacterView->SetCharacterFont(font);
532	fGlyphView->SetFont(&font, B_FONT_FAMILY_AND_STYLE);
533}
534
535
536BMenu*
537CharacterWindow::_CreateFontMenu()
538{
539	BMenu* menu = new BMenu(B_TRANSLATE("Font"));
540	_UpdateFontMenu(menu);
541
542	return menu;
543}
544
545
546void
547CharacterWindow::_UpdateFontMenu(BMenu* menu)
548{
549	BMenuItem* item;
550
551	while (menu->CountItems() > 0) {
552		item = menu->RemoveItem(static_cast<int32>(0));
553		delete(item);
554	}
555
556	font_family currentFamily;
557	font_style currentStyle;
558	fCharacterView->CharacterFont().GetFamilyAndStyle(&currentFamily,
559		&currentStyle);
560
561	int32 numFamilies = count_font_families();
562
563	menu->SetRadioMode(true);
564
565	for (int32 i = 0; i < numFamilies; i++) {
566		font_family family;
567		if (get_font_family(i, &family) == B_OK) {
568			BMenu* subMenu = new BMenu(family);
569			menu->AddItem(new BMenuItem(subMenu,
570				new BMessage(kMsgFontSelected)));
571
572			int numStyles = count_font_styles(family);
573			for (int32 j = 0; j < numStyles; j++) {
574				font_style style;
575				uint32 flags;
576				if (get_font_style(family, j, &style, &flags) == B_OK) {
577					item = new BMenuItem(style, new BMessage(kMsgFontSelected));
578					subMenu->AddItem(item);
579
580					if (!strcmp(family, currentFamily)
581						&& !strcmp(style, currentStyle)) {
582						fSelectedFontItem = item;
583						item->SetMarked(true);
584					}
585				}
586			}
587		}
588	}
589
590	item = menu->FindItem(currentFamily);
591	item->SetMarked(true);
592}
593
594
595void
596CharacterWindow::MenusBeginning()
597{
598	if (update_font_families(false) == true)
599		_UpdateFontMenu(fFontMenu);
600}
601