root/src/apps/people/PictureView.cpp
/*
 * Copyright 2011, Haiku.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *              Philippe Houdoin
 */


#include "PictureView.h"

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

#include <Alert.h>
#include <Bitmap.h>
#include <BitmapStream.h>
#include <Catalog.h>
#include <Clipboard.h>
#include <Directory.h>
#include <File.h>
#include <FilePanel.h>
#include <IconUtils.h>
#include <LayoutUtils.h>
#include <PopUpMenu.h>
#include <DataIO.h>
#include <MenuItem.h>
#include <Messenger.h>
#include <MimeType.h>
#include <NodeInfo.h>
#include <String.h>
#include <TranslatorRoster.h>
#include <TranslationUtils.h>
#include <Window.h>

#include "PeopleApp.h"  // for B_PERSON_MIMETYPE


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "People"


const uint32 kMsgPopUpMenuClosed = 'pmcl';

class PopUpMenu : public BPopUpMenu {
public:
                                                        PopUpMenu(const char* name, BMessenger target);
        virtual                                 ~PopUpMenu();

private:
                BMessenger                      fTarget;
};


PopUpMenu::PopUpMenu(const char* name, BMessenger target)
        :
        BPopUpMenu(name, false, false), fTarget(target)
{
        SetAsyncAutoDestruct(true);
}


PopUpMenu::~PopUpMenu()
{
        fTarget.SendMessage(kMsgPopUpMenuClosed);
}


// #pragma mark -

using std::nothrow;


const float kPictureMargin = 6.0;

PictureView::PictureView(float width, float height, const entry_ref* ref)
        :
        BView("pictureview", B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE | B_NAVIGABLE),
        fPicture(NULL),
        fOriginalPicture(NULL),
        fDefaultPicture(NULL),
        fShowingPopUpMenu(false),
        fPictureType(0),
        fFocusChanging(false),
        fOpenPanel(new BFilePanel(B_OPEN_PANEL))
{
        SetViewColor(255, 255, 255);

        SetToolTip(B_TRANSLATE(
                "Drop an image here,\n"
                "or use the contextual menu."));

        BSize size(width + 2 * kPictureMargin, height + 2 * kPictureMargin);
        SetExplicitMinSize(size);
        SetExplicitMaxSize(size);

        BMimeType mime(B_PERSON_MIMETYPE);
        uint8* iconData;
        size_t iconDataSize;
        if (mime.GetIcon(&iconData, &iconDataSize) == B_OK) {
                float size = width < height ? width : height;
                fDefaultPicture = new BBitmap(BRect(0, 0, size, size),
                        B_RGB32);
                if (fDefaultPicture->InitCheck() != B_OK
                        || BIconUtils::GetVectorIcon(iconData, iconDataSize,
                                fDefaultPicture) != B_OK) {
                        delete fDefaultPicture;
                        fDefaultPicture = NULL;
                }
        }

        Update(ref);
}


PictureView::~PictureView()
{
        delete fDefaultPicture;
        delete fPicture;
        if (fOriginalPicture != fPicture)
                delete fOriginalPicture;

        delete fOpenPanel;
}


bool
PictureView::HasChanged()
{
        return fPicture != fOriginalPicture;
}


void
PictureView::Revert()
{
        if (!HasChanged())
                return;

        _SetPicture(fOriginalPicture);
}


void
PictureView::Update()
{
        if (fOriginalPicture != fPicture) {
                delete fOriginalPicture;
                fOriginalPicture = fPicture;
        }
}


void
PictureView::Update(const entry_ref* ref)
{
        // Don't update when user has modified the picture
        if (HasChanged())
                return;

        if (_LoadPicture(ref) == B_OK) {
                delete fOriginalPicture;
                fOriginalPicture = fPicture;
        }
}


BBitmap*
PictureView::Bitmap()
{
        return fPicture;
}


uint32
PictureView::SuggestedType()
{
        return fPictureType;
}


const char*
PictureView::SuggestedMIMEType()
{
        if (fPictureMIMEType == "")
                return NULL;

        return fPictureMIMEType.String();
}


