root/src/apps/icon-o-matic/generic/property/view/PropertyListView.cpp
/*
 * Copyright 2006-2009, Haiku, Inc. All rights reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *              Stephan Aßmus <superstippi@gmx.de>
 */

#include "PropertyListView.h"

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

#include <Catalog.h>
#include <Clipboard.h>
#ifdef __HAIKU__
#  include <LayoutUtils.h>
#endif
#include <Locale.h>
#include <Menu.h>
#include <MenuItem.h>
#include <Message.h>
#include <Window.h>

#include "CommonPropertyIDs.h"
//#include "LanguageManager.h"
#include "Property.h"
#include "PropertyItemView.h"
#include "PropertyObject.h"
#include "Scrollable.h"
#include "Scroller.h"
#include "ScrollView.h"


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "Icon-O-Matic-Properties"


enum {
        MSG_COPY_PROPERTIES             = 'cppr',
        MSG_PASTE_PROPERTIES    = 'pspr',

        MSG_ADD_KEYFRAME                = 'adkf',

        MSG_SELECT_ALL                  = B_SELECT_ALL,
        MSG_SELECT_NONE                 = 'slnn',
        MSG_INVERT_SELECTION    = 'invs',
};

// TabFilter class

class TabFilter : public BMessageFilter {
 public:
        TabFilter(PropertyListView* target)
                : BMessageFilter(B_ANY_DELIVERY, B_ANY_SOURCE),
                  fTarget(target)
                {
                }
        virtual ~TabFilter()
                {
                }
        virtual filter_result   Filter(BMessage* message, BHandler** target)
                {
                        filter_result result = B_DISPATCH_MESSAGE;
                        switch (message->what) {
                                case B_UNMAPPED_KEY_DOWN:
                                case B_KEY_DOWN: {
                                        uint32 key;
                                        uint32 modifiers;
                                        if (message->FindInt32("raw_char", (int32*)&key) >= B_OK
                                                && message->FindInt32("modifiers", (int32*)&modifiers) >= B_OK)
                                                if (key == B_TAB && fTarget->TabFocus(modifiers & B_SHIFT_KEY))
                                                        result = B_SKIP_MESSAGE;
                                        break;
                                }
                                default:
                                        break;
                        }
                        return result;
                }
 private:
        PropertyListView*               fTarget;
};


// constructor
PropertyListView::PropertyListView()
        : BView(BRect(0.0, 0.0, 100.0, 100.0), NULL, B_FOLLOW_NONE,
                        B_WILL_DRAW | B_FRAME_EVENTS | B_NAVIGABLE),
          Scrollable(),
          BList(20),
          fClipboard(new BClipboard("icon-o-matic properties")),

          fPropertyM(NULL),

          fPropertyObject(NULL),
          fSavedProperties(new PropertyObject()),

          fLastClickedItem(NULL),
          fSuspendUpdates(false),

          fMouseWheelFilter(new MouseWheelFilter(this)),
          fTabFilter(new TabFilter(this))
{
        SetLowColor(ui_color(B_LIST_BACKGROUND_COLOR));
        SetHighColor(ui_color(B_LIST_ITEM_TEXT_COLOR));
        SetViewColor(B_TRANSPARENT_32_BIT);
}

// destructor
PropertyListView::~PropertyListView()
{
        delete fClipboard;

        delete fPropertyObject;
        delete fSavedProperties;

        delete fMouseWheelFilter;
        delete fTabFilter;
}

// AttachedToWindow
void
PropertyListView::AttachedToWindow()
{
        Window()->AddCommonFilter(fMouseWheelFilter);
        Window()->AddCommonFilter(fTabFilter);
}

// DetachedFromWindow
void
PropertyListView::DetachedFromWindow()
{
        Window()->RemoveCommonFilter(fTabFilter);
        Window()->RemoveCommonFilter(fMouseWheelFilter);
}

// FrameResized
void
PropertyListView::FrameResized(float width, float height)
{
        SetVisibleSize(width, height);
        Invalidate();
}

