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(¤tRef); 520 node.ReadAttrString("keymap:name", ¤tName); 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