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