root/src/apps/charactermap/CharacterView.cpp
/*
 * Copyright 2009-2010, Axel Dörfler, axeld@pinc-software.de.
 * Distributed under the terms of the MIT License.
 */


#include "CharacterView.h"

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

#include <Bitmap.h>
#include <Catalog.h>
#include <Clipboard.h>
#include <LayoutUtils.h>
#include <MenuItem.h>
#include <PopUpMenu.h>
#include <ScrollBar.h>
#include <Window.h>

#include "UnicodeBlocks.h"

#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "CharacterView"

static const uint32 kMsgCopyAsEscapedString = 'cesc';


CharacterView::CharacterView(const char* name)
        : BView(name, B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE | B_FRAME_EVENTS
                | B_SCROLL_VIEW_AWARE),
        fTargetCommand(0),
        fClickPoint(-1, 0),
        fHasCharacter(false),
        fShowPrivateBlocks(false),
        fShowContainedBlocksOnly(false)
{
        fTitleTops = new int32[kNumUnicodeBlocks];
        fCharacterFont.SetSize(fCharacterFont.Size() * 1.5f);

        _UpdateFontSize();
        DoLayout();
}


CharacterView::~CharacterView()
{
        delete[] fTitleTops;
}


void
CharacterView::SetTarget(BMessenger target, uint32 command)
{
        fTarget = target;
        fTargetCommand = command;
}


void
CharacterView::SetCharacterFont(const BFont& font)
{
        fCharacterFont = font;
        fUnicodeBlocks = fCharacterFont.Blocks();
        InvalidateLayout();
}


void
CharacterView::ShowPrivateBlocks(bool show)
{
        if (fShowPrivateBlocks == show)
                return;

        fShowPrivateBlocks = show;
        InvalidateLayout();
}


void
CharacterView::ShowContainedBlocksOnly(bool show)
{
        if (fShowContainedBlocksOnly == show)
                return;

        fShowContainedBlocksOnly = show;
        InvalidateLayout();
}


bool
CharacterView::IsShowingBlock(int32 blockIndex) const
{
        if (blockIndex < 0 || blockIndex >= (int32)kNumUnicodeBlocks)
                return false;

        if (!fShowPrivateBlocks && kUnicodeBlocks[blockIndex].private_block)
                return false;

        // The reason for two checks is BeOS compatibility.
        // The first one checks for unicode blocks as defined by Be,
        // but there are only 71 such blocks.
        // The rest of the blocks (denoted by kNoBlock) need to
        // be queried by searching for the start and end codepoints
        // via the IncludesBlock method.
        if (fShowContainedBlocksOnly) {
                if (kUnicodeBlocks[blockIndex].block != kNoBlock)
                        return (fUnicodeBlocks & kUnicodeBlocks[blockIndex].block) != kNoBlock;

                if (!fCharacterFont.IncludesBlock(
                                kUnicodeBlocks[blockIndex].start,
                                kUnicodeBlocks[blockIndex].end))
                        return false;
        }

        return true;
}


void
CharacterView::ScrollToBlock(int32 blockIndex)
{
        // don't scroll if the selected block is already in view.
        // this prevents distracting jumps when crossing a block
        // boundary in the character view.
        if (IsBlockVisible(blockIndex))
                return;

        if (blockIndex < 0)
                blockIndex = 0;
        else if (blockIndex >= (int32)kNumUnicodeBlocks)
                blockIndex = kNumUnicodeBlocks - 1;

        BView::ScrollTo(0.0f, fTitleTops[blockIndex]);
}


void
CharacterView::ScrollToCharacter(uint32 c)
{
        if (IsCharacterVisible(c))
                return;

        BRect frame = _FrameFor(c);
        BView::ScrollTo(0.0f, frame.top);
}


bool
CharacterView::IsCharacterVisible(uint32 c) const
{
        return Bounds().Contains(_FrameFor(c));
}