// Draw
void
PropertyListView::Draw(BRect updateRect)
{
        if (!fSuspendUpdates)
                FillRect(updateRect, B_SOLID_LOW);
}

// MakeFocus
void
PropertyListView::MakeFocus(bool focus)
{
        if (focus == IsFocus())
                return;

        BView::MakeFocus(focus);
        if (::ScrollView* scrollView = dynamic_cast< ::ScrollView*>(Parent()))
                scrollView->ChildFocusChanged(focus);
}

// MouseDown
void
PropertyListView::MouseDown(BPoint where)
{
        if (!(modifiers() & B_SHIFT_KEY)) {
                DeselectAll();
        }
        MakeFocus(true);
}

// MessageReceived
void
PropertyListView::MessageReceived(BMessage* message)
{
        switch (message->what) {
                case MSG_PASTE_PROPERTIES: {
                        if (!fPropertyObject || !fClipboard->Lock())
                                break;

                        BMessage* data = fClipboard->Data();
                        if (!data) {
                                fClipboard->Unlock();
                                break;
                        }

                        PropertyObject propertyObject;
                        BMessage archive;
                        for (int32 i = 0;
                                 data->FindMessage("property", i, &archive) >= B_OK;
                                 i++) {
                                BArchivable* archivable = instantiate_object(&archive);
                                if (!archivable)
                                        continue;
                                // see if this is actually a property
                                Property* property = dynamic_cast<Property*>(archivable);
                                if (property == NULL || !propertyObject.AddProperty(property))
                                        delete archivable;
                        }
                        if (propertyObject.CountProperties() > 0)
                                PasteProperties(&propertyObject);
                        fClipboard->Unlock();
                        break;
                }
                case MSG_COPY_PROPERTIES: {
                        if (!fPropertyObject || !fClipboard->Lock())
                                break;

                        BMessage* data = fClipboard->Data();
                        if (!data) {
                                fClipboard->Unlock();
                                break;
                        }

                        fClipboard->Clear();
                        for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) {
                                if (!item->IsSelected())
                                        continue;
                                const Property* property = item->GetProperty();
                                if (property) {
                                        BMessage archive;
                                        if (property->Archive(&archive) >= B_OK) {
                                                data->AddMessage("property", &archive);
                                        }
                                }
                        }
                        fClipboard->Commit();
                        fClipboard->Unlock();
                        _CheckMenuStatus();
                        break;
                }

                // property selection
                case MSG_SELECT_ALL:
                        for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) {
                                item->SetSelected(true);
                        }
                        _CheckMenuStatus();
                        break;
                case MSG_SELECT_NONE:
                        for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) {
                                item->SetSelected(false);
                        }
                        _CheckMenuStatus();
                        break;
                case MSG_INVERT_SELECTION:
                        for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) {
                                item->SetSelected(!item->IsSelected());
                        }
                        _CheckMenuStatus();
                        break;

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

#ifdef __HAIKU__

BSize
PropertyListView::MinSize()
{
        // We need a stable min size: the BView implementation uses
        // GetPreferredSize(), which by default just returns the current size.
        return BLayoutUtils::ComposeSize(ExplicitMinSize(), BSize(10, 10));
}


BSize
PropertyListView::MaxSize()
{
        return BView::MaxSize();
}


BSize
PropertyListView::PreferredSize()
{
        // We need a stable preferred size: the BView implementation uses
        // GetPreferredSize(), which by default just returns the current size.
        return BLayoutUtils::ComposeSize(ExplicitPreferredSize(), BSize(100, 50));
}

#endif // __HAIKU__

// #pragma mark -

// TabFocus
bool
PropertyListView::TabFocus(bool shift)
{
        bool result = false;
        PropertyItemView* item = NULL;
        if (IsFocus() && !shift) {
                item = _ItemAt(0);
        } else {
                int32 focussedIndex = -1;
                for (int32 i = 0; PropertyItemView* oldItem = _ItemAt(i); i++) {
                        if (oldItem->IsFocused()) {
                                focussedIndex = shift ? i - 1 : i + 1;
                                break;
                        }
                }
                item = _ItemAt(focussedIndex);
        }
        if (item) {
                item->MakeFocus(true);
                result = true;
        }
        return result;
}

