root/src/apps/people/PersonView.cpp
/*
 * Copyright 2010, Haiku, Inc. All rights reserved.
 * Distributed under the terms of the MIT license.
 *
 * Authors:
 *              Robert Polic
 *              Stephan Aßmus <superstippi@gmx.de>
 *
 * Copyright 1999, Be Incorporated.   All Rights Reserved.
 * This file may be used under the terms of the Be Sample Code License.
 */


#include "PersonView.h"

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

#include <BitmapStream.h>
#include <Catalog.h>
#include <fs_attr.h>
#include <Box.h>
#include <ControlLook.h>
#include <GridLayout.h>
#include <Locale.h>
#include <MenuField.h>
#include <MenuItem.h>
#include <PopUpMenu.h>
#include <Query.h>
#include <TranslationUtils.h>
#include <Translator.h>
#include <VolumeRoster.h>
#include <Window.h>

#include "AttributeTextControl.h"
#include "PictureView.h"


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "People"


PersonView::PersonView(const char* name, const char* categoryAttribute,
                const entry_ref *ref)
        :
        BGridView(),
        fLastModificationTime(0),
        fGroups(NULL),
        fControls(20),
        fCategoryAttribute(categoryAttribute),
        fPictureView(NULL),
        fSaving(false)
{
        SetName(name);
        SetFlags(Flags() | B_WILL_DRAW);

        fRef = ref;
        BFile* file = NULL;
        if (fRef != NULL)
                file = new BFile(fRef, B_READ_ONLY);

        // Add picture "field", using ID photo 35mm x 45mm ratio
        fPictureView = new PictureView(70, 90, ref);

        BGridLayout* layout = GridLayout();

        float spacing = be_control_look->DefaultItemSpacing();
        layout->SetInsets(spacing, spacing, spacing, spacing);

        layout->AddView(fPictureView, 0, 0, 1, 5);
        layout->ItemAt(0, 0)->SetExplicitAlignment(
                BAlignment(B_ALIGN_CENTER, B_ALIGN_TOP));

        if (file != NULL)
                file->GetModificationTime(&fLastModificationTime);
        delete file;
}


PersonView::~PersonView()
{
}


void
PersonView::AddAttribute(const char* label, const char* attribute)
{
        // Check if this attribute has already been added.
        AttributeTextControl* control = NULL;
        for (int32 i = fControls.CountItems() - 1; i >= 0; i--) {
                if (fControls.ItemAt(i)->Attribute() == attribute) {
                        return;
                }
        }

        control = new AttributeTextControl(label, attribute);
        fControls.AddItem(control);

        BGridLayout* layout = GridLayout();
        int32 row = fControls.CountItems();

        if (fCategoryAttribute == attribute) {
                // Special case the category attribute. The Group popup field will
                // be added as the label instead.
                fGroups = new BPopUpMenu(label);
                fGroups->SetRadioMode(false);
                BuildGroupMenu();

                BMenuField* field = new BMenuField("", "", fGroups);
                field->SetEnabled(true);
                layout->AddView(field, 1, row);

                control->SetLabel("");
                layout->AddView(control, 2, row);
        } else {
                layout->AddItem(control->CreateLabelLayoutItem(), 1, row);
                layout->AddItem(control->CreateTextViewLayoutItem(), 2, row);
        }

        SetAttribute(attribute, true);
}


void
PersonView::MakeFocus(bool focus)
{
        if (focus && fControls.CountItems() > 0)
                fControls.ItemAt(0)->MakeFocus();
        else
                BView::MakeFocus(focus);
}


void
PersonView::MessageReceived(BMessage* msg)
{
        switch (msg->what) {
                case M_SAVE:
                        Save();
                        break;

                case M_REVERT:
                        if (fPictureView)
                                fPictureView->Revert();

                        for (int32 i = fControls.CountItems() - 1; i >= 0; i--)
                                fControls.ItemAt(i)->Revert();
                        break;

                case M_SELECT:
                        for (int32 i = fControls.CountItems() - 1; i >= 0; i--) {
                                BTextView* text = fControls.ItemAt(i)->TextView();
                                if (text->IsFocus()) {
                                        text->Select(0, text->TextLength());
                                        break;
                                }
                        }
                        break;

                case M_GROUP_MENU:
                {
                        const char* name = NULL;
                        if (msg->FindString("group", &name) == B_OK)
                                SetAttribute(fCategoryAttribute, name, false);
                        break;
                }

                default:
                        BGridView::MessageReceived(msg);
        }
}


void
PersonView::Draw(BRect updateRect)
{
        if (!fPictureView)
                return;

        // Draw a alert/get info-like strip
        BRect stripeRect = Bounds();
        stripeRect.right = GridLayout()->HorizontalSpacing()
                + fPictureView->Bounds().Width() / 2;
        SetHighColor(tint_color(ViewColor(), B_DARKEN_1_TINT));
        FillRect(stripeRect);
}


