root/src/apps/charactermap/CharacterWindow.cpp
/*
 * Copyright 2009-2015, Axel Dörfler, axeld@pinc-software.de.
 * Copyright 2011, Philippe Saint-Pierre, stpere@gmail.com.
 * Distributed under the terms of the MIT License.
 */


#include "CharacterWindow.h"

#include <stdio.h>
#include <string.h>

#include <Application.h>
#include <Button.h>
#include <Catalog.h>
#include <File.h>
#include <FindDirectory.h>
#include <Font.h>
#include <LayoutBuilder.h>
#include <ListView.h>
#include <Menu.h>
#include <MenuBar.h>
#include <MenuItem.h>
#include <MessageFilter.h>
#include <Path.h>
#include <Roster.h>
#include <ScrollView.h>
#include <Slider.h>
#include <StringView.h>
#include <TextControl.h>
#include <UnicodeChar.h>

#include "CharacterView.h"
#include "UnicodeBlockView.h"


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "CharacterWindow"


static const uint32 kMsgUnicodeBlockSelected = 'unbs';
static const uint32 kMsgCharacterChanged = 'chch';
static const uint32 kMsgFontSelected = 'fnts';
static const uint32 kMsgFontSizeChanged = 'fsch';
static const uint32 kMsgPrivateBlocks = 'prbl';
static const uint32 kMsgContainedBlocks = 'cnbl';
static const uint32 kMsgFilterChanged = 'fltr';
static const uint32 kMsgClearFilter = 'clrf';

static const int32 kMinFontSize = 10;
static const int32 kMaxFontSize = 72;


class FontSizeSlider : public BSlider {
public:
        FontSizeSlider(const char* name, const char* label, BMessage* message,
                        int32 min, int32 max)
                : BSlider(name, label, NULL, min, max, B_HORIZONTAL)
        {
                SetModificationMessage(message);
        }

protected:
        const char* UpdateText() const
        {
                snprintf(fText, sizeof(fText), "%" B_PRId32 "pt", Value());
                return fText;
        }

private:
        mutable char    fText[32];
};


class RedirectUpAndDownFilter : public BMessageFilter {
public:
        RedirectUpAndDownFilter(BHandler* target)
                : BMessageFilter(B_ANY_DELIVERY, B_ANY_SOURCE, B_KEY_DOWN),
                fTarget(target)
        {
        }

        virtual filter_result Filter(BMessage* message, BHandler** _target)
        {
                const char* bytes;
                if (message->FindString("bytes", &bytes) != B_OK)
                        return B_DISPATCH_MESSAGE;

                if (bytes[0] == B_UP_ARROW
                        || bytes[0] == B_DOWN_ARROW)
                        *_target = fTarget;

                return B_DISPATCH_MESSAGE;
        }

private:
        BHandler*       fTarget;
};


class EscapeMessageFilter : public BMessageFilter {
public:
        EscapeMessageFilter(uint32 command)
                : BMessageFilter(B_ANY_DELIVERY, B_ANY_SOURCE, B_KEY_DOWN),
                fCommand(command)
        {
        }

        virtual filter_result Filter(BMessage* message, BHandler** /*_target*/)
        {
                const char* bytes;
                if (message->what != B_KEY_DOWN
                        || message->FindString("bytes", &bytes) != B_OK
                        || bytes[0] != B_ESCAPE)
                        return B_DISPATCH_MESSAGE;

                Looper()->PostMessage(fCommand);
                return B_SKIP_MESSAGE;
        }

private:
        uint32  fCommand;
};


