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