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

#include "ShapeListView.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 "AddPathsCommand.h"
#include "AddShapesCommand.h"
#include "AddStylesCommand.h"
#include "CommandStack.h"
#include "CompoundCommand.h"
#include "Container.h"
#include "FreezeTransformationCommand.h"
#include "MainWindow.h"
#include "MoveShapesCommand.h"
#include "Observer.h"
#include "PathSourceShape.h"
#include "ReferenceImage.h"
#include "RemoveShapesCommand.h"
#include "ResetTransformationCommand.h"
#include "Selection.h"
#include "Shape.h"
#include "Style.h"
#include "Util.h"
#include "VectorPath.h"

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


using std::nothrow;

class ShapeListItem : public SimpleItem, public Observer {
public:
        ShapeListItem(Shape* s, ShapeListView* listView)
                :
                SimpleItem(""),
                shape(NULL),
                fListView(listView)
        {
                SetShape(s);
        }


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


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

        void SetShape(Shape* s)
        {
                if (s == shape)
                        return;

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

                shape = s;

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

        void UpdateText()
        {
                SetText(shape->Name());
                if (fListView->LockLooper()) {
                        fListView->InvalidateItem(fListView->IndexOf(this));
                        fListView->UnlockLooper();
                }
        }

public:
        Shape*                  shape;

private:
        ShapeListView*  fListView;
};


// #pragma mark -


enum {
        MSG_REMOVE                                              = 'rmsh',
        MSG_DUPLICATE                                   = 'dpsh',
        MSG_RESET_TRANSFORMATION                = 'rstr',
        MSG_FREEZE_TRANSFORMATION               = 'frzt',

        MSG_DRAG_SHAPE                                  = 'drgs',
};


ShapeListView::ShapeListView(BRect frame, const char* name, BMessage* message,
        BHandler* target)
        :
        SimpleListView(frame, name, NULL, B_MULTIPLE_SELECTION_LIST),
        fMessage(message),
        fShapeContainer(NULL),
        fStyleContainer(NULL),
        fPathContainer(NULL),
        fCommandStack(NULL)
{
        SetDragCommand(MSG_DRAG_SHAPE);
        SetTarget(target);
}


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

        if (fShapeContainer != NULL)
                fShapeContainer->RemoveListener(this);
}


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

        if (!fSyncingToSelection) {
                ShapeListItem* item
                        = dynamic_cast<ShapeListItem*>(ItemAt(CurrentSelection(0)));
                if (fMessage) {
                        BMessage message(*fMessage);
                        message.AddPointer("shape", item ? (void*)item->shape : NULL);
                        Invoke(&message);
                }
        }

        _UpdateMenu();
}


