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