// SetMenu
void
PropertyListView::SetMenu(BMenu* menu)
{
        fPropertyM = menu;
        if (!fPropertyM)
                return;

        fSelectM = new BMenu(B_TRANSLATE("Select"));
        fSelectAllMI = new BMenuItem(B_TRANSLATE("All"), 
                new BMessage(MSG_SELECT_ALL));
        fSelectM->AddItem(fSelectAllMI);
        fSelectNoneMI = new BMenuItem(B_TRANSLATE("None"),
                new BMessage(MSG_SELECT_NONE));
        fSelectM->AddItem(fSelectNoneMI);
        fInvertSelectionMI = new BMenuItem(B_TRANSLATE("Invert selection"),
                new BMessage(MSG_INVERT_SELECTION));
        fSelectM->AddItem(fInvertSelectionMI);
        fSelectM->SetTargetForItems(this);

        fPropertyM->AddItem(fSelectM);

        fPropertyM->AddSeparatorItem();

        fCopyMI = new BMenuItem(B_TRANSLATE("Copy"), 
                new BMessage(MSG_COPY_PROPERTIES));
        fPropertyM->AddItem(fCopyMI);
        fPasteMI = new BMenuItem(B_TRANSLATE("Paste"), 
                new BMessage(MSG_PASTE_PROPERTIES));
        fPropertyM->AddItem(fPasteMI);

        fPropertyM->SetTargetForItems(this);

        // disable menus
        _CheckMenuStatus();
}


// ScrollView
::ScrollView*
PropertyListView::ScrollView() const
{
        return dynamic_cast< ::ScrollView*>(ScrollSource());
}

// #pragma mark -

// SetTo
void
PropertyListView::SetTo(PropertyObject* object)
{
        // try to do without rebuilding the list
        // it should in fact be pretty unlikely that this does not
        // work, but we keep being defensive
        if (fPropertyObject && object &&
                fPropertyObject->ContainsSameProperties(*object)) {
                // iterate over view items and update their value views
                bool error = false;
                for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) {
                        Property* property = object->PropertyAt(i);
                        if (!item->AdoptProperty(property)) {
                                // the reason for this can be that the property is
                                // unkown to the PropertyEditorFactory and therefor
                                // there is no editor view at this item
                                fprintf(stderr, "PropertyListView::_SetTo() - "
                                                                "property mismatch at %" B_PRId32 "\n", i);
                                error = true;
                                break;
                        }
                        if (property)
                                item->SetEnabled(property->IsEditable());
                }
                // we didn't need to make empty, but transfer ownership
                // of the object
                if (!error) {
                        // if the "adopt" process went only halfway,
                        // some properties of the original object
                        // are still referenced, so we can only
                        // delete the original object if the process
                        // was successful and leak Properties otherwise,
                        // but this case is only theoretical anyways...
                        delete fPropertyObject;
                }
                fPropertyObject = object;
        } else {
                // remember scroll pos, selection and focused item
                BPoint scrollOffset = ScrollOffset();
                BList selection(20);
                int32 focused = -1;
                for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) {
                        if (item->IsSelected())
                                selection.AddItem((void*)(long)i);
                        if (item->IsFocused())
                                focused = i;
                }
                if (Window())
                        Window()->BeginViewTransaction();
                fSuspendUpdates = true;

                // rebuild list
                _MakeEmpty();
                fPropertyObject = object;

                if (fPropertyObject) {
                        // fill with content
                        for (int32 i = 0; Property* property = fPropertyObject->PropertyAt(i); i++) {
                                PropertyItemView* item = new PropertyItemView(property);
                                item->SetEnabled(property->IsEditable());
                                _AddItem(item);
                        }
                        _LayoutItems();

                        // restore scroll pos, selection and focus
                        SetScrollOffset(scrollOffset);
                        for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) {
                                if (selection.HasItem((void*)(long)i))
                                        item->SetSelected(true);
                                if (i == focused)
                                        item->MakeFocus(true);
                        }
                }

                if (Window())
                        Window()->EndViewTransaction();
                fSuspendUpdates = false;

                SetDataRect(_ItemsRect());
        }

        _UpdateSavedProperties();
        _CheckMenuStatus();
        Invalidate();
}

