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