xref: /haiku/src/apps/firstbootprompt/BootPromptWindow.cpp (revision 68ea01249e1e2088933cb12f9c28d4e5c5d1c9ef)
1 /*
2  * Copyright 2010, Stephan Aßmus <superstippi@gmx.de>
3  * Copyright 2010, Adrien Destugues <pulkomandy@pulkomandy.ath.cx>
4  * Copyright 2011, Axel Dörfler, axeld@pinc-software.de.
5  * All rights reserved. Distributed under the terms of the MIT License.
6  */
7 
8 
9 #include "BootPromptWindow.h"
10 
11 #include <new>
12 #include <stdio.h>
13 
14 #include <Bitmap.h>
15 #include <Button.h>
16 #include <Catalog.h>
17 #include <ControlLook.h>
18 #include <Directory.h>
19 #include <Entry.h>
20 #include <Font.h>
21 #include <FindDirectory.h>
22 #include <File.h>
23 #include <FormattingConventions.h>
24 #include <IconUtils.h>
25 #include <IconView.h>
26 #include <LayoutBuilder.h>
27 #include <ListView.h>
28 #include <Locale.h>
29 #include <Menu.h>
30 #include <MutableLocaleRoster.h>
31 #include <ObjectList.h>
32 #include <Path.h>
33 #include <Screen.h>
34 #include <ScrollView.h>
35 #include <SeparatorView.h>
36 #include <StringItem.h>
37 #include <StringView.h>
38 #include <TextView.h>
39 #include <UnicodeChar.h>
40 
41 #include "BootPrompt.h"
42 #include "Keymap.h"
43 #include "KeymapNames.h"
44 
45 
46 using BPrivate::MutableLocaleRoster;
47 
48 
49 enum {
50 	MSG_LANGUAGE_SELECTED	= 'lngs',
51 	MSG_KEYMAP_SELECTED		= 'kmps'
52 };
53 
54 
55 #undef B_TRANSLATION_CONTEXT
56 #define B_TRANSLATION_CONTEXT "BootPromptWindow"
57 
58 
59 namespace BPrivate {
60 	void ForceUnloadCatalog();
61 };
62 
63 
64 static const char* kLanguageKeymapMappings[] = {
65 	// While there is a "Dutch" keymap, it apparently has not been widely
66 	// adopted, and the US-International keymap is common
67 	"Dutch", "US-International",
68 
69 	// Cyrillic keymaps are not usable alone, as latin alphabet is required to
70 	// use Terminal. So we stay in US international until the user has a chance
71 	// to set up KeymapSwitcher.
72 	"Belarusian", "US-International",
73 	"Russian", "US-International",
74 	"Ukrainian", "US-International",
75 
76 	// Turkish has two layouts, we must pick one
77 	"Turkish", "Turkish (Type-Q)",
78 };
79 static const size_t kLanguageKeymapMappingsSize
80 	= sizeof(kLanguageKeymapMappings) / sizeof(kLanguageKeymapMappings[0]);
81 
82 
83 class LanguageItem : public BStringItem {
84 public:
85 	LanguageItem(const char* label, const char* language)
86 		:
87 		BStringItem(label),
88 		fLanguage(language)
89 	{
90 	}
91 
92 	~LanguageItem()
93 	{
94 	}
95 
96 	const char* Language() const
97 	{
98 		return fLanguage.String();
99 	}
100 
101 	void DrawItem(BView* owner, BRect frame, bool complete)
102 	{
103 		BStringItem::DrawItem(owner, frame, true/*complete*/);
104 	}
105 
106 private:
107 			BString				fLanguage;
108 };
109 
110 
111 static int
112 compare_void_list_items(const void* _a, const void* _b)
113 {
114 	static BCollator collator;
115 
116 	LanguageItem* a = *(LanguageItem**)_a;
117 	LanguageItem* b = *(LanguageItem**)_b;
118 
119 	return collator.Compare(a->Text(), b->Text());
120 }
121 
122 
123 static int
124 compare_void_menu_items(const void* _a, const void* _b)
125 {
126 	static BCollator collator;
127 
128 	BMenuItem* a = *(BMenuItem**)_a;
129 	BMenuItem* b = *(BMenuItem**)_b;
130 
131 	return collator.Compare(a->Label(), b->Label());
132 }
133 
134 
135 // #pragma mark -
136 
137 
138 BootPromptWindow::BootPromptWindow()
139 	:
140 	BWindow(BRect(0, 0, 530, 400), "",
141 		B_TITLED_WINDOW, B_NOT_ZOOMABLE | B_NOT_MINIMIZABLE | B_NOT_CLOSABLE
142 			| B_NOT_RESIZABLE | B_AUTO_UPDATE_SIZE_LIMITS,
143 		B_ALL_WORKSPACES),
144 	fDefaultKeymapItem(NULL)
145 {
146 	SetSizeLimits(450, 16384, 350, 16384);
147 
148 	rgb_color textColor = ui_color(B_PANEL_TEXT_COLOR);
149 	fInfoTextView = new BTextView("");
150 	fInfoTextView->SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
151 	fInfoTextView->SetFontAndColor(be_plain_font, B_FONT_ALL, &textColor);
152 	fInfoTextView->MakeEditable(false);
153 	fInfoTextView->MakeSelectable(false);
154 	fInfoTextView->MakeResizable(false);
155 
156 	BResources* res = BApplication::AppResources();
157 	size_t size = 0;
158 	const uint8_t* data;
159 
160 	BBitmap desktopIcon(BRect(0, 0, 23, 23), B_RGBA32);
161 	data = (const uint8_t*)res->LoadResource('VICN', "Desktop", &size);
162 	BIconUtils::GetVectorIcon(data, size, &desktopIcon);
163 
164 	BBitmap installerIcon(BRect(0, 0, 23, 23), B_RGBA32);
165 	data = (const uint8_t*)res->LoadResource('VICN', "Installer", &size);
166 	BIconUtils::GetVectorIcon(data, size, &installerIcon);
167 
168 	fDesktopButton = new BButton("", new BMessage(MSG_BOOT_DESKTOP));
169 	fDesktopButton->SetTarget(be_app);
170 	fDesktopButton->MakeDefault(true);
171 	fDesktopButton->SetIcon(&desktopIcon);
172 
173 	fInstallerButton = new BButton("", new BMessage(MSG_RUN_INSTALLER));
174 	fInstallerButton->SetTarget(be_app);
175 	fInstallerButton->SetIcon(&installerIcon);
176 
177 	data = (const uint8_t*)res->LoadResource('VICN', "Language", &size);
178 	IconView* languageIcon = new IconView(B_LARGE_ICON);
179 	languageIcon->SetIcon(data, size, B_LARGE_ICON);
180 
181 	data = (const uint8_t*)res->LoadResource('VICN', "Keymap", &size);
182 	IconView* keymapIcon = new IconView(B_LARGE_ICON);
183 	keymapIcon->SetIcon(data, size, B_LARGE_ICON);
184 
185 	fLanguagesLabelView = new BStringView("languagesLabel", "");
186 	fLanguagesLabelView->SetFont(be_bold_font);
187 	fLanguagesLabelView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED,
188 		B_SIZE_UNSET));
189 
190 	fKeymapsMenuLabel = new BStringView("keymapsLabel", "");
191 	fKeymapsMenuLabel->SetFont(be_bold_font);
192 	fKeymapsMenuLabel->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED,
193 		B_SIZE_UNSET));
194 	// Make sure there is enough space to display the text even in verbose
195 	// locales, to avoid width changes on language changes
196 	float labelWidth = fKeymapsMenuLabel->StringWidth("Disposition du clavier")
197 		+ 16;
198 	fKeymapsMenuLabel->SetExplicitMinSize(BSize(labelWidth, B_SIZE_UNSET));
199 
200 	fLanguagesListView = new BListView();
201 	BScrollView* languagesScrollView = new BScrollView("languagesScroll",
202 		fLanguagesListView, B_WILL_DRAW, false, true);
203 
204 	// Carefully designed to not exceed the 640x480 resolution with a 12pt font.
205 	float width = 640 * be_plain_font->Size() / 12 - (labelWidth + 64);
206 	float height = be_plain_font->Size() * 23;
207 	fInfoTextView->SetExplicitMinSize(BSize(width, height));
208 	fInfoTextView->SetExplicitMaxSize(BSize(width, B_SIZE_UNSET));
209 
210 	// Make sure the language list view is always wide enough to show the
211 	// largest language
212 	fLanguagesListView->SetExplicitMinSize(
213 		BSize(fLanguagesListView->StringWidth("Português (Brasil)"),
214 		height));
215 
216 	fKeymapsMenuField = new BMenuField("", "", new BMenu(""));
217 	fKeymapsMenuField->Menu()->SetLabelFromMarked(true);
218 
219 	_InitCatalog(true);
220 	_PopulateLanguages();
221 	_PopulateKeymaps();
222 
223 	BLayoutBuilder::Group<>(this, B_HORIZONTAL)
224 		.SetInsets(B_USE_WINDOW_SPACING)
225 		.AddGroup(B_VERTICAL, 0)
226 			.SetInsets(0, 0, 0, B_USE_SMALL_SPACING)
227 			.AddGroup(B_HORIZONTAL)
228 				.Add(languageIcon)
229 				.Add(fLanguagesLabelView)
230 				.SetInsets(0, 0, 0, B_USE_SMALL_SPACING)
231 			.End()
232 			.Add(languagesScrollView)
233 			.AddGroup(B_HORIZONTAL)
234 				.Add(keymapIcon)
235 				.Add(fKeymapsMenuLabel)
236 				.SetInsets(0, B_USE_DEFAULT_SPACING, 0,
237 					B_USE_SMALL_SPACING)
238 			.End()
239 			.Add(fKeymapsMenuField)
240 		.End()
241 		.AddGroup(B_VERTICAL)
242 			.SetInsets(0)
243 			.Add(fInfoTextView)
244 			.AddGroup(B_HORIZONTAL)
245 				.SetInsets(0)
246 				.AddGlue()
247 				.Add(fInstallerButton)
248 				.Add(fDesktopButton)
249 			.End()
250 		.End();
251 
252 	fLanguagesListView->MakeFocus();
253 
254 	// Force the info text view to use a reasonable size
255 	fInfoTextView->SetText("x\n\n\n\n\n\n\n\n\n\n\n\n\n\nx");
256 	ResizeToPreferred();
257 
258 	_UpdateStrings();
259 	CenterOnScreen();
260 	Show();
261 }
262 
263 
264 void
265 BootPromptWindow::MessageReceived(BMessage* message)
266 {
267 	switch (message->what) {
268 		case MSG_LANGUAGE_SELECTED:
269 			if (LanguageItem* item = static_cast<LanguageItem*>(
270 					fLanguagesListView->ItemAt(
271 						fLanguagesListView->CurrentSelection(0)))) {
272 				BMessage preferredLanguages;
273 				preferredLanguages.AddString("language", item->Language());
274 				MutableLocaleRoster::Default()->SetPreferredLanguages(
275 					&preferredLanguages);
276 				_InitCatalog(true);
277 				_UpdateKeymapsMenu();
278 
279 				// Select default keymap by language
280 				BLanguage language(item->Language());
281 				BMenuItem* keymapItem = _KeymapItemForLanguage(language);
282 				if (keymapItem != NULL) {
283 					keymapItem->SetMarked(true);
284 					_ActivateKeymap(keymapItem->Message());
285 				}
286 			}
287 			// Calling it here is a cheap way of preventing the user to have
288 			// no item selected. Always the current item will be selected.
289 			_UpdateStrings();
290 			break;
291 
292 		case MSG_KEYMAP_SELECTED:
293 			_ActivateKeymap(message);
294 			break;
295 
296 		default:
297 			BWindow::MessageReceived(message);
298 	}
299 }
300 
301 
302 void
303 BootPromptWindow::_InitCatalog(bool saveSettings)
304 {
305 	// Initilialize the Locale Kit
306 	BPrivate::ForceUnloadCatalog();
307 
308 	if (!saveSettings)
309 		return;
310 
311 	BMessage settings;
312 	BString language;
313 	if (BLocaleRoster::Default()->GetCatalog()->GetLanguage(&language) == B_OK)
314 		settings.AddString("language", language.String());
315 
316 	MutableLocaleRoster::Default()->SetPreferredLanguages(&settings);
317 
318 	BFormattingConventions conventions(language.String());
319 	MutableLocaleRoster::Default()->SetDefaultFormattingConventions(
320 		conventions);
321 }
322 
323 
324 void
325 BootPromptWindow::_UpdateStrings()
326 {
327 	SetTitle(B_TRANSLATE("Welcome to Haiku!"));
328 
329 	fInfoTextView->SetText(B_TRANSLATE_COMMENT(
330 		"Thank you for trying out Haiku! We hope you'll like it!\n\n"
331 		"You can select your preferred language and keyboard "
332 		"layout from the list on the left which will then be used instantly. "
333 		"You can easily change both settings from the Desktop later on on "
334 		"the fly.\n\n"
335 
336 		"Do you wish to run the Installer or continue booting to the "
337 		"Desktop?\n",
338 
339 		"For other languages, a note could be added: \""
340 		"Note: Localization of Haiku applications and other components is "
341 		"an on-going effort. You will frequently encounter untranslated "
342 		"strings, but if you like, you can join in the work at "
343 		"<www.haiku-os.org>.\""));
344 
345 	fDesktopButton->SetLabel(B_TRANSLATE("Boot to Desktop"));
346 	fInstallerButton->SetLabel(B_TRANSLATE("Run Installer"));
347 
348 	fLanguagesLabelView->SetText(B_TRANSLATE("Language"));
349 	fKeymapsMenuLabel->SetText(B_TRANSLATE("Keymap"));
350 	if (fKeymapsMenuField->Menu()->FindMarked() == NULL)
351 		fKeymapsMenuField->MenuItem()->SetLabel(B_TRANSLATE("Custom"));
352 }
353 
354 
355 void
356 BootPromptWindow::_PopulateLanguages()
357 {
358 	// TODO: detect language/country from IP address
359 
360 	// Get current first preferred language of the user
361 	BMessage preferredLanguages;
362 	BLocaleRoster::Default()->GetPreferredLanguages(&preferredLanguages);
363 	const char* firstPreferredLanguage;
364 	if (preferredLanguages.FindString("language", &firstPreferredLanguage)
365 			!= B_OK) {
366 		// Fall back to built-in language of this application.
367 		firstPreferredLanguage = "en";
368 	}
369 
370 	BMessage installedCatalogs;
371 	BLocaleRoster::Default()->GetAvailableCatalogs(&installedCatalogs,
372 		"x-vnd.Haiku-FirstBootPrompt");
373 
374 	BFont font;
375 	fLanguagesListView->GetFont(&font);
376 
377 	// Try to instantiate a BCatalog for each language, it will only work
378 	// for translations of this application. So the list of languages will be
379 	// limited to catalogs written for this application, which is on purpose!
380 
381 	const char* languageID;
382 	LanguageItem* currentItem = NULL;
383 	for (int32 i = 0; installedCatalogs.FindString("language", i, &languageID)
384 			== B_OK; i++) {
385 		BLanguage* language;
386 		if (BLocaleRoster::Default()->GetLanguage(languageID, &language)
387 				== B_OK) {
388 			BString name;
389 			language->GetNativeName(name);
390 
391 			// TODO: the following block fails to detect a couple of language
392 			// names as containing glyphs we can't render. Why's that?
393 			bool hasGlyphs[name.CountChars()];
394 			font.GetHasGlyphs(name.String(), name.CountChars(), hasGlyphs);
395 			for (int32 i = 0; i < name.CountChars(); ++i) {
396 				if (!hasGlyphs[i]) {
397 					// replace by name translated to current language
398 					language->GetName(name);
399 					break;
400 				}
401 			}
402 
403 			LanguageItem* item = new LanguageItem(name.String(),
404 				languageID);
405 			fLanguagesListView->AddItem(item);
406 			// Select this item if it is the first preferred language
407 			if (strcmp(firstPreferredLanguage, languageID) == 0)
408 				currentItem = item;
409 
410 			delete language;
411 		} else
412 			fprintf(stderr, "failed to get BLanguage for %s\n", languageID);
413 	}
414 
415 	fLanguagesListView->SortItems(compare_void_list_items);
416 	if (currentItem != NULL)
417 		fLanguagesListView->Select(fLanguagesListView->IndexOf(currentItem));
418 	fLanguagesListView->ScrollToSelection();
419 
420 	// Re-enable sending the selection message.
421 	fLanguagesListView->SetSelectionMessage(
422 		new BMessage(MSG_LANGUAGE_SELECTED));
423 }
424 
425 
426 void
427 BootPromptWindow::_UpdateKeymapsMenu()
428 {
429 	BMenu *menu = fKeymapsMenuField->Menu();
430 	BMenuItem* item;
431 	BList itemsList;
432 
433 	// Recreate keymapmenu items list, since BMenu could not sort its items.
434 	while ((item = menu->ItemAt(0)) != NULL) {
435 		BMessage* message = item->Message();
436 		entry_ref ref;
437 		message->FindRef("ref", &ref);
438 		item-> SetLabel(B_TRANSLATE_NOCOLLECT_ALL((ref.name),
439 		"KeymapNames", NULL));
440 		itemsList.AddItem(item);
441 		menu->RemoveItem((int32)0);
442 	}
443 	itemsList.SortItems(compare_void_menu_items);
444 	fKeymapsMenuField->Menu()->AddList(&itemsList, 0);
445 }
446 
447 
448 void
449 BootPromptWindow::_PopulateKeymaps()
450 {
451 	// Get the name of the current keymap, so we can mark the correct entry
452 	// in the list view.
453 	BString currentName;
454 	entry_ref currentRef;
455 	if (_GetCurrentKeymapRef(currentRef) == B_OK) {
456 		BNode node(&currentRef);
457 		node.ReadAttrString("keymap:name", &currentName);
458 	}
459 
460 	// TODO: common keymaps!
461 	BPath path;
462 	if (find_directory(B_SYSTEM_DATA_DIRECTORY, &path) != B_OK
463 		|| path.Append("Keymaps") != B_OK) {
464 		return;
465 	}
466 
467 	// US-International is the default keymap, if we could not found a
468 	// matching one
469 	BString usInternational("US-International");
470 
471 	// Populate the menu
472 	BDirectory directory;
473 	if (directory.SetTo(path.Path()) == B_OK) {
474 		entry_ref ref;
475 		BList itemsList;
476 		while (directory.GetNextRef(&ref) == B_OK) {
477 			BMessage* message = new BMessage(MSG_KEYMAP_SELECTED);
478 			message->AddRef("ref", &ref);
479 			BMenuItem* item =
480 				new BMenuItem(B_TRANSLATE_NOCOLLECT_ALL((ref.name),
481 				"KeymapNames", NULL), message);
482 			itemsList.AddItem(item);
483 			if (currentName == ref.name)
484 				item->SetMarked(true);
485 
486 			if (usInternational == ref.name)
487 				fDefaultKeymapItem = item;
488 		}
489 		itemsList.SortItems(compare_void_menu_items);
490 		fKeymapsMenuField->Menu()->AddList(&itemsList, 0);
491 	}
492 }
493 
494 
495 void
496 BootPromptWindow::_ActivateKeymap(const BMessage* message) const
497 {
498 	entry_ref ref;
499 	if (message == NULL || message->FindRef("ref", &ref) != B_OK)
500 		return;
501 
502 	// Load and use the new keymap
503 	Keymap keymap;
504 	if (keymap.Load(ref) != B_OK) {
505 		fprintf(stderr, "Failed to load new keymap file (%s).\n", ref.name);
506 		return;
507 	}
508 
509 	// Get entry_ref to the Key_map file in the user settings.
510 	entry_ref currentRef;
511 	if (_GetCurrentKeymapRef(currentRef) != B_OK) {
512 		fprintf(stderr, "Failed to get ref to user keymap file.\n");
513 		return;
514 	}
515 
516 	if (keymap.Save(currentRef) != B_OK) {
517 		fprintf(stderr, "Failed to save new keymap file (%s).\n", ref.name);
518 		return;
519 	}
520 
521 	keymap.Use();
522 }
523 
524 
525 status_t
526 BootPromptWindow::_GetCurrentKeymapRef(entry_ref& ref) const
527 {
528 	BPath path;
529 	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK
530 		|| path.Append("Key_map") != B_OK) {
531 		return B_ERROR;
532 	}
533 
534 	return get_ref_for_path(path.Path(), &ref);
535 }
536 
537 
538 BMenuItem*
539 BootPromptWindow::_KeymapItemForLanguage(BLanguage& language) const
540 {
541 	BLanguage english("en");
542 	BString name;
543 	if (language.GetName(name, &english) != B_OK)
544 		return fDefaultKeymapItem;
545 
546 	// Check special mappings first
547 	for (size_t i = 0; i < kLanguageKeymapMappingsSize; i += 2) {
548 		if (!strcmp(name, kLanguageKeymapMappings[i])) {
549 			name = kLanguageKeymapMappings[i + 1];
550 			break;
551 		}
552 	}
553 
554 	BMenu* menu = fKeymapsMenuField->Menu();
555 	for (int32 i = 0; i < menu->CountItems(); i++) {
556 		BMenuItem* item = menu->ItemAt(i);
557 		BMessage* message = item->Message();
558 
559 		entry_ref ref;
560 		if (message->FindRef("ref", &ref) == B_OK
561 			&& name == ref.name)
562 			return item;
563 	}
564 
565 	return fDefaultKeymapItem;
566 }
567