CharacterWindow::CharacterWindow()
        :
        BWindow(BRect(100, 100, 700, 550), B_TRANSLATE_SYSTEM_NAME("CharacterMap"),
                B_TITLED_WINDOW, B_ASYNCHRONOUS_CONTROLS | B_QUIT_ON_WINDOW_CLOSE
                        | B_AUTO_UPDATE_SIZE_LIMITS)
{
        BMessage settings;
        _LoadSettings(settings);

        BRect frame;
        if (settings.FindRect("window frame", &frame) == B_OK) {
                MoveTo(frame.LeftTop());
                ResizeTo(frame.Width(), frame.Height());
                MoveOnScreen(B_MOVE_IF_PARTIALLY_OFFSCREEN);
        } else {
                float scaling = be_plain_font->Size() / 12.0f;
                ResizeTo(Frame().Width() * scaling, Frame().Height() * scaling);
                CenterOnScreen();
        }

        // create GUI
        BMenuBar* menuBar = new BMenuBar("menu");

        fFilterControl = new BTextControl(B_TRANSLATE("Filter:"), NULL, NULL);
        fFilterControl->SetModificationMessage(new BMessage(kMsgFilterChanged));

        BButton* clearButton = new BButton("clear", B_TRANSLATE("Clear"),
                new BMessage(kMsgClearFilter));

        fUnicodeBlockView = new UnicodeBlockView("unicodeBlocks");
        fUnicodeBlockView->SetSelectionMessage(
                new BMessage(kMsgUnicodeBlockSelected));

        BScrollView* unicodeScroller = new BScrollView("unicodeScroller",
                fUnicodeBlockView, 0, false, true);

        fCharacterView = new CharacterView("characters");
        fCharacterView->SetTarget(this, kMsgCharacterChanged);

        fGlyphView = new BStringView("glyph", "");
        fGlyphView->SetExplicitMaxSize(BSize(B_SIZE_UNSET,
                fGlyphView->PreferredSize().Height()));

        // TODO: have a context object shared by CharacterView/UnicodeBlockView
        bool show;
        if (settings.FindBool("show private blocks", &show) == B_OK) {
                fCharacterView->ShowPrivateBlocks(show);
                fUnicodeBlockView->ShowPrivateBlocks(show);
        }
        if (settings.FindBool("show contained blocks only", &show) == B_OK) {
                fCharacterView->ShowContainedBlocksOnly(show);
                fUnicodeBlockView->ShowContainedBlocksOnly(show);
        }

        const char* family;
        const char* style;
        BString displayName;

        if (settings.FindString("font family", &family) == B_OK
                && settings.FindString("font style", &style) == B_OK) {
                _SetFont(family, style);
                displayName << family << " " << style;
        } else {
                font_family currentFontFamily;
                font_style currentFontStyle;
                fCharacterView->CharacterFont().GetFamilyAndStyle(&currentFontFamily,
                        &currentFontStyle);
                displayName << currentFontFamily << " " << currentFontStyle;
        }

        int32 fontSize;
        if (settings.FindInt32("font size", &fontSize) == B_OK) {
                BFont font = fCharacterView->CharacterFont();
                if (fontSize < kMinFontSize)
                        fontSize = kMinFontSize;
                else if (fontSize > kMaxFontSize)
                        fontSize = kMaxFontSize;
                font.SetSize(fontSize);

                fCharacterView->SetCharacterFont(font);
                fUnicodeBlockView->SetCharacterFont(font);
        } else
                fontSize = (int32)fCharacterView->CharacterFont().Size();

        BScrollView* characterScroller = new BScrollView("characterScroller",
                fCharacterView, 0, false, true);

        fFontSizeSlider = new FontSizeSlider("fontSizeSlider",
                displayName,
                new BMessage(kMsgFontSizeChanged), kMinFontSize, kMaxFontSize);
        fFontSizeSlider->SetValue(fontSize);

        fCodeView = new BStringView("code", "-");
        fCodeView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED,
                fCodeView->PreferredSize().Height()));

        // Set minimum width for character pane to prevent UI
        // from jumping when longer code strings are displayed.
        // use 'w' character for sizing as it's likely the widest
        // character for a Latin font.  40 characters is a little
        // wider than needed so hopefully this covers other
        // non-Latin fonts that may be wider.
        BFont viewFont;
        fCodeView->GetFont(&viewFont);
        fCharacterView->SetExplicitMinSize(BSize(viewFont.StringWidth("w") * 40,
                B_SIZE_UNSET));

        BLayoutBuilder::Group<>(this, B_VERTICAL, 0)
                .Add(menuBar)
                .AddGroup(B_HORIZONTAL)
                        .SetInsets(B_USE_WINDOW_SPACING)
                        .AddGroup(B_VERTICAL)
                                .AddGroup(B_HORIZONTAL)
                                        .Add(fFilterControl)
                                        .Add(clearButton)
                                .End()
                                .Add(unicodeScroller)
                        .End()
                        .AddGroup(B_VERTICAL)
                                .Add(characterScroller)
                                .Add(fFontSizeSlider)
                                .AddGroup(B_HORIZONTAL)
                                        .Add(fGlyphView)
                                        .Add(fCodeView);

        // Add menu

        // "File" menu
        BMenu* menu = new BMenu(B_TRANSLATE("File"));
        BMenuItem* item;

        menu->AddItem(new BMenuItem(B_TRANSLATE("Quit"),
                new BMessage(B_QUIT_REQUESTED), 'Q'));
        menu->SetTargetForItems(this);
        menuBar->AddItem(menu);

        menu = new BMenu(B_TRANSLATE("View"));
        menu->AddItem(item = new BMenuItem(B_TRANSLATE("Show private blocks"),
                new BMessage(kMsgPrivateBlocks)));
        item->SetMarked(fCharacterView->IsShowingPrivateBlocks());

        menu->AddItem(item = new BMenuItem(
                B_TRANSLATE("Only show blocks contained in font"),
                new BMessage(kMsgContainedBlocks)));
        item->SetMarked(fCharacterView->IsShowingContainedBlocksOnly());
        menuBar->AddItem(menu);

        fFontMenu = _CreateFontMenu();
        menuBar->AddItem(fFontMenu);

        AddCommonFilter(new EscapeMessageFilter(kMsgClearFilter));
        AddCommonFilter(new RedirectUpAndDownFilter(fUnicodeBlockView));

        // TODO: why is this needed?
        fUnicodeBlockView->SetTarget(this);

        fFilterControl->MakeFocus();

        fUnicodeBlockView->SelectBlockForCharacter(0);
}


