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 "Ivory" Vasilopoulos <git@n0toose.net>
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
50using BPrivate::MutableLocaleRoster;
51
52
53#undef B_TRANSLATION_CONTEXT
54#define B_TRANSLATION_CONTEXT "BootPromptWindow"
55
56
57namespace BPrivate {
58	void ForceUnloadCatalog();
59};
60
61
62static 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};
77static const size_t kLanguageKeymapMappingsSize
78	= sizeof(kLanguageKeymapMappings) / sizeof(kLanguageKeymapMappings[0]);
79
80
81class LanguageItem : public BStringItem {
82public:
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
104private:
105			BString				fLanguage;
106};
107
108
109static int
110compare_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
121static int
122compare_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
136BootPromptWindow::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	const BRect iconRect = BRect(BPoint(0, 0), be_control_look->ComposeIconSize(24));
159	BBitmap desktopIcon(iconRect, B_RGBA32);
160	data = (const uint8_t*)res->LoadResource('VICN', "Desktop", &size);
161	BIconUtils::GetVectorIcon(data, size, &desktopIcon);
162
163	BBitmap installerIcon(iconRect, B_RGBA32);
164	data = (const uint8_t*)res->LoadResource('VICN', "Installer", &size);
165	BIconUtils::GetVectorIcon(data, size, &installerIcon);
166
167	fDesktopButton = new BButton("", new BMessage(MSG_BOOT_DESKTOP));
168	fDesktopButton->SetTarget(be_app);
169	fDesktopButton->MakeDefault(true);
170	fDesktopButton->SetIcon(&desktopIcon);
171
172	fInstallerButton = new BButton("", new BMessage(MSG_RUN_INSTALLER));
173	fInstallerButton->SetTarget(be_app);
174	fInstallerButton->SetIcon(&installerIcon);
175
176	data = (const uint8_t*)res->LoadResource('VICN', "Language", &size);
177	IconView* languageIcon = new IconView(B_LARGE_ICON);
178	languageIcon->SetIcon(data, size, B_LARGE_ICON);
179
180	data = (const uint8_t*)res->LoadResource('VICN', "Keymap", &size);
181	IconView* keymapIcon = new IconView(B_LARGE_ICON);
182	keymapIcon->SetIcon(data, size, B_LARGE_ICON);
183
184	fLanguagesLabelView = new BStringView("languagesLabel", "");
185	fLanguagesLabelView->SetFont(be_bold_font);
186	fLanguagesLabelView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED,
187		B_SIZE_UNSET));
188
189	fKeymapsMenuLabel = new BStringView("keymapsLabel", "");
190	fKeymapsMenuLabel->SetFont(be_bold_font);
191	fKeymapsMenuLabel->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED,
192		B_SIZE_UNSET));
193	// Make sure there is enough space to display the text even in verbose
194	// locales, to avoid width changes on language changes
195	float labelWidth = fKeymapsMenuLabel->StringWidth("Disposition du clavier")
196		+ 16;
197	fKeymapsMenuLabel->SetExplicitMinSize(BSize(labelWidth, B_SIZE_UNSET));
198
199	fLanguagesListView = new BListView();
200	BScrollView* languagesScrollView = new BScrollView("languagesScroll",
201		fLanguagesListView, B_WILL_DRAW, false, true);
202
203	// Carefully designed to not exceed the 640x480 resolution with a 12pt font.
204	float width = 640 * be_plain_font->Size() / 12 - (labelWidth + 64);
205	float height = be_plain_font->Size() * 23;
206	fInfoTextView->SetExplicitMinSize(BSize(width, height));
207	fInfoTextView->SetExplicitMaxSize(BSize(width, B_SIZE_UNSET));
208
209	// Make sure the language list view is always wide enough to show the
210	// largest language
211	fLanguagesListView->SetExplicitMinSize(
212		BSize(fLanguagesListView->StringWidth("Portugu��s (Brasil)"),
213		height));
214
215	fKeymapsMenuField = new BMenuField("", "", new BMenu(""));
216	fKeymapsMenuField->Menu()->SetLabelFromMarked(true);
217
218	_InitCatalog(true);
219	_PopulateLanguages();
220	_PopulateKeymaps();
221
222	BLayoutBuilder::Group<>(this, B_HORIZONTAL)
223		.SetInsets(B_USE_WINDOW_SPACING)
224		.AddGroup(B_VERTICAL, 0)
225			.SetInsets(0, 0, 0, B_USE_SMALL_SPACING)
226			.AddGroup(B_HORIZONTAL)
227				.Add(languageIcon)
228				.Add(fLanguagesLabelView)
229				.SetInsets(0, 0, 0, B_USE_SMALL_SPACING)
230			.End()
231			.Add(languagesScrollView)
232			.AddGroup(B_HORIZONTAL)
233				.Add(keymapIcon)
234				.Add(fKeymapsMenuLabel)
235				.SetInsets(0, B_USE_DEFAULT_SPACING, 0,
236					B_USE_SMALL_SPACING)
237			.End()
238			.Add(fKeymapsMenuField)
239		.End()
240		.AddGroup(B_VERTICAL)
241			.SetInsets(0)
242			.Add(fInfoTextView)
243			.AddGroup(B_HORIZONTAL)
244				.SetInsets(0)
245				.AddGlue()
246				.Add(fInstallerButton)
247				.Add(fDesktopButton)
248			.End()
249		.End();
250
251	fLanguagesListView->MakeFocus();
252
253	// Force the info text view to use a reasonable size
254	fInfoTextView->SetText("x\n\n\n\n\n\n\n\n\n\n\n\n\n\nx");
255	ResizeToPreferred();
256
257	_UpdateStrings();
258	CenterOnScreen();
259	Show();
260}
261
262
263void
264BootPromptWindow::MessageReceived(BMessage* message)
265{
266	switch (message->what) {
267		case MSG_LANGUAGE_SELECTED:
268			if (LanguageItem* item = static_cast<LanguageItem*>(
269					fLanguagesListView->ItemAt(
270						fLanguagesListView->CurrentSelection(0)))) {
271				BMessage preferredLanguages;
272				preferredLanguages.AddString("language", item->Language());
273				MutableLocaleRoster::Default()->SetPreferredLanguages(
274					&preferredLanguages);
275				_InitCatalog(true);
276				_UpdateKeymapsMenu();
277
278				// Select default keymap by language
279				BLanguage language(item->Language());
280				BMenuItem* keymapItem = _KeymapItemForLanguage(language);
281				if (keymapItem != NULL) {
282					keymapItem->SetMarked(true);
283					_ActivateKeymap(keymapItem->Message());
284				}
285			}
286			// Calling it here is a cheap way of preventing the user to have
287			// no item selected. Always the current item will be selected.
288			_UpdateStrings();
289			break;
290
291		case MSG_KEYMAP_SELECTED:
292			_ActivateKeymap(message);
293			break;
294
295		default:
296			BWindow::MessageReceived(message);
297	}
298}
299
300
301bool
302BootPromptWindow::QuitRequested()
303{
304	// If the Deskbar is not running, then FirstBootPrompt is
305	// is the only thing visible on the screen and that we won't
306	// have anything else to show. In that case, it would make
307	// sense to reboot the machine instead, but doing so without
308	// a warning could be confusing.
309	//
310	// Rebooting is managed by BootPrompt.cpp.
311
312	BAlert* alert = new(std::nothrow) BAlert(
313		B_TRANSLATE_SYSTEM_NAME("Quit Haiku"),
314		B_TRANSLATE("Are you sure you want to close this window? This will "
315			"restart your system!"),
316		B_TRANSLATE("Cancel"), B_TRANSLATE("Restart system"), NULL,
317		B_WIDTH_AS_USUAL, B_STOP_ALERT);
318
319	// If there is not enough memory to create the alert here, we may as
320	// well try to reboot. There probably isn't much else to do anyway.
321	if (alert != NULL) {
322		alert->SetShortcut(0, B_ESCAPE);
323
324		if (alert->Go() == 0) {
325			// User doesn't want to exit after all
326			return false;
327		}
328	}
329
330	// If deskbar is running, don't actually reboot: we are in test mode
331	// (probably run by a developer manually).
332	if (!be_roster->IsRunning(kDeskbarSignature))
333		be_app->PostMessage(MSG_REBOOT_REQUESTED);
334
335	return true;
336}
337
338
339void
340BootPromptWindow::_InitCatalog(bool saveSettings)
341{
342	// Initilialize the Locale Kit
343	BPrivate::ForceUnloadCatalog();
344
345	if (!saveSettings)
346		return;
347
348	BMessage settings;
349	BString language;
350	if (BLocaleRoster::Default()->GetCatalog()->GetLanguage(&language) == B_OK)
351		settings.AddString("language", language.String());
352
353	MutableLocaleRoster::Default()->SetPreferredLanguages(&settings);
354
355	BFormattingConventions conventions(language.String());
356	MutableLocaleRoster::Default()->SetDefaultFormattingConventions(
357		conventions);
358}
359
360
361void
362BootPromptWindow::_UpdateStrings()
363{
364	BString titleTextHaiku = B_TRANSLATE("Welcome to Haiku!");
365	BString mainTextHaiku = B_TRANSLATE_COMMENT(
366		"Thank you for trying out Haiku! We hope you'll like it!\n\n"
367		"Please select your preferred language and keymap. Both settings can "
368		"also be changed later when running Haiku.\n\n"
369
370		"Do you wish to install Haiku now, or try it out first?",
371
372		"For other languages, a note could be added: \""
373		"Note: Localization of Haiku applications and other components is "
374		"an on-going effort. You will frequently encounter untranslated "
375		"strings, but if you like, you can join in the work at "
376		"<www.haiku-os.org>.\"");
377	BString desktopTextHaiku = B_TRANSLATE("Try Haiku");
378	BString installTextHaiku = B_TRANSLATE("Install Haiku");
379
380	BString titleTextDebranded = B_TRANSLATE("Welcome!");
381	BString mainTextDebranded = B_TRANSLATE_COMMENT(
382			"Thank you for trying out our operating system! We hope you'll "
383			"like it!\n\n"
384			"Please select your preferred language and keymap. Both settings "
385			"can also be changed later.\n\n"
386
387			"Do you wish to install the operating system now, or try it out "
388			"first?",
389
390			"This notice appears when the build of Haiku that's currently "
391			"being used is unofficial, as in, not distributed by Haiku itself."
392			"For other languages, a note could be added: \""
393			"Note: Localization of Haiku applications and other components is "
394			"an on-going effort. You will frequently encounter untranslated "
395			"strings, but if you like, you can join in the work at "
396			"<www.haiku-os.org>.\"");
397	BString desktopTextDebranded = B_TRANSLATE("Try it out");
398	BString installTextDebranded = B_TRANSLATE("Install");
399
400#ifdef HAIKU_DISTRO_COMPATIBILITY_OFFICIAL
401	SetTitle(titleTextHaiku);
402	fInfoTextView->SetText(mainTextHaiku);
403	fDesktopButton->SetLabel(desktopTextHaiku);
404	fInstallerButton->SetLabel(installTextHaiku);
405#else
406	SetTitle(titleTextDebranded);
407	fInfoTextView->SetText(mainTextDebranded);
408	fDesktopButton->SetLabel(desktopTextDebranded);
409	fInstallerButton->SetLabel(installTextDebranded);
410#endif
411
412	fLanguagesLabelView->SetText(B_TRANSLATE("Language"));
413	fKeymapsMenuLabel->SetText(B_TRANSLATE("Keymap"));
414	if (fKeymapsMenuField->Menu()->FindMarked() == NULL)
415		fKeymapsMenuField->MenuItem()->SetLabel(B_TRANSLATE("Custom"));
416}
417
418
419void
420BootPromptWindow::_PopulateLanguages()
421{
422	// TODO: detect language/country from IP address
423
424	// Get current first preferred language of the user
425	BMessage preferredLanguages;
426	BLocaleRoster::Default()->GetPreferredLanguages(&preferredLanguages);
427	const char* firstPreferredLanguage;
428	if (preferredLanguages.FindString("language", &firstPreferredLanguage)
429			!= B_OK) {
430		// Fall back to built-in language of this application.
431		firstPreferredLanguage = "en";
432	}
433
434	BMessage installedCatalogs;
435	BLocaleRoster::Default()->GetAvailableCatalogs(&installedCatalogs,
436		"x-vnd.Haiku-FirstBootPrompt");
437
438	BFont font;
439	fLanguagesListView->GetFont(&font);
440
441	// Try to instantiate a BCatalog for each language, it will only work
442	// for translations of this application. So the list of languages will be
443	// limited to catalogs written for this application, which is on purpose!
444
445	const char* languageID;
446	LanguageItem* currentItem = NULL;
447	for (int32 i = 0; installedCatalogs.FindString("language", i, &languageID)
448			== B_OK; i++) {
449		BLanguage* language;
450		if (BLocaleRoster::Default()->GetLanguage(languageID, &language)
451				== B_OK) {
452			BString name;
453			language->GetNativeName(name);
454
455			// TODO: the following block fails to detect a couple of language
456			// names as containing glyphs we can't render. Why's that?
457			bool hasGlyphs[name.CountChars()];
458			font.GetHasGlyphs(name.String(), name.CountChars(), hasGlyphs);
459			for (int32 i = 0; i < name.CountChars(); ++i) {
460				if (!hasGlyphs[i]) {
461					// replace by name translated to current language
462					language->GetName(name);
463					break;
464				}
465			}
466
467			LanguageItem* item = new LanguageItem(name.String(),
468				languageID);
469			fLanguagesListView->AddItem(item);
470			// Select this item if it is the first preferred language
471			if (strcmp(firstPreferredLanguage, languageID) == 0)
472				currentItem = item;
473
474			delete language;
475		} else
476			fprintf(stderr, "failed to get BLanguage for %s\n", languageID);
477	}
478
479	fLanguagesListView->SortItems(compare_void_list_items);
480	if (currentItem != NULL)
481		fLanguagesListView->Select(fLanguagesListView->IndexOf(currentItem));
482	fLanguagesListView->ScrollToSelection();
483
484	// Re-enable sending the selection message.
485	fLanguagesListView->SetSelectionMessage(
486		new BMessage(MSG_LANGUAGE_SELECTED));
487}
488
489
490void
491BootPromptWindow::_UpdateKeymapsMenu()
492{
493	BMenu *menu = fKeymapsMenuField->Menu();
494	BMenuItem* item;
495	BList itemsList;
496
497	// Recreate keymapmenu items list, since BMenu could not sort its items.
498	while ((item = menu->ItemAt(0)) != NULL) {
499		BMessage* message = item->Message();
500		entry_ref ref;
501		message->FindRef("ref", &ref);
502		item-> SetLabel(B_TRANSLATE_NOCOLLECT_ALL((ref.name),
503		"KeymapNames", NULL));
504		itemsList.AddItem(item);
505		menu->RemoveItem((int32)0);
506	}
507	itemsList.SortItems(compare_void_menu_items);
508	fKeymapsMenuField->Menu()->AddList(&itemsList, 0);
509}
510
511
512void
513BootPromptWindow::_PopulateKeymaps()
514{
515	// Get the name of the current keymap, so we can mark the correct entry
516	// in the list view.
517	BString currentName;
518	entry_ref currentRef;
519	if (_GetCurrentKeymapRef(currentRef) == B_OK) {
520		BNode node(&currentRef);
521		node.ReadAttrString("keymap:name", &currentName);
522	}
523
524	// TODO: common keymaps!
525	BPath path;
526	if (find_directory(B_SYSTEM_DATA_DIRECTORY, &path) != B_OK
527		|| path.Append("Keymaps") != B_OK) {
528		return;
529	}
530
531	// US-International is the default keymap, if we could not found a
532	// matching one
533	BString usInternational("US-International");
534
535	// Populate the menu
536	BDirectory directory;
537	if (directory.SetTo(path.Path()) == B_OK) {
538		entry_ref ref;
539		BList itemsList;
540		while (directory.GetNextRef(&ref) == B_OK) {
541			BMessage* message = new BMessage(MSG_KEYMAP_SELECTED);
542			message->AddRef("ref", &ref);
543			BMenuItem* item =
544				new BMenuItem(B_TRANSLATE_NOCOLLECT_ALL((ref.name),
545				"KeymapNames", NULL), message);
546			itemsList.AddItem(item);
547			if (currentName == ref.name)
548				item->SetMarked(true);
549
550			if (usInternational == ref.name)
551				fDefaultKeymapItem = item;
552		}
553		itemsList.SortItems(compare_void_menu_items);
554		fKeymapsMenuField->Menu()->AddList(&itemsList, 0);
555	}
556}
557
558
559void
560BootPromptWindow::_ActivateKeymap(const BMessage* message) const
561{
562	entry_ref ref;
563	if (message == NULL || message->FindRef("ref", &ref) != B_OK)
564		return;
565
566	// Load and use the new keymap
567	Keymap keymap;
568	if (keymap.Load(ref) != B_OK) {
569		fprintf(stderr, "Failed to load new keymap file (%s).\n", ref.name);
570		return;
571	}
572
573	// Get entry_ref to the Key_map file in the user settings.
574	entry_ref currentRef;
575	if (_GetCurrentKeymapRef(currentRef) != B_OK) {
576		fprintf(stderr, "Failed to get ref to user keymap file.\n");
577		return;
578	}
579
580	if (keymap.Save(currentRef) != B_OK) {
581		fprintf(stderr, "Failed to save new keymap file (%s).\n", ref.name);
582		return;
583	}
584
585	keymap.Use();
586}
587
588
589status_t
590BootPromptWindow::_GetCurrentKeymapRef(entry_ref& ref) const
591{
592	BPath path;
593	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK
594		|| path.Append("Key_map") != B_OK) {
595		return B_ERROR;
596	}
597
598	return get_ref_for_path(path.Path(), &ref);
599}
600
601
602BMenuItem*
603BootPromptWindow::_KeymapItemForLanguage(BLanguage& language) const
604{
605	BLanguage english("en");
606	BString name;
607	if (language.GetName(name, &english) != B_OK)
608		return fDefaultKeymapItem;
609
610	// Check special mappings first
611	for (size_t i = 0; i < kLanguageKeymapMappingsSize; i += 2) {
612		if (!strcmp(name, kLanguageKeymapMappings[i])) {
613			name = kLanguageKeymapMappings[i + 1];
614			break;
615		}
616	}
617
618	BMenu* menu = fKeymapsMenuField->Menu();
619	for (int32 i = 0; i < menu->CountItems(); i++) {
620		BMenuItem* item = menu->ItemAt(i);
621		BMessage* message = item->Message();
622
623		entry_ref ref;
624		if (message->FindRef("ref", &ref) == B_OK
625			&& name == ref.name)
626			return item;
627	}
628
629	return fDefaultKeymapItem;
630}
631