bool
CharacterView::IsBlockVisible(int32 block) const
{
        int32 topBlock = _BlockAt(BPoint(Bounds().left, Bounds().top));
        int32 bottomBlock = _BlockAt(BPoint(Bounds().right, Bounds().bottom));

        if (block >= topBlock && block <= bottomBlock)
                return true;

        return false;
}


/*static*/ void
CharacterView::UnicodeToUTF8(uint32 c, char* text, size_t textSize)
{
        if (textSize < 5) {
                if (textSize > 0)
                        text[0] = '\0';
                return;
        }

        char* s = text;

        if (c < 0x80)
                *(s++) = c;
        else if (c < 0x800) {
                *(s++) = 0xc0 | (c >> 6);
                *(s++) = 0x80 | (c & 0x3f);
        } else if (c < 0x10000) {
                *(s++) = 0xe0 | (c >> 12);
                *(s++) = 0x80 | ((c >> 6) & 0x3f);
                *(s++) = 0x80 | (c & 0x3f);
        } else if (c <= 0x10ffff) {
                *(s++) = 0xf0 | (c >> 18);
                *(s++) = 0x80 | ((c >> 12) & 0x3f);
                *(s++) = 0x80 | ((c >> 6) & 0x3f);
                *(s++) = 0x80 | (c & 0x3f);
        }

        s[0] = '\0';
}


/*static*/ void
CharacterView::UnicodeToUTF8Hex(uint32 c, char* text, size_t textSize)
{
        if (c == 0) {
                snprintf(text, textSize, "\\x00");
                return;
        }

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

        int size = 0;
        for (int32 i = 0; character[i] && size < (int)textSize; i++) {
                size += snprintf(text + size, textSize - size, "\\x%02x",
                        (uint8)character[i]);
        }
}


