root/src/apps/icon-o-matic/gui/StyleListView.cpp
/*
 * Copyright 2006-2012, Stephan Aßmus <superstippi@gmx.de>.
 * Copyright 2023, Haiku.
 * All rights reserved. Distributed under the terms of the MIT License.
 *
 * Authors:
 *              Zardshard
 */


#include "StyleListView.h"

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

#include <Application.h>
#include <Catalog.h>
#include <ListItem.h>
#include <Locale.h>
#include <Menu.h>
#include <MenuItem.h>
#include <Message.h>
#include <Mime.h>
#include <Window.h>

#include "AddStylesCommand.h"
#include "AssignStyleCommand.h"
#include "CurrentColor.h"
#include "CommandStack.h"
#include "GradientTransformable.h"
#include "MoveStylesCommand.h"
#include "PathSourceShape.h"
#include "RemoveStylesCommand.h"
#include "Style.h"
#include "Observer.h"
#include "ResetTransformationCommand.h"
#include "Shape.h"
#include "Selection.h"
#include "Util.h"


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


using std::nothrow;

static const float kMarkWidth           = 14.0;
static const float kBorderOffset        = 3.0;
static const float kTextOffset          = 4.0;

enum {
        MSG_ADD                                                 = 'adst',
        MSG_REMOVE                                              = 'rmst',
        MSG_DUPLICATE                                   = 'dpst',
        MSG_RESET_TRANSFORMATION                = 'rstr',
};

class StyleListItem : public SimpleItem,
                                         public Observer {
public:
        StyleListItem(Style* s, StyleListView* listView, bool markEnabled)
                :
                SimpleItem(""),
                style(NULL),
                fListView(listView),
                fMarkEnabled(markEnabled),
                fMarked(false)
        {
                SetStyle(s);
        }

        virtual ~StyleListItem()
        {
                SetStyle(NULL);
        }

        // SimpleItem interface
        virtual void DrawItem(BView* owner, BRect itemFrame, bool even)
        {
                SimpleItem::DrawBackground(owner, itemFrame, even);

                float offset = kBorderOffset + kMarkWidth + kTextOffset;
                SimpleItem::DrawItem(owner, itemFrame.OffsetByCopy(offset, 0), even);

                if (!fMarkEnabled)
                        return;

                // mark
                BRect markRect = itemFrame;
                float markRectBorderTint = B_DARKEN_1_TINT;
                float markRectFillTint = 1.04;
                float markTint = B_DARKEN_4_TINT;
                                        // Dark Themes
                rgb_color lowColor = owner->LowColor();
                if (lowColor.red + lowColor.green + lowColor.blue < 128 * 3) {
                        markRectBorderTint = B_LIGHTEN_2_TINT;
                        markRectFillTint = 0.85;
                        markTint = 0.1;
                }
                markRect.left += kBorderOffset;
                markRect.right = markRect.left + kMarkWidth;
                markRect.top = (markRect.top + markRect.bottom - kMarkWidth) / 2.0;
                markRect.bottom = markRect.top + kMarkWidth;
                owner->SetHighColor(tint_color(owner->LowColor(), markRectBorderTint));
                owner->StrokeRect(markRect);
                markRect.InsetBy(1, 1);
                owner->SetHighColor(tint_color(owner->LowColor(), markRectFillTint));
                owner->FillRect(markRect);
                if (fMarked) {
                        markRect.InsetBy(2, 2);
                        owner->SetHighColor(tint_color(owner->LowColor(),
                                markTint));
                        owner->SetPenSize(2);
                        owner->StrokeLine(markRect.LeftTop(), markRect.RightBottom());
                        owner->StrokeLine(markRect.LeftBottom(), markRect.RightTop());
                        owner->SetPenSize(1);
                }
        }

        // Observer interface
        virtual void    ObjectChanged(const Observable* object)
        {
                UpdateText();
        }

        // StyleListItem
        void SetStyle(Style* s)
        {
                if (s == style)
                        return;

                if (style) {
                        style->RemoveObserver(this);
                        style->ReleaseReference();
                }

                style = s;

                if (style) {
                        style->AcquireReference();
                        style->AddObserver(this);
                        UpdateText();
                }
        }

        void UpdateText()
        {
                SetText(style->Name());
                Invalidate();
        }

        void SetMarkEnabled(bool enabled)
        {
                if (fMarkEnabled == enabled)
                        return;
                fMarkEnabled = enabled;
                Invalidate();
        }

        void SetMarked(bool marked)
        {
                if (fMarked == marked)
                        return;
                fMarked = marked;
                Invalidate();
        }

        void Invalidate()
        {
                if (fListView->LockLooper()) {
                        fListView->InvalidateItem(fListView->IndexOf(this));
                        fListView->UnlockLooper();
                }
        }

public:
        Style*                  style;

private:
        StyleListView*  fListView;
        bool                    fMarkEnabled;
        bool                    fMarked;
};


