root/src/apps/firstbootprompt/BootPromptWindow.cpp
/*
 * Copyright 2010, Stephan Aßmus <superstippi@gmx.de>
 * Copyright 2010-2021, Adrien Destugues, pulkomandy@pulkomandy.tk.
 * Copyright 2011, Axel Dörfler, axeld@pinc-software.de.
 * Copyright 2020-2021, Panagiotis "Ivory" Vasilopoulos <git@n0toose.net>
 *
 * All rights reserved. Distributed under the terms of the MIT License.
 */


#include "BootPromptWindow.h"

#include <new>
#include <stdio.h>

#include <Alert.h>
#include <Bitmap.h>
#include <Button.h>
#include <Catalog.h>
#include <ControlLook.h>
#include <Directory.h>
#include <Entry.h>
#include <Font.h>
#include <FindDirectory.h>
#include <File.h>
#include <FormattingConventions.h>
#include <IconUtils.h>
#include <IconView.h>
#include <LayoutBuilder.h>
#include <ListView.h>
#include <Locale.h>
#include <Menu.h>
#include <MutableLocaleRoster.h>
#include <ObjectList.h>
#include <Path.h>
#include <Roster.h>
#include <Screen.h>
#include <ScrollView.h>
#include <SeparatorView.h>
#include <StringItem.h>
#include <StringView.h>
#include <TextView.h>
#include <UnicodeChar.h>

#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;

        const BRect iconRect = BRect(BPoint(0, 0), be_control_look->ComposeIconSize(24));
        BBitmap desktopIcon(iconRect, B_RGBA32);
        data = (const uint8_t*)res->LoadResource('VICN', "Desktop", &size);
        BIconUtils::GetVectorIcon(data, size, &desktopIcon);

        BBitmap installerIcon(iconRect, 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<LanguageItem*>(
                                        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 "
                "<www.haiku-os.org>.\"");
        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 "
                        "<www.haiku-os.org>.\"");
        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(&currentRef);
                node.ReadAttrString("keymap:name", &currentName);
        }

        // 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;
}