void
ShapeListView::MessageReceived(BMessage* message)
{
        switch (message->what) {
                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:
                {
                        BList shapes;
                        _GetSelectedShapes(shapes);
                        int32 count = shapes.CountItems();
                        if (count < 0)
                                break;

                        Transformable* transformables[count];
                        for (int32 i = 0; i < count; i++) {
                                Shape* shape = (Shape*)shapes.ItemAtFast(i);
                                transformables[i] = shape;
                        }

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

                        fCommandStack->Perform(command);
                        break;
                }
                
                case MSG_FREEZE_TRANSFORMATION:
                {
                        BList shapes;
                        _GetSelectedShapes(shapes);
                        int32 count = shapes.CountItems();
                        if (count < 0)
                                break;

                        BList pathSourceShapes;

                        for (int i = 0; i < count; i++) {
                                Shape* shape = (Shape*) shapes.ItemAtFast(i);
                                if (dynamic_cast<PathSourceShape*>(shape) != NULL)
                                        pathSourceShapes.AddItem(shape);
                        }

                        count = pathSourceShapes.CountItems();

                        FreezeTransformationCommand* command
                                = new FreezeTransformationCommand(
                                        (PathSourceShape**)pathSourceShapes.Items(),
                                        count);

                        fCommandStack->Perform(command);
                        break;
                }

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


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

        int32 count = CountSelectedItems();
        for (int32 i = 0; i < count; i++) {
                ShapeListItem* item = dynamic_cast<ShapeListItem*>(
                        ItemAt(CurrentSelection(i)));
                if (item == NULL)
                        return B_ERROR;

                PathSourceShape* shape = dynamic_cast<PathSourceShape*>(item->shape);
                if (shape != NULL) {
                        BMessage archive;
                        archive.what = PathSourceShape::archive_code;

                        BMessage styleArchive;
                        if (shape->Style() != NULL) {
                                shape->Style()->Archive(&styleArchive, true);
                                archive.AddMessage("style", &styleArchive);
                        }

                        if (shape->Paths() != NULL) {
                                int32 pathsCount = shape->Paths()->CountItems();
                                for (int32 j = 0; j < pathsCount; j++) {
                                        BMessage pathArchive;
                                        if (shape->Paths()->ItemAt(j) != NULL) {
                                                shape->Paths()->ItemAt(j)->Archive(&pathArchive, true);
                                                archive.AddMessage("path", &pathArchive);
                                        }
                                }
                        }

                        BMessage shapeArchive;
                        shape->Archive(&shapeArchive, true);
                        archive.AddMessage("shape", &shapeArchive);

                        into->AddMessage("shape archive", &archive);
                        continue;
                }

                ReferenceImage* referenceImage = dynamic_cast<ReferenceImage*>(item->shape);
                if (referenceImage != NULL) {
                        BMessage archive;
                        archive.what = ReferenceImage::archive_code;

                        BMessage shapeArchive;
                        referenceImage->Archive(&shapeArchive, true);
                        archive.AddMessage("shape", &shapeArchive);

                        into->AddMessage("shape archive", &archive);
                        continue;
                }
        }

        return B_OK;
}


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

        // Drag may have come from another instance, like in another window.
        // Reconstruct the Shapes from the archive and add them at the drop
        // index.
        int index = 0;
        BList styles;
        BList paths;
        BList shapes;
        while (true) {
                BMessage shapeArchive;
                if (archive->FindMessage("shape archive", index, &shapeArchive) != B_OK)
                        break;

                if (shapeArchive.what == PathSourceShape::archive_code) {
                        // Extract the style
                        BMessage styleArchive;
                        if (shapeArchive.FindMessage("style", &styleArchive) != B_OK)
                                break;

                        Style* style = new Style(&styleArchive);
                        if (style == NULL)
                                break;

                        Style* styleToAssign = style;
                        // Try to find an existing style that is the same as the extracted
                        // style and use that one instead.
                        for (int32 i = 0; i < fStyleContainer->CountItems(); i++) {
                                Style* other = fStyleContainer->ItemAtFast(i);
                                if (*other == *style) {
                                        styleToAssign = other;
                                        delete style;
                                        style = NULL;
                                        break;
                                }
                        }

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

                        // Create the shape using the given style
                        PathSourceShape* shape = new(std::nothrow) PathSourceShape(styleToAssign);
                        if (shape == NULL)
                                break;

                        // Extract the shape archive
                        BMessage shapeMessage;
                        if (shapeArchive.FindMessage("shape", &shapeMessage) != B_OK)
                                break;
                        if (shape->Unarchive(&shapeMessage) != B_OK
                                || !shapes.AddItem(shape)) {
                                delete shape;
                                if (style != NULL) {
                                        styles.RemoveItem(style);
                                        delete style;
                                }
                                break;
                        }

                        // Extract the paths
                        int pathIndex = 0;
                        while (true) {
                                BMessage pathArchive;
                                if (shapeArchive.FindMessage("path", pathIndex, &pathArchive) != B_OK)
                                        break;

                                VectorPath* path = new(nothrow) VectorPath(&pathArchive);
                                if (path == NULL)
                                        break;

                                VectorPath* pathToInclude = path;
                                for (int32 i = 0; i < fPathContainer->CountItems(); i++) {
                                        VectorPath* other = fPathContainer->ItemAtFast(i);
                                        if (*other == *path) {
                                                pathToInclude = other;
                                                delete path;
                                                path = NULL;
                                                break;
                                        }
                                }

                                if (path != NULL && !paths.AddItem(path)) {
                                        delete path;
                                        break;
                                }

                                shape->Paths()->AddItem(pathToInclude);

                                pathIndex++;
                        }
                } else if (shapeArchive.what == ReferenceImage::archive_code) {
                        BMessage shapeMessage;
                        if (shapeArchive.FindMessage("shape", &shapeMessage) != B_OK)
                                break;

                        ReferenceImage* shape = new (std::nothrow) ReferenceImage(&shapeMessage);
                        if (shape == NULL)
                                break;

                        if (shapes.AddItem(shape) != B_OK)
                                break;
                }

                index++;
        }

        int32 shapeCount = shapes.CountItems();
        if (shapeCount == 0)
                return false;

        // TODO: Add allocation checks beyond this point.

        AddStylesCommand* stylesCommand = new(std::nothrow) AddStylesCommand(
                fStyleContainer, (Style**)styles.Items(), styles.CountItems(),
                fStyleContainer->CountItems());

        AddPathsCommand* pathsCommand = new(std::nothrow) AddPathsCommand(
                fPathContainer, (VectorPath**)paths.Items(), paths.CountItems(),
                true, fPathContainer->CountItems());

        AddShapesCommand* shapesCommand = new(std::nothrow) AddShapesCommand(
                fShapeContainer, (Shape**)shapes.Items(), shapeCount, dropIndex);

        ::Command** commands = new(std::nothrow) ::Command*[3];

        commands[0] = stylesCommand;
        commands[1] = pathsCommand;
        commands[2] = shapesCommand;

        CompoundCommand* command = new CompoundCommand(commands, 3,
                B_TRANSLATE("Drop shapes"), -1);

        fCommandStack->Perform(command);

        return true;
}


