/* * Copyright 2010, Stephan Aßmus * Copyright 2010-2021, Adrien Destugues, pulkomandy@pulkomandy.tk. * Copyright 2011, Axel Dörfler, axeld@pinc-software.de. * Copyright 2020-2021, Panagiotis "Ivory" Vasilopoulos * * All rights reserved. Distributed under the terms of the MIT License. */ #include "BootPromptWindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "BootPrompt.h" #include "Keymap.h" #include "KeymapNames.h" using BPrivate::MutableLocaleRoster; #undef B_TRANSLATION_CONTEXT #define B_TRANSLATION_CONTEXT "BootPromptWindow" namespace BPrivate { void ForceUnloadCatalog(); }; static const char* kLanguageKeymapMappings[] = { // While there is a "Dutch" keymap, it apparently has not been widely // adopted, and the US-International keymap is common "Dutch", "US-International", // Cyrillic keymaps are not usable alone, as latin alphabet is required to // use Terminal. So we stay in US international until the user has a chance // to set up KeymapSwitcher. "Belarusian", "US-International", "Russian", "US-International", "Ukrainian", "US-International", // Turkish has two layouts, we must pick one "Turkish", "Turkish (Type-Q)", }; static const size_t kLanguageKeymapMappingsSize = sizeof(kLanguageKeymapMappings) / sizeof(kLanguageKeymapMappings[0]); class LanguageItem : public BStringItem { public: LanguageItem(const char* label, const char* language) : BStringItem(label), fLanguage(language) { } ~LanguageItem() { } const char* Language() const { return fLanguage.String(); } void DrawItem(BView* owner, BRect frame, bool complete) { BStringItem::DrawItem(owner, frame, true/*complete*/); } private: BString fLanguage; }; static int compare_void_list_items(const void* _a, const void* _b) { static BCollator collator; LanguageItem* a = *(LanguageItem**)_a; LanguageItem* b = *(LanguageItem**)_b; return collator.Compare(a->Text(), b->Text()); } static int compare_void_menu_items(const void* _a, const void* _b) { static BCollator collator; BMenuItem* a = *(BMenuItem**)_a; BMenuItem* b = *(BMenuItem**)_b; return collator.Compare(a->Label(), b->Label()); } // #pragma mark - BootPromptWindow::BootPromptWindow() : BWindow(BRect(0, 0, 530, 400), "", B_TITLED_WINDOW, B_NOT_ZOOMABLE | B_NOT_MINIMIZABLE | B_NOT_RESIZABLE | B_AUTO_UPDATE_SIZE_LIMITS | B_QUIT_ON_WINDOW_CLOSE, B_ALL_WORKSPACES), fDefaultKeymapItem(NULL) { SetSizeLimits(450, 16384, 350, 16384); rgb_color textColor = ui_color(B_PANEL_TEXT_COLOR); fInfoTextView = new BTextView(""); fInfoTextView->SetViewUIColor(B_PANEL_BACKGROUND_COLOR); fInfoTextView->SetFontAndColor(be_plain_font, B_FONT_ALL, &textColor); fInfoTextView->MakeEditable(false); fInfoTextView->MakeSelectable(false); fInfoTextView->MakeResizable(false); BResources* res = BApplication::AppResources(); size_t size = 0; const uint8_t* data; BBitmap desktopIcon(BRect(0, 0, 23, 23), B_RGBA32); data = (const uint8_t*)res->LoadResource('VICN', "Desktop", &size); BIconUtils::GetVectorIcon(data, size, &desktopIcon); BBitmap installerIcon(BRect(0, 0, 23, 23), B_RGBA32); data = (const uint8_t*)res->LoadResource('VICN', "Installer", &size); BIconUtils::GetVectorIcon(data, size, &installerIcon); fDesktopButton = new BButton("", new BMessage(MSG_BOOT_DESKTOP)); fDesktopButton->SetTarget(be_app); fDesktopButton->MakeDefault(true); fDesktopButton->SetIcon(&desktopIcon); fInstallerButton = new BButton("", new BMessage(MSG_RUN_INSTALLER)); fInstallerButton->SetTarget(be_app); fInstallerButton->SetIcon(&installerIcon); data = (const uint8_t*)res->LoadResource('VICN', "Language", &size); IconView* languageIcon = new IconView(B_LARGE_ICON); languageIcon->SetIcon(data, size, B_LARGE_ICON); data = (const uint8_t*)res->LoadResource('VICN', "Keymap", &size); IconView* keymapIcon = new IconView(B_LARGE_ICON); keymapIcon->SetIcon(data, size, B_LARGE_ICON); fLanguagesLabelView = new BStringView("languagesLabel", ""); fLanguagesLabelView->SetFont(be_bold_font); fLanguagesLabelView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET)); fKeymapsMenuLabel = new BStringView("keymapsLabel", ""); fKeymapsMenuLabel->SetFont(be_bold_font); fKeymapsMenuLabel->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET)); // Make sure there is enough space to display the text even in verbose // locales, to avoid width changes on language changes float labelWidth = fKeymapsMenuLabel->StringWidth("Disposition du clavier") + 16; fKeymapsMenuLabel->SetExplicitMinSize(BSize(labelWidth, B_SIZE_UNSET)); fLanguagesListView = new BListView(); BScrollView* languagesScrollView = new BScrollView("languagesScroll", fLanguagesListView, B_WILL_DRAW, false, true); // Carefully designed to not exceed the 640x480 resolution with a 12pt font. float width = 640 * be_plain_font->Size() / 12 - (labelWidth + 64); float height = be_plain_font->Size() * 23; fInfoTextView->SetExplicitMinSize(BSize(width, height)); fInfoTextView->SetExplicitMaxSize(BSize(width, B_SIZE_UNSET)); // Make sure the language list view is always wide enough to show the // largest language fLanguagesListView->SetExplicitMinSize( BSize(fLanguagesListView->StringWidth("Português (Brasil)"), height)); fKeymapsMenuField = new BMenuField("", "", new BMenu("")); fKeymapsMenuField->Menu()->SetLabelFromMarked(true); _InitCatalog(true); _PopulateLanguages(); _PopulateKeymaps(); BLayoutBuilder::Group<>(this, B_HORIZONTAL) .SetInsets(B_USE_WINDOW_SPACING) .AddGroup(B_VERTICAL, 0) .SetInsets(0, 0, 0, B_USE_SMALL_SPACING) .AddGroup(B_HORIZONTAL) .Add(languageIcon) .Add(fLanguagesLabelView) .SetInsets(0, 0, 0, B_USE_SMALL_SPACING) .End() .Add(languagesScrollView) .AddGroup(B_HORIZONTAL) .Add(keymapIcon) .Add(fKeymapsMenuLabel) .SetInsets(0, B_USE_DEFAULT_SPACING, 0, B_USE_SMALL_SPACING) .End() .Add(fKeymapsMenuField) .End() .AddGroup(B_VERTICAL) .SetInsets(0) .Add(fInfoTextView) .AddGroup(B_HORIZONTAL) .SetInsets(0) .AddGlue() .Add(fInstallerButton) .Add(fDesktopButton) .End() .End(); fLanguagesListView->MakeFocus(); // Force the info text view to use a reasonable size fInfoTextView->SetText("x\n\n\n\n\n\n\n\n\n\n\n\n\n\nx"); ResizeToPreferred(); _UpdateStrings(); CenterOnScreen(); Show(); } void BootPromptWindow::MessageReceived(BMessage* message) { switch (message->what) { case MSG_LANGUAGE_SELECTED: if (LanguageItem* item = static_cast( fLanguagesListView->ItemAt( fLanguagesListView->CurrentSelection(0)))) { BMessage preferredLanguages; preferredLanguages.AddString("language", item->Language()); MutableLocaleRoster::Default()->SetPreferredLanguages( &preferredLanguages); _InitCatalog(true); _UpdateKeymapsMenu(); // Select default keymap by language BLanguage language(item->Language()); BMenuItem* keymapItem = _KeymapItemForLanguage(language); if (keymapItem != NULL) { keymapItem->SetMarked(true); _ActivateKeymap(keymapItem->Message()); } } // Calling it here is a cheap way of preventing the user to have // no item selected. Always the current item will be selected. _UpdateStrings(); break; case MSG_KEYMAP_SELECTED: _ActivateKeymap(message); break; default: BWindow::MessageReceived(message); } } bool BootPromptWindow::QuitRequested() { // If the Deskbar is not running, then FirstBootPrompt is // is the only thing visible on the screen and that we won't // have anything else to show. In that case, it would make // sense to reboot the machine instead, but doing so without // a warning could be confusing. // // Rebooting is managed by BootPrompt.cpp. BAlert* alert = new(std::nothrow) BAlert( B_TRANSLATE_SYSTEM_NAME("Quit Haiku"), B_TRANSLATE("Are you sure you want to close this window? This will " "restart your system!"), B_TRANSLATE("Cancel"), B_TRANSLATE("Restart system"), NULL, B_WIDTH_AS_USUAL, B_STOP_ALERT); // If there is not enough memory to create the alert here, we may as // well try to reboot. There probably isn't much else to do anyway. if (alert != NULL) { alert->SetShortcut(0, B_ESCAPE); if (alert->Go() == 0) { // User doesn't want to exit after all return false; } } // If deskbar is running, don't actually reboot: we are in test mode // (probably run by a developer manually). if (!be_roster->IsRunning(kDeskbarSignature)) be_app->PostMessage(MSG_REBOOT_REQUESTED); return true; } void BootPromptWindow::_InitCatalog(bool saveSettings) { // Initilialize the Locale Kit BPrivate::ForceUnloadCatalog(); if (!saveSettings) return; BMessage settings; BString language; if (BLocaleRoster::Default()->GetCatalog()->GetLanguage(&language) == B_OK) settings.AddString("language", language.String()); MutableLocaleRoster::Default()->SetPreferredLanguages(&settings); BFormattingConventions conventions(language.String()); MutableLocaleRoster::Default()->SetDefaultFormattingConventions( conventions); } void BootPromptWindow::_UpdateStrings() { BString titleTextHaiku = B_TRANSLATE("Welcome to Haiku!"); BString mainTextHaiku = B_TRANSLATE_COMMENT( "Thank you for trying out Haiku! We hope you'll like it!\n\n" "Please select your preferred language and keymap. Both settings can " "also be changed later when running Haiku.\n\n" "Do you wish to install Haiku now, or try it out first?", "For other languages, a note could be added: \"" "Note: Localization of Haiku applications and other components is " "an on-going effort. You will frequently encounter untranslated " "strings, but if you like, you can join in the work at " ".\""); BString desktopTextHaiku = B_TRANSLATE("Try Haiku"); BString installTextHaiku = B_TRANSLATE("Install Haiku"); BString titleTextDebranded = B_TRANSLATE("Welcome!"); BString mainTextDebranded = B_TRANSLATE_COMMENT( "Thank you for trying out our operating system! We hope you'll " "like it!\n\n" "Please select your preferred language and keymap. Both settings " "can also be changed later.\n\n" "Do you wish to install the operating system now, or try it out " "first?", "This notice appears when the build of Haiku that's currently " "being used is unofficial, as in, not distributed by Haiku itself." "For other languages, a note could be added: \"" "Note: Localization of Haiku applications and other components is " "an on-going effort. You will frequently encounter untranslated " "strings, but if you like, you can join in the work at " ".\""); BString desktopTextDebranded = B_TRANSLATE("Try it out"); BString installTextDebranded = B_TRANSLATE("Install"); #ifdef HAIKU_DISTRO_COMPATIBILITY_OFFICIAL SetTitle(titleTextHaiku); fInfoTextView->SetText(mainTextHaiku); fDesktopButton->SetLabel(desktopTextHaiku); fInstallerButton->SetLabel(installTextHaiku); #else SetTitle(titleTextDebranded); fInfoTextView->SetText(mainTextDebranded); fDesktopButton->SetLabel(desktopTextDebranded); fInstallerButton->SetLabel(installTextDebranded); #endif fLanguagesLabelView->SetText(B_TRANSLATE("Language")); fKeymapsMenuLabel->SetText(B_TRANSLATE("Keymap")); if (fKeymapsMenuField->Menu()->FindMarked() == NULL) fKeymapsMenuField->MenuItem()->SetLabel(B_TRANSLATE("Custom")); } void BootPromptWindow::_PopulateLanguages() { // TODO: detect language/country from IP address // Get current first preferred language of the user BMessage preferredLanguages; BLocaleRoster::Default()->GetPreferredLanguages(&preferredLanguages); const char* firstPreferredLanguage; if (preferredLanguages.FindString("language", &firstPreferredLanguage) != B_OK) { // Fall back to built-in language of this application. firstPreferredLanguage = "en"; } BMessage installedCatalogs; BLocaleRoster::Default()->GetAvailableCatalogs(&installedCatalogs, "x-vnd.Haiku-FirstBootPrompt"); BFont font; fLanguagesListView->GetFont(&font); // Try to instantiate a BCatalog for each language, it will only work // for translations of this application. So the list of languages will be // limited to catalogs written for this application, which is on purpose! const char* languageID; LanguageItem* currentItem = NULL; for (int32 i = 0; installedCatalogs.FindString("language", i, &languageID) == B_OK; i++) { BLanguage* language; if (BLocaleRoster::Default()->GetLanguage(languageID, &language) == B_OK) { BString name; language->GetNativeName(name); // TODO: the following block fails to detect a couple of language // names as containing glyphs we can't render. Why's that? bool hasGlyphs[name.CountChars()]; font.GetHasGlyphs(name.String(), name.CountChars(), hasGlyphs); for (int32 i = 0; i < name.CountChars(); ++i) { if (!hasGlyphs[i]) { // replace by name translated to current language language->GetName(name); break; } } LanguageItem* item = new LanguageItem(name.String(), languageID); fLanguagesListView->AddItem(item); // Select this item if it is the first preferred language if (strcmp(firstPreferredLanguage, languageID) == 0) currentItem = item; delete language; } else fprintf(stderr, "failed to get BLanguage for %s\n", languageID); } fLanguagesListView->SortItems(compare_void_list_items); if (currentItem != NULL) fLanguagesListView->Select(fLanguagesListView->IndexOf(currentItem)); fLanguagesListView->ScrollToSelection(); // Re-enable sending the selection message. fLanguagesListView->SetSelectionMessage( new BMessage(MSG_LANGUAGE_SELECTED)); } void BootPromptWindow::_UpdateKeymapsMenu() { BMenu *menu = fKeymapsMenuField->Menu(); BMenuItem* item; BList itemsList; // Recreate keymapmenu items list, since BMenu could not sort its items. while ((item = menu->ItemAt(0)) != NULL) { BMessage* message = item->Message(); entry_ref ref; message->FindRef("ref", &ref); item-> SetLabel(B_TRANSLATE_NOCOLLECT_ALL((ref.name), "KeymapNames", NULL)); itemsList.AddItem(item); menu->RemoveItem((int32)0); } itemsList.SortItems(compare_void_menu_items); fKeymapsMenuField->Menu()->AddList(&itemsList, 0); } void BootPromptWindow::_PopulateKeymaps() { // Get the name of the current keymap, so we can mark the correct entry // in the list view. BString currentName; entry_ref currentRef; if (_GetCurrentKeymapRef(currentRef) == B_OK) { BNode node(¤tRef); node.ReadAttrString("keymap:name", ¤tName); } // TODO: common keymaps! BPath path; if (find_directory(B_SYSTEM_DATA_DIRECTORY, &path) != B_OK || path.Append("Keymaps") != B_OK) { return; } // US-International is the default keymap, if we could not found a // matching one BString usInternational("US-International"); // Populate the menu BDirectory directory; if (directory.SetTo(path.Path()) == B_OK) { entry_ref ref; BList itemsList; while (directory.GetNextRef(&ref) == B_OK) { BMessage* message = new BMessage(MSG_KEYMAP_SELECTED); message->AddRef("ref", &ref); BMenuItem* item = new BMenuItem(B_TRANSLATE_NOCOLLECT_ALL((ref.name), "KeymapNames", NULL), message); itemsList.AddItem(item); if (currentName == ref.name) item->SetMarked(true); if (usInternational == ref.name) fDefaultKeymapItem = item; } itemsList.SortItems(compare_void_menu_items); fKeymapsMenuField->Menu()->AddList(&itemsList, 0); } } void BootPromptWindow::_ActivateKeymap(const BMessage* message) const { entry_ref ref; if (message == NULL || message->FindRef("ref", &ref) != B_OK) return; // Load and use the new keymap Keymap keymap; if (keymap.Load(ref) != B_OK) { fprintf(stderr, "Failed to load new keymap file (%s).\n", ref.name); return; } // Get entry_ref to the Key_map file in the user settings. entry_ref currentRef; if (_GetCurrentKeymapRef(currentRef) != B_OK) { fprintf(stderr, "Failed to get ref to user keymap file.\n"); return; } if (keymap.Save(currentRef) != B_OK) { fprintf(stderr, "Failed to save new keymap file (%s).\n", ref.name); return; } keymap.Use(); } status_t BootPromptWindow::_GetCurrentKeymapRef(entry_ref& ref) const { BPath path; if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK || path.Append("Key_map") != B_OK) { return B_ERROR; } return get_ref_for_path(path.Path(), &ref); } BMenuItem* BootPromptWindow::_KeymapItemForLanguage(BLanguage& language) const { BLanguage english("en"); BString name; if (language.GetName(name, &english) != B_OK) return fDefaultKeymapItem; // Check special mappings first for (size_t i = 0; i < kLanguageKeymapMappingsSize; i += 2) { if (!strcmp(name, kLanguageKeymapMappings[i])) { name = kLanguageKeymapMappings[i + 1]; break; } } BMenu* menu = fKeymapsMenuField->Menu(); for (int32 i = 0; i < menu->CountItems(); i++) { BMenuItem* item = menu->ItemAt(i); BMessage* message = item->Message(); entry_ref ref; if (message->FindRef("ref", &ref) == B_OK && name == ref.name) return item; } return fDefaultKeymapItem; }