class ShapeStyleListener : public ShapeListener,
        public ContainerListener<Shape> {
public:
        ShapeStyleListener(StyleListView* listView)
                :
                fListView(listView),
                fShape(NULL)
        {
        }

        virtual ~ShapeStyleListener()
        {
                SetShape(NULL);
        }

        // ShapeListener interface
        virtual void TransformerAdded(Transformer* t, int32 index)
        {
        }
        
        virtual void TransformerRemoved(Transformer* t)
        {
        }

        virtual void StyleChanged(Style* oldStyle, Style* newStyle)
        {
                fListView->_SetStyleMarked(oldStyle, false);
                fListView->_SetStyleMarked(newStyle, true);
        }

        // ContainerListener<Shape> interface
        virtual void ItemAdded(Shape* shape, int32 index)
        {
        }

        virtual void ItemRemoved(Shape* shape)
        {
                fListView->SetCurrentShape(NULL);
        }

        // ShapeStyleListener
        void SetShape(PathSourceShape* shape)
        {
                if (fShape == shape)
                        return;

                if (fShape)
                        fShape->RemoveListener(this);

                fShape = shape;

                if (fShape)
                        fShape->AddListener(this);
        }

        PathSourceShape* CurrentShape() const
        {
                return fShape;
        }

private:
        StyleListView*          fListView;
        PathSourceShape*        fShape;
};


// #pragma mark -


StyleListView::StyleListView(BRect frame, const char* name, BMessage* message,
        BHandler* target)
        :
        SimpleListView(frame, name, NULL, B_SINGLE_SELECTION_LIST),
        fMessage(message),
        fStyleContainer(NULL),
        fShapeContainer(NULL),
        fCommandStack(NULL),
        fCurrentColor(NULL),

        fCurrentShape(NULL),
        fShapeListener(new ShapeStyleListener(this)),

        fMenu(NULL)
{
        SetTarget(target);
}


StyleListView::~StyleListView()
{
        _MakeEmpty();
        delete fMessage;

        if (fStyleContainer != NULL)
                fStyleContainer->RemoveListener(this);

        if (fShapeContainer != NULL)
                fShapeContainer->RemoveListener(fShapeListener);

        delete fShapeListener;
}


// #pragma mark -