// #pragma mark -


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

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

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

        MoveShapesCommand* command = new (nothrow) MoveShapesCommand(
                fShapeContainer, shapes, count, toIndex);
        if (command == NULL) {
                delete[] shapes;
                return;
        }

        fCommandStack->Perform(command);
}

// CopyItems
void
ShapeListView::CopyItems(BList& items, int32 toIndex)
{
        if (fCommandStack == NULL || fShapeContainer == NULL)
                return;

        int32 count = items.CountItems();
        Shape* shapes[count];

        for (int32 i = 0; i < count; i++) {
                ShapeListItem* item
                        = dynamic_cast<ShapeListItem*>((BListItem*)items.ItemAtFast(i));
                shapes[i] = item ? item->shape->Clone() : NULL;
        }

        AddShapesCommand* command = new(nothrow) AddShapesCommand(fShapeContainer,
                shapes, count, toIndex);
        if (command == NULL) {
                for (int32 i = 0; i < count; i++)
                        delete shapes[i];
                return;
        }

        fCommandStack->Perform(command);
}


void
ShapeListView::RemoveItemList(BList& items)
{
        if (fCommandStack == NULL || fShapeContainer == NULL)
                return;

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

        RemoveShapesCommand* command = new(nothrow) RemoveShapesCommand(
                fShapeContainer, indices, count);

        fCommandStack->Perform(command);
}


BListItem*
ShapeListView::CloneItem(int32 index) const
{
        ShapeListItem* item = dynamic_cast<ShapeListItem*>(ItemAt(index));
        if (item != NULL) {
                return new ShapeListItem(item->shape,
                        const_cast<ShapeListView*>(this));
        }
        return NULL;
}


int32
ShapeListView::IndexOfSelectable(Selectable* selectable) const
{
        Shape* shape = dynamic_cast<Shape*>(selectable);
        if (shape == NULL) {
                Transformer* transformer = dynamic_cast<Transformer*>(selectable);
                if (transformer == NULL)
                        return -1;
                int32 count = CountItems();
                for (int32 i = 0; i < count; i++) {
                        ShapeListItem* item = dynamic_cast<ShapeListItem*>(ItemAt(i));
                        if (item != NULL && item->shape->Transformers()->HasItem(transformer))
                                return i;
                }
        } else {
                int32 count = CountItems();
                for (int32 i = 0; i < count; i++) {
                        ShapeListItem* item = dynamic_cast<ShapeListItem*>(ItemAt(i));
                        if (item != NULL && item->shape == shape)
                                return i;
                }
        }

        return -1;
}


Selectable*
ShapeListView::SelectableFor(BListItem* item) const
{
        ShapeListItem* shapeItem = dynamic_cast<ShapeListItem*>(item);
        if (shapeItem != NULL)
                return shapeItem->shape;
        return NULL;
}


// #pragma mark -


void
ShapeListView::ItemAdded(Shape* shape, int32 index)
{
        // NOTE: we are in the thread that messed with the
        // ShapeContainer, 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 (_AddShape(shape, index))
                Select(index);

        UnlockLooper();
}


void
ShapeListView::ItemRemoved(Shape* shape)
{
        // NOTE: we are in the thread that messed with the
        // ShapeContainer, 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 Shape objects
        _RemoveShape(shape);

        UnlockLooper();
}