CharacterWindow::~CharacterWindow()
{
}


void
CharacterWindow::MessageReceived(BMessage* message)
{
        if (message->WasDropped()) {
                const char* text;
                ssize_t size;
                uint32 c;
                if (message->FindInt32("character", (int32*)&c) == B_OK) {
                        fCharacterView->ScrollToCharacter(c);
                        return;
                } else if (message->FindData("text/plain", B_MIME_TYPE,
                                (const void**)&text, &size) == B_OK) {
                        fCharacterView->ScrollToCharacter(BUnicodeChar::FromUTF8(text));
                        return;
                }
        }

        switch (message->what) {
                case B_COPY:
                        PostMessage(message, fCharacterView);
                        break;

                case kMsgUnicodeBlockSelected:
                {
                        int32 index;
                        if (message->FindInt32("index", &index) != B_OK
                                || index < 0)
                                break;

                        BlockListItem* item
                                = static_cast<BlockListItem*>(fUnicodeBlockView->ItemAt(index));
                        fCharacterView->ScrollToBlock(item->BlockIndex());

                        fFilterControl->MakeFocus();
                        break;
                }

                case kMsgCharacterChanged:
                {
                        uint32 character;
                        if (message->FindInt32("character", (int32*)&character) != B_OK)
                                break;

                        char utf8[16];
                        CharacterView::UnicodeToUTF8(character, utf8, sizeof(utf8));

                        char utf8Hex[32];
                        CharacterView::UnicodeToUTF8Hex(character, utf8Hex,
                                sizeof(utf8Hex));

                        char text[128];
                        snprintf(text, sizeof(text), " %s: %#" B_PRIx32 " (%" B_PRId32 "), UTF-8: %s",
                                B_TRANSLATE("Code"), character, character, utf8Hex);

                        char glyph[20];
                        snprintf(glyph, sizeof(glyph), "'%s'", utf8);

                        fGlyphView->SetText(glyph);
                        fCodeView->SetText(text);

                        fUnicodeBlockView->SelectBlockForCharacter(character);
                        break;
                }

                case kMsgFontSelected:
                {
                        BMenuItem* item;

                        if (message->FindPointer("source", (void**)&item) != B_OK)
                                break;

                        fSelectedFontItem->SetMarked(false);

                        // If it's the family menu, just select the first style
                        if (item->Submenu() != NULL) {
                                item->SetMarked(true);
                                item = item->Submenu()->ItemAt(0);
                        }

                        if (item != NULL) {
                                item->SetMarked(true);
                                fSelectedFontItem = item;

                                _SetFont(item->Menu()->Name(), item->Label());

                                BString displayName;
                                displayName << item->Menu()->Name() << " " << item->Label();

                                fFontSizeSlider->SetLabel(displayName);

                                item = item->Menu()->Superitem();
                                item->SetMarked(true);
                        }
                        break;
                }

                case kMsgFontSizeChanged:
                {
                        int32 size = fFontSizeSlider->Value();
                        if (size < kMinFontSize)
                                size = kMinFontSize;
                        else if (size > kMaxFontSize)
                                size = kMaxFontSize;

                        BFont font = fCharacterView->CharacterFont();
                        font.SetSize(size);
                        fCharacterView->SetCharacterFont(font);
                        fUnicodeBlockView->SetCharacterFont(font);
                        break;
                }

                case kMsgPrivateBlocks:
                {
                        BMenuItem* item;
                        if (message->FindPointer("source", (void**)&item) != B_OK
                                || item == NULL)
                                break;

                        item->SetMarked(!item->IsMarked());

                        fCharacterView->ShowPrivateBlocks(item->IsMarked());
                        fUnicodeBlockView->ShowPrivateBlocks(item->IsMarked());
                        break;
                }

                case kMsgContainedBlocks:
                {
                        BMenuItem* item;
                        if (message->FindPointer("source", (void**)&item) != B_OK
                                || item == NULL)
                                break;

                        item->SetMarked(!item->IsMarked());

                        fCharacterView->ShowContainedBlocksOnly(item->IsMarked());
                        fUnicodeBlockView->ShowContainedBlocksOnly(item->IsMarked());
                        break;
                }

                case kMsgFilterChanged:
                        fUnicodeBlockView->SetFilter(fFilterControl->Text());
                        fUnicodeBlockView->Select(0);
                        break;

                case kMsgClearFilter:
                        fFilterControl->SetText("");
                        fFilterControl->MakeFocus();
                        break;

                default:
                        BWindow::MessageReceived(message);
                        break;
        }
}


