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