// PropertyChanged
void
PropertyListView::PropertyChanged(const Property* previous,
                                                                  const Property* current)
{
        printf("PropertyListView::PropertyChanged(%s)\n",
                name_for_id(current->Identifier()));
}

// PasteProperties
void
PropertyListView::PasteProperties(const PropertyObject* object)
{
        if (!fPropertyObject)
                return;

        // default implementation is to adopt the pasted properties
        int32 count = object->CountProperties();
        for (int32 i = 0; i < count; i++) {
                Property* p = object->PropertyAtFast(i);
                Property* local = fPropertyObject->FindProperty(p->Identifier());
                if (local)
                        local->SetValue(p);
        }
}

// IsEditingMultipleObjects
bool
PropertyListView::IsEditingMultipleObjects()
{
        return false;
}

// #pragma mark -

// UpdateObject
void
PropertyListView::UpdateObject(uint32 propertyID)
{
        Property* previous = fSavedProperties->FindProperty(propertyID);
        Property* current = fPropertyObject->FindProperty(propertyID);
        if (previous && current) {
                // call hook function
                PropertyChanged(previous, current);
                // update saved property if it is still contained
                // in the saved properties (if not, the notification
                // mechanism has caused to update the properties
                // and "previous" and "current" are toast)
                if (fSavedProperties->HasProperty(previous)
                        && fPropertyObject->HasProperty(current))
                        previous->SetValue(current);
        }
}

// ScrollOffsetChanged
void
PropertyListView::ScrollOffsetChanged(BPoint oldOffset, BPoint newOffset)
{
        ScrollBy(newOffset.x - oldOffset.x,
                         newOffset.y - oldOffset.y);
}

// Select
void
PropertyListView::Select(PropertyItemView* item)
{
        if (item) {
                if (modifiers() & B_SHIFT_KEY) {
                        item->SetSelected(!item->IsSelected());
                } else if (modifiers() & B_OPTION_KEY) {
                        item->SetSelected(true);
                        int32 firstSelected = _CountItems();
                        int32 lastSelected = -1;
                        for (int32 i = 0; PropertyItemView* otherItem = _ItemAt(i); i++) {
                                if (otherItem->IsSelected()) {
                                         if (i < firstSelected)
                                                firstSelected = i;
                                         if (i > lastSelected)
                                                lastSelected = i;
                                }
                        }
                        if (lastSelected > firstSelected) {
                                for (int32 i = firstSelected; PropertyItemView* otherItem = _ItemAt(i); i++) {
                                        if (i > lastSelected)
                                                break;
                                        otherItem->SetSelected(true);
                                }
                        }
                } else {
                        for (int32 i = 0; PropertyItemView* otherItem = _ItemAt(i); i++) {
                                otherItem->SetSelected(otherItem == item);
                        }
                }
        }
        _CheckMenuStatus();
}

// DeselectAll
void
PropertyListView::DeselectAll()
{
        for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) {
                item->SetSelected(false);
        }
        _CheckMenuStatus();
}

// Clicked
void
PropertyListView::Clicked(PropertyItemView* item)
{
        fLastClickedItem = item;
}

// DoubleClicked
void
PropertyListView::DoubleClicked(PropertyItemView* item)
{
        if (fLastClickedItem == item) {
                printf("implement PropertyListView::DoubleClicked()\n");
        }
        fLastClickedItem = NULL;
}

// #pragma mark -

