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: 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 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 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 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 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 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 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 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 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 560 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 590 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* 603 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