bool
CharacterWindow::QuitRequested()
{
        _SaveSettings();
        be_app->PostMessage(B_QUIT_REQUESTED);
        return true;
}


status_t
CharacterWindow::_OpenSettings(BFile& file, uint32 mode)
{
        BPath path;
        if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK)
                return B_ERROR;

        path.Append("CharacterMap settings");

        return file.SetTo(path.Path(), mode);
}


status_t
CharacterWindow::_LoadSettings(BMessage& settings)
{
        BFile file;
        status_t status = _OpenSettings(file, B_READ_ONLY);
        if (status != B_OK)
                return status;

        return settings.Unflatten(&file);
}


status_t
CharacterWindow::_SaveSettings()
{
        BFile file;
        status_t status = _OpenSettings(file, B_WRITE_ONLY | B_CREATE_FILE
                | B_ERASE_FILE);
        if (status < B_OK)
                return status;

        BMessage settings('chrm');
        status = settings.AddRect("window frame", Frame());
        if (status != B_OK)
                return status;

        if (status == B_OK) {
                status = settings.AddBool("show private blocks",
                        fCharacterView->IsShowingPrivateBlocks());
        }
        if (status == B_OK) {
                status = settings.AddBool("show contained blocks only",
                        fCharacterView->IsShowingContainedBlocksOnly());
        }

        if (status == B_OK) {
                BFont font = fCharacterView->CharacterFont();
                status = settings.AddInt32("font size", font.Size());

                font_family family;
                font_style style;
                if (status == B_OK)
                        font.GetFamilyAndStyle(&family, &style);
                if (status == B_OK)
                        status = settings.AddString("font family", family);
                if (status == B_OK)
                        status = settings.AddString("font style", style);
        }

        if (status == B_OK)
                status = settings.Flatten(&file);

        return status;
}


void
CharacterWindow::_SetFont(const char* family, const char* style)
{
        BFont font = fCharacterView->CharacterFont();
        font.SetFamilyAndStyle(family, style);

        fCharacterView->SetCharacterFont(font);
        fUnicodeBlockView->SetCharacterFont(font);
        fGlyphView->SetFont(&font, B_FONT_FAMILY_AND_STYLE);
}


BMenu*
CharacterWindow::_CreateFontMenu()
{
        BMenu* menu = new BMenu(B_TRANSLATE("Font"));
        _UpdateFontMenu(menu);

        return menu;
}


void
CharacterWindow::_UpdateFontMenu(BMenu* menu)
{
        BMenuItem* item;

        while (menu->CountItems() > 0) {
                item = menu->RemoveItem(static_cast<int32>(0));
                delete(item);
        }

        font_family currentFamily;
        font_style currentStyle;
        fCharacterView->CharacterFont().GetFamilyAndStyle(&currentFamily,
                &currentStyle);

        int32 numFamilies = count_font_families();

        menu->SetRadioMode(true);

        for (int32 i = 0; i < numFamilies; i++) {
                font_family family;
                if (get_font_family(i, &family) == B_OK) {
                        BMenu* subMenu = new BMenu(family);
                        menu->AddItem(new BMenuItem(subMenu,
                                new BMessage(kMsgFontSelected)));

                        int numStyles = count_font_styles(family);
                        for (int32 j = 0; j < numStyles; j++) {
                                font_style style;
                                uint32 flags;
                                if (get_font_style(family, j, &style, &flags) == B_OK) {
                                        item = new BMenuItem(style, new BMessage(kMsgFontSelected));
                                        subMenu->AddItem(item);

                                        if (!strcmp(family, currentFamily)
                                                && !strcmp(style, currentStyle)) {
                                                fSelectedFontItem = item;
                                                item->SetMarked(true);
                                        }
                                }
                        }
                }
        }

        item = menu->FindItem(currentFamily);
        item->SetMarked(true);
}


void
CharacterWindow::MenusBeginning()
{
        if (update_font_families(false) == true)
                _UpdateFontMenu(fFontMenu);
}