xref: /haiku/src/apps/firstbootprompt/BootPromptWindow.cpp (revision 73254051b196497dfee9ab89eb0c2f60cc305819)
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 	_UpdateStrings();
219 	CenterOnScreen();
220 	Show();
221 }
222 
223 
224 void
225 BootPromptWindow::MessageReceived(BMessage* message)
226 {
227 	switch (message->what) {
228 		case MSG_LANGUAGE_SELECTED:
229 			if (LanguageItem* item = static_cast<LanguageItem*>(
230 					fLanguagesListView->ItemAt(
231 						fLanguagesListView->CurrentSelection(0)))) {
232 				BMessage preferredLanguages;
233 				preferredLanguages.AddString("language", item->Language());
234 				MutableLocaleRoster::Default()->SetPreferredLanguages(
235 					&preferredLanguages);
236 				_InitCatalog(true);
237 
238 				// Select default keymap by language
239 				BLanguage language(item->Language());
240 				BMenuItem* keymapItem = _KeymapItemForLanguage(language);
241 				if (keymapItem != NULL) {
242 					keymapItem->SetMarked(true);
243 					_ActivateKeymap(keymapItem->Message());
244 				}
245 			}
246 			// Calling it here is a cheap way of preventing the user to have
247 			// no item selected. Always the current item will be selected.
248 			_UpdateStrings();
249 			break;
250 
251 		case MSG_KEYMAP_SELECTED:
252 			_ActivateKeymap(message);
253 			break;
254 
255 		default:
256 			BWindow::MessageReceived(message);
257 	}
258 }
259 
260 
261 void
262 BootPromptWindow::_InitCatalog(bool saveSettings)
263 {
264 	// Initilialize the Locale Kit
265 	BPrivate::ForceUnloadCatalog();
266 
267 	if (!saveSettings)
268 		return;
269 
270 	BMessage settings;
271 	BString language;
272 	if (BLocaleRoster::Default()->GetCatalog()->GetLanguage(&language) == B_OK)
273 		settings.AddString("language", language.String());
274 
275 	MutableLocaleRoster::Default()->SetPreferredLanguages(&settings);
276 
277 	BFormattingConventions conventions(language.String());
278 	MutableLocaleRoster::Default()->SetDefaultFormattingConventions(
279 		conventions);
280 }
281 
282 
283 void
284 BootPromptWindow::_UpdateStrings()
285 {
286 	SetTitle(B_TRANSLATE("Welcome to Haiku!"));
287 
288 	fInfoTextView->SetText(B_TRANSLATE_COMMENT(
289 		"Thank you for trying out Haiku! We hope you'll like it!\n\n"
290 		"You can select your preferred language and keyboard "
291 		"layout from the list on the left which will then be used instantly. "
292 		"You can easily change both settings from the Desktop later on on "
293 		"the fly.\n\n"
294 
295 		"Do you wish to run the Installer or continue booting to the "
296 		"Desktop?\n",
297 
298 		"For other languages, a note could be added: \""
299 		"Note: Localization of Haiku applications and other components is "
300 		"an on-going effort. You will frequently encounter untranslated "
301 		"strings, but if you like, you can join in the work at "
302 		"<www.haiku-os.org>.\""));
303 
304 	fDesktopButton->SetLabel(B_TRANSLATE("Boot to Desktop"));
305 	fInstallerButton->SetLabel(B_TRANSLATE("Run Installer"));
306 
307 	fLanguagesLabelView->SetText(B_TRANSLATE("Language"));
308 	fKeymapsMenuField->SetLabel(B_TRANSLATE("Keymap"));
309 	if (fKeymapsMenuField->Menu()->FindMarked() == NULL)
310 		fKeymapsMenuField->MenuItem()->SetLabel(B_TRANSLATE("Custom"));
311 }
312 
313 
314 void
315 BootPromptWindow::_PopulateLanguages()
316 {
317 	// TODO: detect language/country from IP address
318 
319 	// Get current first preferred language of the user
320 	BMessage preferredLanguages;
321 	BLocaleRoster::Default()->GetPreferredLanguages(&preferredLanguages);
322 	const char* firstPreferredLanguage;
323 	if (preferredLanguages.FindString("language", &firstPreferredLanguage)
324 			!= B_OK) {
325 		// Fall back to built-in language of this application.
326 		firstPreferredLanguage = "en";
327 	}
328 
329 	BMessage installedCatalogs;
330 	BLocaleRoster::Default()->GetAvailableCatalogs(&installedCatalogs,
331 		"x-vnd.Haiku-FirstBootPrompt");
332 
333 	BFont font;
334 	fLanguagesListView->GetFont(&font);
335 
336 	// Try to instantiate a BCatalog for each language, it will only work
337 	// for translations of this application. So the list of languages will be
338 	// limited to catalogs written for this application, which is on purpose!
339 
340 	const char* languageID;
341 	LanguageItem* currentItem = NULL;
342 	for (int32 i = 0; installedCatalogs.FindString("language", i, &languageID)
343 			== B_OK; i++) {
344 		BLanguage* language;
345 		if (BLocaleRoster::Default()->GetLanguage(languageID, &language)
346 				== B_OK) {
347 			BString name;
348 			language->GetNativeName(name);
349 
350 			// TODO: the following block fails to detect a couple of language
351 			// names as containing glyphs we can't render. Why's that?
352 			bool hasGlyphs[name.CountChars()];
353 			font.GetHasGlyphs(name.String(), name.CountChars(), hasGlyphs);
354 			for (int32 i = 0; i < name.CountChars(); ++i) {
355 				if (!hasGlyphs[i]) {
356 					// replace by name translated to current language
357 					language->GetName(name);
358 					break;
359 				}
360 			}
361 
362 			LanguageItem* item = new LanguageItem(name.String(),
363 				languageID);
364 			fLanguagesListView->AddItem(item);
365 			// Select this item if it is the first preferred language
366 			if (strcmp(firstPreferredLanguage, languageID) == 0)
367 				currentItem = item;
368 
369 			delete language;
370 		} else
371 			fprintf(stderr, "failed to get BLanguage for %s\n", languageID);
372 	}
373 
374 	fLanguagesListView->SortItems(compare_void_list_items);
375 	if (currentItem != NULL)
376 		fLanguagesListView->Select(fLanguagesListView->IndexOf(currentItem));
377 	fLanguagesListView->ScrollToSelection();
378 
379 	// Re-enable sending the selection message.
380 	fLanguagesListView->SetSelectionMessage(
381 		new BMessage(MSG_LANGUAGE_SELECTED));
382 }
383 
384 
385 void
386 BootPromptWindow::_PopulateKeymaps()
387 {
388 	// Get the name of the current keymap, so we can mark the correct entry
389 	// in the list view.
390 	BString currentName;
391 	entry_ref currentRef;
392 	if (_GetCurrentKeymapRef(currentRef) == B_OK) {
393 		BNode node(&currentRef);
394 		node.ReadAttrString("keymap:name", &currentName);
395 	}
396 
397 	// TODO: common keymaps!
398 	BPath path;
399 	if (find_directory(B_SYSTEM_DATA_DIRECTORY, &path) != B_OK
400 		|| path.Append("Keymaps") != B_OK) {
401 		return;
402 	}
403 
404 	// US-International is the default keymap, if we could not found a
405 	// matching one
406 	BString usInternational("US-International");
407 
408 	// Populate the menu
409 	BDirectory directory;
410 	if (directory.SetTo(path.Path()) == B_OK) {
411 		entry_ref ref;
412 		BList itemsList;
413 		while (directory.GetNextRef(&ref) == B_OK) {
414 			BMessage* message = new BMessage(MSG_KEYMAP_SELECTED);
415 			message->AddRef("ref", &ref);
416 			BMenuItem* item = new BMenuItem(ref.name, message);
417 			itemsList.AddItem(item);
418 			if (currentName == ref.name)
419 				item->SetMarked(true);
420 
421 			if (usInternational == ref.name)
422 				fDefaultKeymapItem = item;
423 		}
424 		itemsList.SortItems(compare_void_menu_items);
425 		fKeymapsMenuField->Menu()->AddList(&itemsList, 0);
426 	}
427 }
428 
429 
430 void
431 BootPromptWindow::_ActivateKeymap(const BMessage* message) const
432 {
433 	entry_ref ref;
434 	if (message == NULL || message->FindRef("ref", &ref) != B_OK)
435 		return;
436 
437 	// Load and use the new keymap
438 	Keymap keymap;
439 	if (keymap.Load(ref) != B_OK) {
440 		fprintf(stderr, "Failed to load new keymap file (%s).\n", ref.name);
441 		return;
442 	}
443 
444 	// Get entry_ref to the Key_map file in the user settings.
445 	entry_ref currentRef;
446 	if (_GetCurrentKeymapRef(currentRef) != B_OK) {
447 		fprintf(stderr, "Failed to get ref to user keymap file.\n");
448 		return;
449 	}
450 
451 	if (keymap.Save(currentRef) != B_OK) {
452 		fprintf(stderr, "Failed to save new keymap file (%s).\n", ref.name);
453 		return;
454 	}
455 
456 	keymap.Use();
457 }
458 
459 
460 status_t
461 BootPromptWindow::_GetCurrentKeymapRef(entry_ref& ref) const
462 {
463 	BPath path;
464 	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK
465 		|| path.Append("Key_map") != B_OK) {
466 		return B_ERROR;
467 	}
468 
469 	return get_ref_for_path(path.Path(), &ref);
470 }
471 
472 
473 BMenuItem*
474 BootPromptWindow::_KeymapItemForLanguage(BLanguage& language) const
475 {
476 	BLanguage english("en");
477 	BString name;
478 	if (language.GetName(name, &english) != B_OK)
479 		return fDefaultKeymapItem;
480 
481 	// Check special mappings first
482 	for (size_t i = 0; i < kLanguageKeymapMappingsSize; i += 2) {
483 		if (!strcmp(name, kLanguageKeymapMappings[i])) {
484 			name = kLanguageKeymapMappings[i + 1];
485 			break;
486 		}
487 	}
488 
489 	BMenu* menu = fKeymapsMenuField->Menu();
490 	for (int32 i = 0; i < menu->CountItems(); i++) {
491 		BMenuItem* item = menu->ItemAt(i);
492 		BMessage* message = item->Message();
493 
494 		entry_ref ref;
495 		if (message->FindRef("ref", &ref) == B_OK
496 			&& name == ref.name)
497 			return item;
498 	}
499 
500 	return fDefaultKeymapItem;
501 }
502