// _UpdateSavedProperties
void
PropertyListView::_UpdateSavedProperties()
{
        fSavedProperties->DeleteProperties();

        if (!fPropertyObject)
                return;

        int32 count = fPropertyObject->CountProperties();
        for (int32 i = 0; i < count; i++) {
                const Property* p = fPropertyObject->PropertyAtFast(i);
                fSavedProperties->AddProperty(p->Clone());
        }
}

// _AddItem
bool
PropertyListView::_AddItem(PropertyItemView* item)
{
        if (item && BList::AddItem((void*)item)) {
//              AddChild(item);
// NOTE: for now added in _LayoutItems()
                item->SetListView(this);
                return true;
        }
        return false;
}

// _RemoveItem
PropertyItemView*
PropertyListView::_RemoveItem(int32 index)
{
        PropertyItemView* item = (PropertyItemView*)BList::RemoveItem(index);
        if (item) {
                item->SetListView(NULL);
                if (!RemoveChild(item))
                        fprintf(stderr, "failed to remove view in PropertyListView::_RemoveItem()\n");
        }
        return item;
}

// _ItemAt
PropertyItemView*
PropertyListView::_ItemAt(int32 index) const
{
        return (PropertyItemView*)BList::ItemAt(index);
}

// _CountItems
int32
PropertyListView::_CountItems() const
{
        return BList::CountItems();
}

// _MakeEmpty
void
PropertyListView::_MakeEmpty()
{
        int32 count = _CountItems();
        while (PropertyItemView* item = _RemoveItem(count - 1)) {
                delete item;
                count--;
        }
        delete fPropertyObject;
        fPropertyObject = NULL;

        SetScrollOffset(BPoint(0.0, 0.0));
}

// _ItemsRect
BRect
PropertyListView::_ItemsRect() const
{
        float width = Bounds().Width();
        float height = -1.0;
        for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) {
                height += item->PreferredHeight() + 1.0;
        }
        if (height < 0.0)
                height = 0.0;
        return BRect(0.0, 0.0, width, height);
}

// _LayoutItems
void
PropertyListView::_LayoutItems()
{
        // figure out maximum label width
        float labelWidth = 0.0;
        for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) {
                if (item->PreferredLabelWidth() > labelWidth)
                        labelWidth = item->PreferredLabelWidth();
        }
        labelWidth = ceilf(labelWidth);
        // layout items
        float top = 0.0;
        float width = Bounds().Width();
        for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) {
                item->MoveTo(BPoint(0.0, top));
                float height = item->PreferredHeight();
                item->SetLabelWidth(labelWidth);
                item->ResizeTo(width, height);
                item->FrameResized(item->Bounds().Width(),
                                                   item->Bounds().Height());
                top += height + 1.0;

                AddChild(item);
        }
}

// _CheckMenuStatus
void
PropertyListView::_CheckMenuStatus()
{
        if (!fPropertyM || fSuspendUpdates)
                return;

        if (!fPropertyObject) {
                fPropertyM->SetEnabled(false);
                return;
        } else
                fPropertyM->SetEnabled(false);

        bool gotSelection = false;
        for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) {
                if (item->IsSelected()) {
                        gotSelection = true;
                        break;
                }
        }
        fCopyMI->SetEnabled(gotSelection);

        bool clipboardHasData = false;
        if (fClipboard->Lock()) {
                if (BMessage* data = fClipboard->Data()) {
                        clipboardHasData = data->HasMessage("property");
                }
                fClipboard->Unlock();
        }

        fPasteMI->SetEnabled(clipboardHasData);
        if (IsEditingMultipleObjects())
                fPasteMI->SetLabel(B_TRANSLATE("Multi-paste"));
        else
                fPasteMI->SetLabel(B_TRANSLATE("Paste"));

        bool enableMenu = fPropertyObject;
        if (fPropertyM->IsEnabled() != enableMenu)
                fPropertyM->SetEnabled(enableMenu);

        bool gotItems = _CountItems() > 0;
        fSelectM->SetEnabled(gotItems);
}