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