void
PictureView::MessageReceived(BMessage* message)
{
        switch (message->what) {
                case B_REFS_RECEIVED:
                case B_SIMPLE_DATA:
                {
                        entry_ref ref;
                        if (message->FindRef("refs", &ref) == B_OK
                                && _LoadPicture(&ref) == B_OK)
                                MakeFocus(true);
                        else
                                _HandleDrop(message);
                        break;
                }

                case B_MIME_DATA:
                        // TODO
                        break;

                case B_COPY_TARGET:
                        _HandleDrop(message);
                        break;

                case B_PASTE:
                {
                        if (be_clipboard->Lock() != B_OK)
                                break;

                        BMessage* data = be_clipboard->Data();
                        BMessage archivedBitmap;
                        if (data->FindMessage("image/bitmap", &archivedBitmap) == B_OK) {
                                BBitmap* picture = new(std::nothrow) BBitmap(&archivedBitmap);
                                _SetPicture(picture);
                        }

                        be_clipboard->Unlock();
                        break;
                }

                case B_DELETE:
                case B_TRASH_TARGET:
                        _SetPicture(NULL);
                        break;

                case kMsgLoadImage:
                        fOpenPanel->SetTarget(BMessenger(this));
                        fOpenPanel->Show();
                        break;

                case kMsgPopUpMenuClosed:
                        fShowingPopUpMenu = false;
                        break;

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


void
PictureView::Draw(BRect updateRect)
{
        BRect rect = Bounds();

        // Draw the outer frame
        rgb_color base = ui_color(B_PANEL_BACKGROUND_COLOR);
        if (IsFocus() && Window() && Window()->IsActive())
                SetHighColor(ui_color(B_KEYBOARD_NAVIGATION_COLOR));
        else
                SetHighColor(tint_color(base, B_DARKEN_3_TINT));
        StrokeRect(rect);

        if (fFocusChanging) {
                // focus frame is already redraw, stop here
                return;
        }

        BBitmap* picture = fPicture ? fPicture : fDefaultPicture;
        if (picture != NULL) {
                // scale to fit and center picture in frame
                BRect frame = rect.InsetByCopy(kPictureMargin, kPictureMargin);
                BRect srcRect = picture->Bounds();
                BSize size = frame.Size();
                float pictureAspect = srcRect.Height() / srcRect.Width();
                float frameAspect = size.height / size.width;

                if (pictureAspect > frameAspect)
                        size.width = srcRect.Width() * size.height / srcRect.Height();
                else if (pictureAspect < frameAspect)
                        size.height = srcRect.Height() * size.width / srcRect.Width();

                fPictureRect = BLayoutUtils::AlignInFrame(frame, size,
                        BAlignment(B_ALIGN_HORIZONTAL_CENTER, B_ALIGN_VERTICAL_CENTER));

                SetDrawingMode(B_OP_ALPHA);
                if (picture == fDefaultPicture) {
                        SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
                        SetHighColor(0, 0, 0, 24);
                }

                DrawBitmapAsync(picture, srcRect, fPictureRect,
                        B_FILTER_BITMAP_BILINEAR);

                SetDrawingMode(B_OP_OVER);
        }
}


void
PictureView::WindowActivated(bool active)
{
        BView::WindowActivated(active);

        if (IsFocus())
                Invalidate();
}


void
PictureView::MakeFocus(bool focused)
{
        if (focused == IsFocus())
                return;

        BView::MakeFocus(focused);

        if (Window()) {
                fFocusChanging = true;
                Invalidate();
                Flush();
                fFocusChanging = false;
        }
}


void
PictureView::MouseDown(BPoint position)
{
        MakeFocus(true);

        uint32 buttons = 0;
        if (Window() != NULL && Window()->CurrentMessage() != NULL)
                buttons = Window()->CurrentMessage()->FindInt32("buttons");

        if (fPicture != NULL && fPictureRect.Contains(position)
                && (buttons
                        & (B_PRIMARY_MOUSE_BUTTON | B_SECONDARY_MOUSE_BUTTON)) != 0) {

                _BeginDrag(position);

        } else if (buttons == B_SECONDARY_MOUSE_BUTTON)
                _ShowPopUpMenu(ConvertToScreen(position));
}


void
PictureView::KeyDown(const char* bytes, int32 numBytes)
{
        if (numBytes != 1) {
                BView::KeyDown(bytes, numBytes);
                return;
        }

        switch (*bytes) {
                case B_DELETE:
                        _SetPicture(NULL);
                        break;

                default:
                        BView::KeyDown(bytes, numBytes);
                        break;
        }
}


// #pragma mark -


void
PictureView::_ShowPopUpMenu(BPoint screen)
{
        if (fShowingPopUpMenu)
                return;

        PopUpMenu* menu = new PopUpMenu("PopUpMenu", this);

        BMenuItem* item = new BMenuItem(B_TRANSLATE("Load image" B_UTF8_ELLIPSIS),
                new BMessage(kMsgLoadImage));
        menu->AddItem(item);

        item = new BMenuItem(B_TRANSLATE("Remove image"), new BMessage(B_DELETE));
        item->SetEnabled(fPicture != NULL);
        menu->AddItem(item);

        menu->SetTargetForItems(this);
        menu->Go(screen, true, true, true);
        fShowingPopUpMenu = true;
}


BBitmap*
PictureView::_CopyPicture(uint8 alpha)
{
        bool hasAlpha = alpha != 255;

        if (!fPicture)
                return NULL;

        BRect rect = fPictureRect.OffsetToCopy(B_ORIGIN);
        BView view(rect, NULL, B_FOLLOW_NONE, B_WILL_DRAW);
        BBitmap* bitmap = new(nothrow) BBitmap(rect, hasAlpha ? B_RGBA32
                : fPicture->ColorSpace(), true);
        if (bitmap == NULL || !bitmap->IsValid()) {
                delete bitmap;
                return NULL;
        }

        if (bitmap->Lock()) {
                bitmap->AddChild(&view);
                if (hasAlpha) {
                        view.SetHighColor(0, 0, 0, 0);
                        view.FillRect(rect);
                        view.SetDrawingMode(B_OP_ALPHA);
                        view.SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_COMPOSITE);
                        view.SetHighColor(0, 0, 0, alpha);
                }
                view.DrawBitmap(fPicture, fPicture->Bounds().OffsetToCopy(B_ORIGIN),
                        rect, B_FILTER_BITMAP_BILINEAR);
                view.Sync();
                bitmap->RemoveChild(&view);
                bitmap->Unlock();
        }

        return bitmap;
}


void
PictureView::_BeginDrag(BPoint sourcePoint)
{
        BBitmap* bitmap = _CopyPicture(128);
        if (bitmap == NULL)
                return;

        // fill the drag message
        BMessage drag(B_SIMPLE_DATA);
        drag.AddInt32("be:actions", B_COPY_TARGET);
        drag.AddInt32("be:actions", B_TRASH_TARGET);

        // name the clip after person name, if any
        BString name = B_TRANSLATE("%name% picture");
        name.ReplaceFirst("%name%", Window() ? Window()->Title() :
                B_TRANSLATE("Unnamed person"));
        drag.AddString("be:clip_name", name.String());

        BTranslatorRoster* roster = BTranslatorRoster::Default();
        if (roster == NULL) {
                delete bitmap;
                return;
        }

        int32 infoCount;
        translator_info* info;
        BBitmapStream stream(bitmap);
        if (roster->GetTranslators(&stream, NULL, &info, &infoCount) == B_OK) {
                for (int32 i = 0; i < infoCount; i++) {
                        const translation_format* formats;
                        int32 count;
                        roster->GetOutputFormats(info[i].translator, &formats, &count);
                        for (int32 j = 0; j < count; j++) {
                                if (strcmp(formats[j].MIME, "image/x-be-bitmap") != 0) {
                                        // needed to send data in message
                                        drag.AddString("be:types", formats[j].MIME);
                                        // needed to pass data via file
                                        drag.AddString("be:filetypes", formats[j].MIME);
                                        drag.AddString("be:type_descriptions", formats[j].name);
                                }
                        }
                }
        }
        stream.DetachBitmap(&bitmap);

        // we also support "Passing Data via File" protocol
        drag.AddString("be:types", B_FILE_MIME_TYPE);

        sourcePoint -= fPictureRect.LeftTop();

        SetMouseEventMask(B_POINTER_EVENTS);

        DragMessage(&drag, bitmap, B_OP_ALPHA, sourcePoint);
        bitmap = NULL;
}


void
PictureView::_HandleDrop(BMessage* msg)
{
        entry_ref dirRef;
        BString name, type;
        bool saveToFile = msg->FindString("be:filetypes", &type) == B_OK
                && msg->FindRef("directory", &dirRef) == B_OK
                && msg->FindString("name", &name) == B_OK;

        bool sendInMessage = !saveToFile
                && msg->FindString("be:types", &type) == B_OK;

        if (!sendInMessage && !saveToFile)
                return;

        BBitmap* bitmap = fPicture;
        if (bitmap == NULL)
                return;

        BTranslatorRoster* roster = BTranslatorRoster::Default();
        if (roster == NULL)
                return;

        BBitmapStream stream(bitmap);

        // find translation format we're asked for
        translator_info* outInfo;
        int32 outNumInfo;
        bool found = false;
        translation_format format;

        if (roster->GetTranslators(&stream, NULL, &outInfo, &outNumInfo) == B_OK) {
                for (int32 i = 0; i < outNumInfo; i++) {
                        const translation_format* formats;
                        int32 formatCount;
                        roster->GetOutputFormats(outInfo[i].translator, &formats,
                                        &formatCount);
                        for (int32 j = 0; j < formatCount; j++) {
                                if (strcmp(formats[j].MIME, type.String()) == 0) {
                                        format = formats[j];
                                        found = true;
                                        break;
                                }
                        }
                }
        }

        if (!found) {
                stream.DetachBitmap(&bitmap);
                return;
        }

        if (sendInMessage) {

                BMessage reply(B_MIME_DATA);
                BMallocIO memStream;
                if (roster->Translate(&stream, NULL, NULL, &memStream,
                        format.type) == B_OK) {
                        reply.AddData(format.MIME, B_MIME_TYPE, memStream.Buffer(),
                                memStream.BufferLength());
                        msg->SendReply(&reply);
                }

        } else {

                BDirectory dir(&dirRef);
                BFile file(&dir, name.String(), B_WRITE_ONLY | B_CREATE_FILE
                        | B_ERASE_FILE);

                if (file.InitCheck() == B_OK
                        && roster->Translate(&stream, NULL, NULL, &file,
                                format.type) == B_OK) {
                        BNodeInfo nodeInfo(&file);
                        if (nodeInfo.InitCheck() == B_OK)
                                nodeInfo.SetType(type.String());
                } else {
                        BString text = B_TRANSLATE("The file '%name%' could not "
                                "be written.");
                        text.ReplaceFirst("%name%", name);
                        BAlert* alert = new BAlert(B_TRANSLATE("Error"), text.String(),
                                B_TRANSLATE("OK"), NULL, NULL, B_WIDTH_AS_USUAL, B_STOP_ALERT);
                        alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
                        alert->Go();
                }
        }

        // Detach, as we don't want our fPicture to be deleted
        stream.DetachBitmap(&bitmap);
}


status_t
PictureView::_LoadPicture(const entry_ref* ref)
{
        BFile file;
        status_t status = file.SetTo(ref, B_READ_ONLY);
        if (status != B_OK)
                return status;

        off_t fileSize;
        status = file.GetSize(&fileSize);
        if (status != B_OK)
                return status;

        // Check that we've at least some data to translate...
        if (fileSize < 1)
                return B_OK;

        translator_info info;
        memset(&info, 0, sizeof(translator_info));
        BMessage ioExtension;

        BTranslatorRoster* roster = BTranslatorRoster::Default();
        if (roster == NULL)
                return B_ERROR;

        status = roster->Identify(&file, &ioExtension, &info, 0, NULL,
                B_TRANSLATOR_BITMAP);

        BBitmapStream stream;

        if (status == B_OK) {
                status = roster->Translate(&file, &info, &ioExtension, &stream,
                        B_TRANSLATOR_BITMAP);
        }
        if (status != B_OK)
                return status;

        BBitmap* picture = NULL;
        if (stream.DetachBitmap(&picture) != B_OK
                || picture == NULL)
                return B_ERROR;

        // Remember image format so we could store using the same
        fPictureMIMEType = info.MIME;
        fPictureType = info.type;

        _SetPicture(picture);
        return B_OK;
}


void
PictureView::_SetPicture(BBitmap* picture)
{
        if (fPicture != fOriginalPicture)
                delete fPicture;

        fPicture = picture;

        if (picture == NULL) {
                fPictureType = 0;
                fPictureMIMEType = "";
        }

        Invalidate();
}