void
StyleListView::MessageReceived(BMessage* message)
{
        switch (message->what) {
                case MSG_ADD:
                {
                        Style* style;
                        AddStylesCommand* command;
                        rgb_color color;
                        if (fCurrentColor != NULL)
                                color = fCurrentColor->Color();
                        else {
                                color.red = 0;
                                color.green = 0;
                                color.blue = 0;
                                color.alpha = 255;
                        }
                        new_style(color, fStyleContainer, &style, &command);
                        fCommandStack->Perform(command);
                        break;
                }

                case MSG_REMOVE:
                        RemoveSelected();
                        break;

                case MSG_DUPLICATE:
                {
                        int32 count = CountSelectedItems();
                        int32 index = 0;
                        BList items;
                        for (int32 i = 0; i < count; i++) {
                                index = CurrentSelection(i);
                                BListItem* item = ItemAt(index);
                                if (item)
                                        items.AddItem((void*)item);
                        }
                        CopyItems(items, index + 1);
                        break;
                }

                case MSG_RESET_TRANSFORMATION:
                {
                        int32 count = CountSelectedItems();
                        BList gradients;
                        for (int32 i = 0; i < count; i++) {
                                StyleListItem* item = dynamic_cast<StyleListItem*>(
                                        ItemAt(CurrentSelection(i)));
                                if (item && item->style && item->style->Gradient()) {
                                        if (!gradients.AddItem((void*)item->style->Gradient()))
                                                break;
                                }
                        }
                        count = gradients.CountItems();
                        if (count <= 0)
                                break;

                        Transformable* transformables[count];
                        for (int32 i = 0; i < count; i++) {
                                Gradient* gradient = (Gradient*)gradients.ItemAtFast(i);
                                transformables[i] = gradient;
                        }

                        ResetTransformationCommand* command
                                = new ResetTransformationCommand(transformables, count);

                        fCommandStack->Perform(command);
                        break;
                }

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


void
StyleListView::SelectionChanged()
{
        SimpleListView::SelectionChanged();

        if (!fSyncingToSelection) {
                // NOTE: single selection list
                StyleListItem* item
                        = dynamic_cast<StyleListItem*>(ItemAt(CurrentSelection(0)));
                if (fMessage) {
                        BMessage message(*fMessage);
                        message.AddPointer("style", item ? (void*)item->style : NULL);
                        Invoke(&message);
                }
        }

        _UpdateMenu();
}


void
StyleListView::MouseDown(BPoint where)
{
        if (fCurrentShape == NULL) {
                SimpleListView::MouseDown(where);
                return;
        }

        bool handled = false;
        int32 index = IndexOf(where);
        StyleListItem* item = dynamic_cast<StyleListItem*>(ItemAt(index));
        if (item != NULL) {
                BRect itemFrame(ItemFrame(index));
                itemFrame.right = itemFrame.left + kBorderOffset + kMarkWidth
                        + kTextOffset / 2.0;
                Style* style = item->style;
                if (itemFrame.Contains(where)) {
                        // set the style on the shape
                        if (fCommandStack) {
                                ::Command* command = new AssignStyleCommand(
                                                                                        fCurrentShape, style);
                                fCommandStack->Perform(command);
                        } else {
                                fCurrentShape->SetStyle(style);
                        }
                        handled = true;
                }
        }

        if (!handled)
                SimpleListView::MouseDown(where);
}


status_t
StyleListView::ArchiveSelection(BMessage* into, bool deep) const
{
        into->what = StyleListView::kSelectionArchiveCode;

        int32 count = CountSelectedItems();
        for (int32 i = 0; i < count; i++) {
                StyleListItem* item = dynamic_cast<StyleListItem*>(
                        ItemAt(CurrentSelection(i)));
                if (item != NULL) {
                        BMessage archive;
                        if (item->style->Archive(&archive, true) == B_OK)
                                into->AddMessage("style", &archive);
                } else
                        return B_ERROR;
        }
        return B_OK;
}


bool
StyleListView::InstantiateSelection(const BMessage* archive, int32 dropIndex)
{
        if (archive->what != StyleListView::kSelectionArchiveCode
                || fCommandStack == NULL || fStyleContainer == NULL)
                return false;

        // Drag may have come from another instance, like in another window.
        // Reconstruct the Styles from the archive and add them at the drop
        // index.
        int index = 0;
        BList styles;
        while (true) {
                BMessage styleArchive;
                if (archive->FindMessage("style", index, &styleArchive) != B_OK)
                        break;
                Style* style = new(std::nothrow) Style(&styleArchive);
                if (style == NULL)
                        break;

                if (!styles.AddItem(style)) {
                        delete style;
                        break;
                }

                index++;
        }

        int32 count = styles.CountItems();
        if (count == 0)
                return false;

        AddCommand<Style>* command = new(std::nothrow) AddCommand<Style>(
                fStyleContainer, (Style**)styles.Items(), count, true, dropIndex);

        if (command == NULL) {
                for (int32 i = 0; i < count; i++)
                        delete (Style*)styles.ItemAtFast(i);
                return false;
        }

        fCommandStack->Perform(command);

        return true;
}


void
StyleListView::MoveItems(BList& items, int32 toIndex)
{
        if (fCommandStack == NULL || fStyleContainer == NULL)
                return;

        int32 count = items.CountItems();
        Style** styles = new (nothrow) Style*[count];
        if (styles == NULL)
                return;

        for (int32 i = 0; i < count; i++) {
                StyleListItem* item
                        = dynamic_cast<StyleListItem*>((BListItem*)items.ItemAtFast(i));
                styles[i] = item ? item->style : NULL;
        }

        MoveStylesCommand* command = new (nothrow) MoveStylesCommand(
                fStyleContainer, styles, count, toIndex);
        if (command == NULL) {
                delete[] styles;
                return;
        }

        fCommandStack->Perform(command);
}


void
StyleListView::CopyItems(BList& items, int32 toIndex)
{
        if (fCommandStack == NULL || fStyleContainer == NULL)
                return;

        int32 count = items.CountItems();
        Style* styles[count];

        for (int32 i = 0; i < count; i++) {
                StyleListItem* item
                        = dynamic_cast<StyleListItem*>((BListItem*)items.ItemAtFast(i));
                styles[i] = item ? new (nothrow) Style(*item->style) : NULL;
        }

        AddCommand<Style>* command
                = new (nothrow) AddCommand<Style>(fStyleContainer, styles, count, true, toIndex);
        if (!command) {
                for (int32 i = 0; i < count; i++)
                        delete styles[i];
                return;
        }

        fCommandStack->Perform(command);
}


void
StyleListView::RemoveItemList(BList& items)
{
        if (!fCommandStack || !fStyleContainer)
                return;

        int32 count = items.CountItems();
        int32 indices[count];
        for (int32 i = 0; i < count; i++)
                indices[i] = IndexOf((BListItem*)items.ItemAtFast(i));

        RemoveStylesCommand* command
                = new (nothrow) RemoveStylesCommand(fStyleContainer, indices, count);
        fCommandStack->Perform(command);
}


BListItem*
StyleListView::CloneItem(int32 index) const
{
        if (StyleListItem* item = dynamic_cast<StyleListItem*>(ItemAt(index))) {
                return new StyleListItem(item->style,
                        const_cast<StyleListView*>(this),
                        fCurrentShape != NULL);
        }
        return NULL;
}


int32
StyleListView::IndexOfSelectable(Selectable* selectable) const
{
        Style* style = dynamic_cast<Style*>(selectable);
        if (style == NULL)
                return -1;

        int count = CountItems();
        for (int32 i = 0; i < count; i++) {
                if (SelectableFor(ItemAt(i)) == style)
                        return i;
        }

        return -1;
}


Selectable*
StyleListView::SelectableFor(BListItem* item) const
{
        StyleListItem* styleItem = dynamic_cast<StyleListItem*>(item);
        if (styleItem != NULL)
                return styleItem->style;
        return NULL;
}


// #pragma mark -


void
StyleListView::ItemAdded(Style* style, int32 index)
{
        // NOTE: we are in the thread that messed with the
        // StyleContainer, so no need to lock the
        // container, when this is changed to asynchronous
        // notifications, then it would need to be read-locked!
        if (!LockLooper())
                return;

        if (_AddStyle(style, index))
                Select(index);

        UnlockLooper();
}


void
StyleListView::ItemRemoved(Style* style)
{
        // NOTE: we are in the thread that messed with the
        // StyleContainer, so no need to lock the
        // container, when this is changed to asynchronous
        // notifications, then it would need to be read-locked!
        if (!LockLooper())
                return;

        // NOTE: we're only interested in Style objects
        _RemoveStyle(style);

        UnlockLooper();
}


// #pragma mark -


void
StyleListView::SetMenu(BMenu* menu)
{
        if (fMenu == menu)
                return;

        fMenu = menu;
        if (fMenu == NULL)
                return;

        fAddItem = new BMenuItem(B_TRANSLATE("Add"), new BMessage(MSG_ADD));
        fMenu->AddItem(fAddItem);

        fMenu->AddSeparatorItem();

        fDuplicateItem = new BMenuItem(B_TRANSLATE("Duplicate"),
                new BMessage(MSG_DUPLICATE));
        fMenu->AddItem(fDuplicateItem);

        fResetTransformationItem = new BMenuItem(B_TRANSLATE("Reset transformation"),
                new BMessage(MSG_RESET_TRANSFORMATION));
        fMenu->AddItem(fResetTransformationItem);

        fMenu->AddSeparatorItem();

        fRemoveItem = new BMenuItem(B_TRANSLATE("Remove"), new BMessage(MSG_REMOVE));
        fMenu->AddItem(fRemoveItem);

        fMenu->SetTargetForItems(this);

        _UpdateMenu();
}


void
StyleListView::SetStyleContainer(Container<Style>* container)
{
        if (fStyleContainer == container)
                return;

        // detach from old container
        if (fStyleContainer != NULL)
                fStyleContainer->RemoveListener(this);

        _MakeEmpty();

        fStyleContainer = container;

        if (fStyleContainer == NULL)
                return;

        fStyleContainer->AddListener(this);

        // sync
        int32 count = fStyleContainer->CountItems();
        for (int32 i = 0; i < count; i++)
                _AddStyle(fStyleContainer->ItemAtFast(i), i);
}


void
StyleListView::SetShapeContainer(Container<Shape>* container)
{
        if (fShapeContainer == container)
                return;

        // detach from old container
        if (fShapeContainer)
                fShapeContainer->RemoveListener(fShapeListener);

        fShapeContainer = container;

        if (fShapeContainer)
                fShapeContainer->AddListener(fShapeListener);
}


void
StyleListView::SetCommandStack(CommandStack* stack)
{
        fCommandStack = stack;
}


void
StyleListView::SetCurrentColor(CurrentColor* color)
{
        fCurrentColor = color;
}


void
StyleListView::SetCurrentShape(Shape* shape)
{
        PathSourceShape* pathSourceShape = dynamic_cast<PathSourceShape*>(shape);

        if (fCurrentShape == pathSourceShape)
                return;

        fCurrentShape = pathSourceShape;
        fShapeListener->SetShape(pathSourceShape);

        _UpdateMarks();
}


// #pragma mark -


bool
StyleListView::_AddStyle(Style* style, int32 index)
{
        if (style != NULL) {
                 return AddItem(new StyleListItem(
                        style, this, fCurrentShape != NULL), index);
        }
        return false;
}


bool
StyleListView::_RemoveStyle(Style* style)
{
        StyleListItem* item = _ItemForStyle(style);
        if (item != NULL && RemoveItem(item)) {
                delete item;
                return true;
        }
        return false;
}


StyleListItem*
StyleListView::_ItemForStyle(Style* style) const
{
        int count = CountItems();
        for (int32 i = 0; i < count; i++) {
                StyleListItem* item = dynamic_cast<StyleListItem*>(ItemAt(i));
                if (item == NULL)
                        continue;
                if (item->style == style)
                        return item;
        }
        return NULL;
}


// #pragma mark -


void
StyleListView::_UpdateMarks()
{
        int32 count = CountItems();
        if (fCurrentShape) {
                // enable display of marks and mark items whoes
                // style is contained in fCurrentShape
                for (int32 i = 0; i < count; i++) {
                        StyleListItem* item = dynamic_cast<StyleListItem*>(ItemAt(i));
                        if (item == NULL)
                                continue;
                        item->SetMarkEnabled(true);
                        item->SetMarked(fCurrentShape->Style() == item->style);
                }
        } else {
                // disable display of marks
                for (int32 i = 0; i < count; i++) {
                        StyleListItem* item = dynamic_cast<StyleListItem*>(ItemAt(i));
                        if (item == NULL)
                                continue;
                        item->SetMarkEnabled(false);
                }
        }

        Invalidate();
}


void
StyleListView::_SetStyleMarked(Style* style, bool marked)
{
        StyleListItem* item = _ItemForStyle(style);
        if (item != NULL)
                item->SetMarked(marked);
}


void
StyleListView::_UpdateMenu()
{
        if (fMenu == NULL)
                return;

        bool hasSelection = CurrentSelection(0) >= 0;

        fDuplicateItem->SetEnabled(hasSelection);
        // TODO: only enable fResetTransformationItem if styles
        // with gradients are selected!
        fResetTransformationItem->SetEnabled(hasSelection);
        fRemoveItem->SetEnabled(hasSelection);
}