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
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:
LanguageItem(const char * label,const char * language)83 LanguageItem(const char* label, const char* language)
84 :
85 BStringItem(label),
86 fLanguage(language)
87 {
88 }
89
~LanguageItem()90 ~LanguageItem()
91 {
92 }
93
Language() const94 const char* Language() const
95 {
96 return fLanguage.String();
97 }
98
DrawItem(BView * owner,BRect frame,bool complete)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
compare_void_list_items(const void * _a,const void * _b)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
compare_void_menu_items(const void * _a,const void * _b)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
BootPromptWindow()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 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
263 void
MessageReceived(BMessage * message)264 BootPromptWindow::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
301 bool
QuitRequested()302 BootPromptWindow::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
339 void
_InitCatalog(bool saveSettings)340 BootPromptWindow::_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
361 void
_UpdateStrings()362 BootPromptWindow::_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
419 void
_PopulateLanguages()420 BootPromptWindow::_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
490 void
_UpdateKeymapsMenu()491 BootPromptWindow::_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
512 void
_PopulateKeymaps()513 BootPromptWindow::_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(¤tRef);
521 node.ReadAttrString("keymap:name", ¤tName);
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
559 void
_ActivateKeymap(const BMessage * message) const560 BootPromptWindow::_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
589 status_t
_GetCurrentKeymapRef(entry_ref & ref) const590 BootPromptWindow::_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
602 BMenuItem*
_KeymapItemForLanguage(BLanguage & language) const603 BootPromptWindow::_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