// #pragma mark -


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

        fMenu = menu;

        if (fMenu == NULL)
                return;

        BMessage* message = new BMessage(MSG_ADD_SHAPE);
        fAddEmptyItem = new BMenuItem(B_TRANSLATE("Add empty"), message);

        message = new BMessage(MSG_ADD_SHAPE);
        message->AddBool("path", true);
        fAddWidthPathItem = new BMenuItem(B_TRANSLATE("Add with path"), message);

        message = new BMessage(MSG_ADD_SHAPE);
        message->AddBool("style", true);
        fAddWidthStyleItem = new BMenuItem(B_TRANSLATE("Add with style"), message);

        message = new BMessage(MSG_ADD_SHAPE);
        message->AddBool("path", true);
        message->AddBool("style", true);
        fAddWidthPathAndStyleItem = new BMenuItem(
                B_TRANSLATE("Add with path & style"), message);

        message = new BMessage(MSG_OPEN);
        message->AddBool("reference image", true);
        fAddReferenceImageItem = new BMenuItem(B_TRANSLATE("Add reference image"), message);

        fDuplicateItem = new BMenuItem(B_TRANSLATE("Duplicate"),
                new BMessage(MSG_DUPLICATE));
        fResetTransformationItem = new BMenuItem(B_TRANSLATE("Reset transformation"),
                new BMessage(MSG_RESET_TRANSFORMATION));
        fFreezeTransformationItem = new BMenuItem(
                B_TRANSLATE("Freeze transformation"), 
                new BMessage(MSG_FREEZE_TRANSFORMATION));

        fRemoveItem = new BMenuItem(B_TRANSLATE("Remove"), new BMessage(MSG_REMOVE));


        fMenu->AddItem(fAddEmptyItem);
        fMenu->AddItem(fAddWidthPathItem);
        fMenu->AddItem(fAddWidthStyleItem);
        fMenu->AddItem(fAddWidthPathAndStyleItem);

        fMenu->AddSeparatorItem();

        fMenu->AddItem(fAddReferenceImageItem);

        fMenu->AddSeparatorItem();

        fMenu->AddItem(fDuplicateItem);
        fMenu->AddItem(fResetTransformationItem);
        fMenu->AddItem(fFreezeTransformationItem);
        fMenu->AddSeparatorItem();

        fMenu->AddItem(fRemoveItem);

        fDuplicateItem->SetTarget(this);
        fResetTransformationItem->SetTarget(this);
        fFreezeTransformationItem->SetTarget(this);
        fRemoveItem->SetTarget(this);

        _UpdateMenu();
}


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

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

        _MakeEmpty();

        fShapeContainer = container;

        if (fShapeContainer == NULL)
                return;

        fShapeContainer->AddListener(this);

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


void
ShapeListView::SetStyleContainer(Container<Style>* container)
{
        fStyleContainer = container;
}


void
ShapeListView::SetPathContainer(Container<VectorPath>* container)
{
        fPathContainer = container;
}


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


// #pragma mark -


bool
ShapeListView::_AddShape(Shape* shape, int32 index)
{
        if (shape == NULL)
                return false;
        
        ShapeListItem* item = new(std::nothrow) ShapeListItem(shape, this);
        if (item == NULL)
                return false;
        
        if (!AddItem(item, index)) {
                delete item;
                return false;
        }
        
        return true;
}


bool
ShapeListView::_RemoveShape(Shape* shape)
{
        ShapeListItem* item = _ItemForShape(shape);
        if (item != NULL && RemoveItem(item)) {
                delete item;
                return true;
        }
        return false;
}


ShapeListItem*
ShapeListView::_ItemForShape(Shape* shape) const
{
        int32 count = CountItems();
        for (int32 i = 0; i < count; i++) {
                ShapeListItem* item = dynamic_cast<ShapeListItem*>(ItemAt(i));
                if (item != NULL && item->shape == shape)
                        return item;
        }
        return NULL;
}


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

        bool hasSelection = CurrentSelection(0) >= 0;

        fDuplicateItem->SetEnabled(hasSelection);
        fResetTransformationItem->SetEnabled(hasSelection);
        fFreezeTransformationItem->SetEnabled(hasSelection);
        fRemoveItem->SetEnabled(hasSelection);

        if (hasSelection) {
                bool hasPathSourceShape = false;

                int32 count = CountSelectedItems();
                for (int32 i = 0; i < count; i++) {
                        ShapeListItem* item
                                = dynamic_cast<ShapeListItem*>(ItemAt(CurrentSelection(i)));
                        bool isPathSourceShape
                                = item ? dynamic_cast<PathSourceShape*>(item->shape) != NULL : false;
                        hasPathSourceShape |= isPathSourceShape;
                }

                fFreezeTransformationItem->SetEnabled(hasPathSourceShape);
        }
}


void
ShapeListView::_GetSelectedShapes(BList& shapes) const
{
        int32 count = CountSelectedItems();
        for (int32 i = 0; i < count; i++) {
                ShapeListItem* item = dynamic_cast<ShapeListItem*>(
                        ItemAt(CurrentSelection(i)));
                if (item != NULL && item->shape != NULL) {
                        if (!shapes.AddItem((void*)item->shape))
                                break;
                }
        }
}