void
CharacterView::MessageReceived(BMessage* message)
{
        switch (message->what) {
                case kMsgCopyAsEscapedString:
                case B_COPY:
                {
                        uint32 character;
                        if (message->FindInt32("character", (int32*)&character) != B_OK) {
                                if (!fHasCharacter)
                                        break;

                                character = fCurrentCharacter;
                        }

                        char text[17];
                        if (message->what == kMsgCopyAsEscapedString)
                                UnicodeToUTF8Hex(character, text, sizeof(text));
                        else
                                UnicodeToUTF8(character, text, sizeof(text));

                        _CopyToClipboard(text);
                        break;
                }

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


void
CharacterView::AttachedToWindow()
{
        Window()->AddShortcut('C', B_SHIFT_KEY,
                new BMessage(kMsgCopyAsEscapedString), this);
        SetViewUIColor(B_LIST_BACKGROUND_COLOR);
        SetLowColor(ViewColor());
}


void
CharacterView::DetachedFromWindow()
{
}


BSize
CharacterView::MinSize()
{
        return BLayoutUtils::ComposeSize(ExplicitMinSize(),
                BSize(fCharacterWidth, fCharacterHeight + fTitleHeight));
}


void
CharacterView::FrameResized(float width, float height)
{
        // Scroll to character

        if (!fHasTopCharacter)
                return;

        BRect frame = _FrameFor(fTopCharacter);
        if (!frame.IsValid())
                return;

        BView::ScrollTo(0, frame.top - fTopOffset);
        fHasTopCharacter = false;
}


class PreviewItem: public BMenuItem
{
        public:
                PreviewItem(const char* text, float width, float height)
                        : BMenuItem(text, NULL),
                        fWidth(width * 2),
                        fHeight(height * 2)
                {
                }

                void GetContentSize(float* width, float* height)
                {
                        *width = fWidth;
                        *height = fHeight;
                }

                void Draw()
                {
                        BMenu* menu = Menu();
                        BRect box = Frame();

                        menu->PushState();
                        menu->SetLowUIColor(B_DOCUMENT_BACKGROUND_COLOR);
                        menu->SetViewUIColor(B_DOCUMENT_BACKGROUND_COLOR);
                        if (IsEnabled()) {
                                menu->SetHighUIColor(B_DOCUMENT_TEXT_COLOR);
                        } else {
                                rgb_color textColor = ui_color(B_DOCUMENT_TEXT_COLOR);
                                rgb_color backColor = ui_color(B_DOCUMENT_BACKGROUND_COLOR);
                                menu->SetHighColor(disable_color(textColor, backColor));
                        }
                        menu->FillRect(box, B_SOLID_LOW);

                        // Draw the character in the center of the menu
                        float charWidth = menu->StringWidth(Label());
                        font_height fontHeight;
                        menu->GetFontHeight(&fontHeight);

                        box.left += (box.Width() - charWidth) / 2;
                        box.bottom -= (box.Height() - fontHeight.ascent
                                + fontHeight.descent) / 2;

                        menu->DrawString(Label(), BPoint(box.left, box.bottom));

                        menu->PopState();
                }

        private:
                float fWidth;
                float fHeight;
};


class NoMarginMenu: public BPopUpMenu
{
        public:
                NoMarginMenu()
                        : BPopUpMenu(B_EMPTY_STRING, false, false)
                {
                        // Try to have the size right (should be exactly 2x the cell width)
                        // and the item text centered in it.
                        float left, top, bottom, right;
                        GetItemMargins(&left, &top, &bottom, &right);
                        SetItemMargins(left, top, bottom, left);
                }
};


void
CharacterView::MouseDown(BPoint where)
{
        if (!fHasCharacter
                || Window()->CurrentMessage() == NULL)
                return;

        int32 buttons;
        if (Window()->CurrentMessage()->FindInt32("buttons", &buttons) == B_OK) {
                if ((buttons & B_PRIMARY_MOUSE_BUTTON) != 0) {
                        // Memorize click point for dragging
                        fClickPoint = where;

                        char text[5];
                        UnicodeToUTF8(fCurrentCharacter, text, sizeof(text));

                        fMenu = new NoMarginMenu();
                        fMenu->AddItem(new PreviewItem(text, fCharacterWidth - fGap, fCharacterHeight));
                        fMenu->SetFont(&fCharacterFont);
                        fMenu->SetFontSize(fCharacterFont.Size() * 2.5);
                        fMenu->ItemAt(0)->SetEnabled(_HasGlyphForCharacter(text));

                        uint32 character;
                        BRect rect;

                        // Position the menu exactly above the character
                        _GetCharacterAt(where, character, &rect);
                        fMenu->DoLayout();
                        where = rect.LeftTop();
                        where.x += (rect.Width() - fMenu->Frame().Width()) / 2;
                        where.y += (rect.Height() - fMenu->Frame().Height()) / 2;

                        ConvertToScreen(&where);
                        fMenu->Go(where, true, true, true);
                } else {
                        // Show context menu
                        BPopUpMenu* menu = new BPopUpMenu(B_EMPTY_STRING, false, false);
                        menu->SetFont(be_plain_font);

                        BMessage* message =  new BMessage(B_COPY);
                        message->AddInt32("character", fCurrentCharacter);
                        menu->AddItem(new BMenuItem(B_TRANSLATE("Copy character"), message,
                                'C'));

                        message =  new BMessage(kMsgCopyAsEscapedString);
                        message->AddInt32("character", fCurrentCharacter);
                        menu->AddItem(new BMenuItem(
                                B_TRANSLATE("Copy as escaped byte string"),
                                message, 'C', B_SHIFT_KEY));

                        menu->SetTargetForItems(this);

                        ConvertToScreen(&where);
                        menu->Go(where, true, true, true);
                }
        }
}


void
CharacterView::MouseUp(BPoint where)
{
        fClickPoint.x = -1;
}


void
CharacterView::MouseMoved(BPoint where, uint32 transit,
        const BMessage* dragMessage)
{
        if (dragMessage != NULL)
                return;

        BRect frame;
        uint32 character;
        bool hasCharacter = _GetCharacterAt(where, character, &frame);

        if (fHasCharacter && (character != fCurrentCharacter || !hasCharacter))
                Invalidate(fCurrentCharacterFrame);

        if (hasCharacter && (character != fCurrentCharacter || !fHasCharacter)) {
                BMessage update(fTargetCommand);
                update.AddInt32("character", character);
                fTarget.SendMessage(&update);

                Invalidate(frame);
        }

        fHasCharacter = hasCharacter;
        fCurrentCharacter = character;
        fCurrentCharacterFrame = frame;

        if (fClickPoint.x >= 0 && (fabs(where.x - fClickPoint.x) > 4
                        || fabs(where.y - fClickPoint.y) > 4)) {
                // Start dragging

                // Update character - we want to drag the one we originally clicked
                // on, not the one the mouse might be over now.
                if (!_GetCharacterAt(fClickPoint, character, &frame))
                        return;

                BPoint offset = fClickPoint - frame.LeftTop();
                frame.OffsetTo(B_ORIGIN);

                BBitmap* bitmap = new BBitmap(frame, B_BITMAP_ACCEPTS_VIEWS, B_RGBA32);
                if (bitmap->InitCheck() != B_OK) {
                        delete bitmap;
                        return;
                }
                bitmap->Lock();

                BView* view = new BView(frame, "drag", 0, 0);
                bitmap->AddChild(view);

                view->SetLowColor(B_TRANSPARENT_COLOR);
                view->FillRect(frame, B_SOLID_LOW);

                // Draw character
                char text[17];
                UnicodeToUTF8(character, text, sizeof(text));

                view->SetDrawingMode(B_OP_ALPHA);
                view->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_COMPOSITE);
                view->SetFont(&fCharacterFont);
                view->DrawString(text,
                        BPoint((fCharacterWidth - view->StringWidth(text)) / 2,
                                fCharacterBase));

                view->Sync();
                bitmap->RemoveChild(view);
                bitmap->Unlock();

                BMessage drag(B_MIME_DATA);
                if ((modifiers() & (B_SHIFT_KEY | B_OPTION_KEY)) != 0) {
                        // paste UTF-8 hex string
                        CharacterView::UnicodeToUTF8Hex(character, text, sizeof(text));
                }
                drag.AddData("text/plain", B_MIME_DATA, text, strlen(text));

                DragMessage(&drag, bitmap, B_OP_ALPHA, offset);
                fClickPoint.x = -1;

                fHasCharacter = false;
                Invalidate(fCurrentCharacterFrame);
        }
}


void
CharacterView::Draw(BRect updateRect)
{
        BFont font;
        GetFont(&font);

        rgb_color color = ui_color(B_LIST_ITEM_TEXT_COLOR);
        rgb_color highlight = ui_color(B_LIST_SELECTED_BACKGROUND_COLOR);
        rgb_color enclose = mix_color(highlight, ui_color(B_CONTROL_HIGHLIGHT_COLOR), 128);
        rgb_color disabled = tint_color(disable_color(color, ViewColor()),
                color.IsLight() ? B_LIGHTEN_1_TINT : B_DARKEN_2_TINT);
        rgb_color selected = ui_color(B_LIST_SELECTED_ITEM_TEXT_COLOR);
        rgb_color selectedDisabled = tint_color(disable_color(selected, ViewColor()),
                selected.IsLight() ? B_LIGHTEN_1_TINT : B_DARKEN_2_TINT);

        for (int32 i = _BlockAt(updateRect.LeftTop()); i < (int32)kNumUnicodeBlocks;
                        i++) {
                if (!IsShowingBlock(i))
                        continue;

                int32 y = fTitleTops[i];
                if (y > updateRect.bottom)
                        break;

                SetHighColor(color);
                DrawString(kUnicodeBlocks[i].name, BPoint(3, y + fTitleBase));

                y += fTitleHeight;
                int32 x = (int32)fDataRect.left;
                SetFont(&fCharacterFont);

                for (uint32 c = kUnicodeBlocks[i].start; c <= kUnicodeBlocks[i].end;
                                c++) {
                        if (y + fCharacterHeight > updateRect.top
                                && y < updateRect.bottom) {
                                // Stroke frame around the active character
                                bool selection = fHasCharacter && fCurrentCharacter == c;
                                if (selection) {
                                        int32 xInset = fGap / 2;
                                        BRect enclosingRect(x + xInset, y, x - xInset + fCharacterWidth - 1,
                                                y + fCharacterHeight - fGap);
                                        SetHighColor(highlight);
                                        FillRect(enclosingRect);
                                        SetHighColor(enclose);
                                        StrokeRect(enclosingRect);
                                }

                                // Draw character
                                char character[5];
                                UnicodeToUTF8(c, character, sizeof(character));

                                if (selection)
                                        SetHighColor(_HasGlyphForCharacter(character) ? selected : selectedDisabled);
                                else
                                        SetHighColor(_HasGlyphForCharacter(character) ? color : disabled);

                                DrawString(character,
                                        BPoint(x + (fCharacterWidth - StringWidth(character)) / 2,
                                                y + fCharacterBase));
                        }

                        x += fCharacterWidth;
                        if (x + fCharacterWidth > fDataRect.right) {
                                y += fCharacterHeight;
                                x = (int32)fDataRect.left;
                        }
                }

                if (x != fDataRect.left)
                        y += fCharacterHeight;
                y += fTitleGap;

                SetFont(&font);
        }
}


void
CharacterView::DoLayout()
{
        fHasTopCharacter = _GetTopmostCharacter(fTopCharacter, fTopOffset);
        _UpdateSize();
}


int32
CharacterView::_BlockAt(BPoint point) const
{
        uint32 min = 0;
        uint32 max = kNumUnicodeBlocks;
        uint32 guess = (max + min) / 2;

        while ((max >= min) && (guess < kNumUnicodeBlocks - 1 )) {
                if (fTitleTops[guess] <= point.y && fTitleTops[guess + 1] > point.y)
                        return guess;

                if (fTitleTops[guess + 1] <= point.y)
                        min = guess + 1;
                else
                        max = guess - 1;

                guess = (max + min) / 2;
        }

        return -1;
}


bool
CharacterView::_GetCharacterAt(BPoint point, uint32& character,
        BRect* _frame) const
{
        if (point.x < fDataRect.left)
                return false;

        int32 column = (int32)(point.x - fDataRect.left) / fCharacterWidth;
        if (column >= fCharactersPerLine)
                return false;

        int32 i = _BlockAt(point);
        if (i == -1)
                return false;

        int32 y = fTitleTops[i] + fTitleHeight;
        if (y > point.y)
                return false;

        int32 row = ((int32)point.y - y) / fCharacterHeight;
        uint32 c = kUnicodeBlocks[i].start + column + fCharactersPerLine * row;
        if (c <= kUnicodeBlocks[i].end) {
                character = c;

                if (_frame != NULL) {
                        int x = (int32)fDataRect.left + column * fCharacterWidth;
                        y += row * fCharacterHeight;
                        _frame->Set(x, y, x + fCharacterWidth - 1, y + fCharacterHeight - 1);
                }

                return true;
        }

        return false;
}


void
CharacterView::_UpdateFontSize()
{
        font_height fontHeight;
        GetFontHeight(&fontHeight);
        fTitleHeight = (int32)ceilf(fontHeight.ascent + fontHeight.descent
                + fontHeight.leading) + 2;
        fTitleBase = (int32)ceilf(fontHeight.ascent);

        // Find widest character
        fCharacterWidth = (int32)ceilf(fCharacterFont.StringWidth("W") * 1.5f);

        if (fCharacterFont.IsFullAndHalfFixed()) {
                // TODO: improve this!
                fCharacterWidth = (int32)ceilf(fCharacterWidth * 1.4);
        }

        fCharacterFont.GetHeight(&fontHeight);
        fCharacterHeight = (int32)ceilf(fontHeight.ascent + fontHeight.descent
                + fontHeight.leading);
        fCharacterBase = (int32)ceilf(fontHeight.ascent);

        fGap = (int32)roundf(fCharacterHeight / 8.0);
        if (fGap < 3)
                fGap = 3;

        fCharacterWidth += fGap;
        fCharacterHeight += fGap;
        fTitleGap = fGap * 3;
}


void
CharacterView::_UpdateSize()
{
        // Compute data rect

        BRect bounds = Bounds();

        _UpdateFontSize();

        fDataRect.right = bounds.Width();
        fDataRect.bottom = 0;

        fCharactersPerLine = int32(bounds.Width() / fCharacterWidth);
        if (fCharactersPerLine == 0)
                fCharactersPerLine = 1;

        int32 waste = bounds.IntegerWidth() - fCharacterWidth * fCharactersPerLine;
        fCharacterWidth += waste / fCharactersPerLine;
        fDataRect.left = (int)((waste % fCharactersPerLine) / 2);

        for (uint32 i = 0; i < kNumUnicodeBlocks; i++) {
                fTitleTops[i] = (int32)ceilf(fDataRect.bottom);

                if (!IsShowingBlock(i))
                        continue;

                int32 lines = (kUnicodeBlocks[i].Count() + fCharactersPerLine - 1)
                        / fCharactersPerLine;
                fDataRect.bottom += lines * fCharacterHeight + fTitleHeight + fTitleGap;
        }

        // Update scroll bars

        BScrollBar* scroller = ScrollBar(B_VERTICAL);
        if (scroller == NULL)
                return;

        if (bounds.Height() > fDataRect.Height()) {
                // no scrolling
                scroller->SetRange(0.0f, 0.0f);
                scroller->SetValue(0.0f);
        } else {
                scroller->SetRange(0.0f, fDataRect.Height() - bounds.Height() - 1.0f);
                scroller->SetProportion(bounds.Height () / fDataRect.Height());
                scroller->SetSteps(fCharacterHeight,
                        Bounds().Height() - fCharacterHeight);

                // scroll up if there is empty room on bottom
                if (fDataRect.Height() < bounds.bottom)
                        ScrollBy(0.0f, bounds.bottom - fDataRect.Height());
        }

        Invalidate();
}


bool
CharacterView::_GetTopmostCharacter(uint32& character, int32& offset) const
{
        int32 top = (int32)Bounds().top;

        int32 i = _BlockAt(BPoint(0, top));
        if (i == -1)
                return false;

        int32 characterTop = fTitleTops[i] + fTitleHeight;
        if (characterTop > top) {
                character = kUnicodeBlocks[i].start;
                offset = characterTop - top;
                return true;
        }

        int32 lines = (top - characterTop + fCharacterHeight - 1)
                / fCharacterHeight;

        character = kUnicodeBlocks[i].start + lines * fCharactersPerLine;
        offset = top - characterTop - lines * fCharacterHeight;
        return true;
}


BRect
CharacterView::_FrameFor(uint32 character) const
{
        // find block containing the character
        int32 blockNumber = BlockForCharacter(character);

        if (blockNumber >= 0) {
                int32 diff = character - kUnicodeBlocks[blockNumber].start;
                int32 y = fTitleTops[blockNumber] + fTitleHeight
                        + (diff / fCharactersPerLine) * fCharacterHeight;
                int32 x = (diff % fCharactersPerLine) * fCharacterWidth + (int32)fDataRect.left;

                return BRect(x, y, x + fCharacterWidth - 1, y + fCharacterHeight - 1);
        }

        return BRect();
}


void
CharacterView::_CopyToClipboard(const char* text)
{
        if (!be_clipboard->Lock())
                return;

        be_clipboard->Clear();

        BMessage* clip = be_clipboard->Data();
        if (clip != NULL) {
                clip->AddData("text/plain", B_MIME_TYPE, text, strlen(text));
                be_clipboard->Commit();
        }

        be_clipboard->Unlock();
}


bool
CharacterView::_HasGlyphForCharacter(const char* character) const
{
        bool hasGlyph;
        fCharacterFont.GetHasGlyphs(character, 1, &hasGlyph, false);
        return hasGlyph;
}