void
PersonView::BuildGroupMenu()
{
        if (fGroups == NULL)
                return;

        BMenuItem* item;
        while ((item = fGroups->ItemAt(0)) != NULL) {
                fGroups->RemoveItem(item);
                delete item;
        }

        int32 count = 0;

        BVolumeRoster volumeRoster;
        BVolume volume;
        while (volumeRoster.GetNextVolume(&volume) == B_OK) {
                BQuery query;
                query.SetVolume(&volume);

                char buffer[256];
                snprintf(buffer, sizeof(buffer), "%s=*", fCategoryAttribute.String());
                query.SetPredicate(buffer);
                query.Fetch();

                BEntry entry;
                while (query.GetNextEntry(&entry) == B_OK) {
                        BFile file(&entry, B_READ_ONLY);
                        attr_info info;

                        if (file.InitCheck() == B_OK
                                && file.GetAttrInfo(fCategoryAttribute, &info) == B_OK
                                && info.size > 1) {
                                if (info.size > (off_t)sizeof(buffer))
                                        info.size = sizeof(buffer);

                                if (file.ReadAttr(fCategoryAttribute.String(), B_STRING_TYPE,
                                                0, buffer, info.size) < 0) {
                                        continue;
                                }

                                const char *text = buffer;
                                while (true) {
                                        char* offset = strstr(text, ",");
                                        if (offset != NULL)
                                                offset[0] = '\0';

                                        if (!fGroups->FindItem(text)) {
                                                int32 index = 0;
                                                while ((item = fGroups->ItemAt(index)) != NULL) {
                                                        if (strcmp(text, item->Label()) < 0)
                                                                break;
                                                        index++;
                                                }
                                                BMessage* message = new BMessage(M_GROUP_MENU);
                                                message->AddString("group", text);
                                                fGroups->AddItem(new BMenuItem(text, message), index);
                                                count++;
                                        }
                                        if (offset) {
                                                text = offset + 1;
                                                while (*text == ' ')
                                                        text++;
                                        }
                                        else
                                                break;
                                }
                        }
                }
        }

        if (count == 0) {
                fGroups->AddItem(item = new BMenuItem(
                        B_TRANSLATE_CONTEXT("none", "Groups list"),
                        new BMessage(M_GROUP_MENU)));
                item->SetEnabled(false);
        }

        fGroups->SetTargetForItems(this);
}


void
PersonView::CreateFile(const entry_ref* ref)
{
        fRef = ref;
        Save();
}


bool
PersonView::IsSaved() const
{
        if (fPictureView && fPictureView->HasChanged())
                return false;

        for (int32 i = fControls.CountItems() - 1; i >= 0; i--) {
                if (fControls.ItemAt(i)->HasChanged())
                        return false;
        }

        return true;
}


void
PersonView::Save()
{
        BFile file(fRef, B_READ_WRITE);
        if (file.InitCheck() != B_NO_ERROR)
                return;

        fSaving = true;

        int32 count = fControls.CountItems();
        for (int32 i = 0; i < count; i++) {
                AttributeTextControl* control = fControls.ItemAt(i);
                const char* value = control->Text();
                file.WriteAttr(control->Attribute().String(), B_STRING_TYPE, 0,
                        value, strlen(value) + 1);
                control->Update();
        }

        // Write the picture, if any, in the person file content
        if (fPictureView) {
                // Trim any previous content
                file.Seek(0, SEEK_SET);
                file.SetSize(0);

                BBitmap* picture = fPictureView->Bitmap();
                if (picture) {
                        BBitmapStream stream(picture);
                        // Detach *our* bitmap from stream to avoid its deletion
                        // at stream object destruction
                        stream.DetachBitmap(&picture);

                        BTranslatorRoster* roster = BTranslatorRoster::Default();
                        roster->Translate(&stream, NULL, NULL, &file,
                                fPictureView->SuggestedType(), B_TRANSLATOR_BITMAP,
                                fPictureView->SuggestedMIMEType());

                }

                fPictureView->Update();
        }

        file.GetModificationTime(&fLastModificationTime);

        fSaving = false;
}


const char*
PersonView::AttributeValue(const char* attribute) const
{
        for (int32 i = fControls.CountItems() - 1; i >= 0; i--) {
                if (fControls.ItemAt(i)->Attribute() == attribute)
                        return fControls.ItemAt(i)->Text();
        }

        return "";
}


void
PersonView::SetAttribute(const char* attribute, bool update)
{
        char* value = NULL;
        attr_info info;
        BFile* file = NULL;

        if (fRef != NULL)
                file = new(std::nothrow) BFile(fRef, B_READ_ONLY);

        if (file != NULL && file->GetAttrInfo(attribute, &info) == B_OK) {
                value = (char*)calloc(info.size, 1);
                file->ReadAttr(attribute, B_STRING_TYPE, 0, value, info.size);
        }

        SetAttribute(attribute, value, update);

        free(value);
        delete file;
}


void
PersonView::SetAttribute(const char* attribute, const char* value,
        bool update)
{
        if (!LockLooper())
                return;

        AttributeTextControl* control = NULL;
        for (int32 i = fControls.CountItems() - 1; i >= 0; i--) {
                if (fControls.ItemAt(i)->Attribute() == attribute) {
                        control = fControls.ItemAt(i);
                        break;
                }
        }

        if (control == NULL) {
                UnlockLooper();
                return;
        }

        if (update) {
                control->SetText(value);
                control->Update();
        } else {
                BTextView* text = control->TextView();

                int32 start, end;
                text->GetSelection(&start, &end);
                if (start != end) {
                        text->Delete();
                        text->Insert(value);
                } else if ((end = text->TextLength())) {
                        text->Select(end, end);
                        text->Insert(",");
                        text->Insert(value);
                        text->Select(text->TextLength(), text->TextLength());
                } else
                        control->SetText(value);
        }

        UnlockLooper();
}


void
PersonView::UpdatePicture(const entry_ref* ref)
{
        if (fPictureView == NULL)
                return;

        if (fSaving)
                return;

        time_t modificationTime = 0;
        BEntry entry(ref);
        entry.GetModificationTime(&modificationTime);

        if (entry.InitCheck() == B_OK
                && modificationTime <= fLastModificationTime) {
                return;
        }

        fPictureView->Update(ref);
}


bool
PersonView::IsTextSelected() const
{
        for (int32 i = fControls.CountItems() - 1; i >= 0; i--) {
                BTextView* text = fControls.ItemAt(i)->TextView();

                int32 start, end;
                text->GetSelection(&start, &end);
                if (start != end)
                        return true;
        }
        return false;
}