root/src/apps/icon-o-matic/gui/PathListView.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 "PathListView.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 "CleanUpPathCommand.h"
#include "CommandStack.h"
#include "MovePathsCommand.h"
#include "Observer.h"
#include "PathSourceShape.h"
#include "RemovePathsCommand.h"
#include "ReversePathCommand.h"
#include "RotatePathIndicesCommand.h"
#include "Shape.h"
#include "Selection.h"
#include "UnassignPathCommand.h"
#include "Util.h"
#include "VectorPath.h"


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


using std::nothrow;

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


class PathListItem : public SimpleItem, public Observer {
public:
        PathListItem(VectorPath* p, PathListView* listView, bool markEnabled)
                :
                SimpleItem(""),
                path(NULL),
                fListView(listView),
                fMarkEnabled(markEnabled),
                fMarked(false)
        {
                SetPath(p);
        }


        virtual ~PathListItem()
        {
                SetPath(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();
        }


        // PathListItem
        void SetPath(VectorPath* p)
        {
                if (p == path)
                        return;

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

                path = p;

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


        void UpdateText()
        {
                SetText(path->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:
        VectorPath*     path;

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


class ShapePathListener : public ContainerListener<VectorPath>,
        public ContainerListener<Shape> {
public:
        ShapePathListener(PathListView* listView)
                :
                fListView(listView),
                fShape(NULL)
        {
        }


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


        // ContainerListener<VectorPath> interface
        virtual void ItemAdded(VectorPath* path, int32 index)
        {
                fListView->_SetPathMarked(path, true);
        }


        virtual void ItemRemoved(VectorPath* path)
        {
                fListView->_SetPathMarked(path, false);
        }


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


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


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

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

                fShape = shape;

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


        Shape* CurrentShape() const
        {
                return fShape;
        }

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


// #pragma mark -


enum {
        MSG_ADD                                 = 'addp',

        MSG_ADD_RECT                    = 'addr',
        MSG_ADD_CIRCLE                  = 'addc',
        MSG_ADD_ARC                             = 'adda',

        MSG_DUPLICATE                   = 'dupp',

        MSG_REVERSE                             = 'rvrs',
        MSG_CLEAN_UP                    = 'clup',
        MSG_ROTATE_INDICES_CW   = 'ricw',
        MSG_ROTATE_INDICES_CCW  = 'ricc',

        MSG_REMOVE                              = 'remp',
};


PathListView::PathListView(BRect frame, const char* name, BMessage* message,
        BHandler* target)
        :
        SimpleListView(frame, name, NULL, B_SINGLE_SELECTION_LIST),
        fMessage(message),
        fMenu(NULL),

        fPathContainer(NULL),
        fShapeContainer(NULL),
        fCommandStack(NULL),

        fCurrentShape(NULL),
        fShapePathListener(new ShapePathListener(this))
{
        SetTarget(target);
}


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

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

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

        delete fShapePathListener;
}


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

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

        _UpdateMenu();
}


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

        bool handled = false;
        int32 index = IndexOf(where);
        PathListItem* item = dynamic_cast<PathListItem*>(ItemAt(index));
        if (item != NULL) {
                BRect itemFrame(ItemFrame(index));
                itemFrame.right = itemFrame.left + kBorderOffset + kMarkWidth
                        + kTextOffset / 2.0;

                VectorPath* path = item->path;
                if (itemFrame.Contains(where) && fCommandStack) {
                        // add or remove the path to the shape
                        ::Command* command;
                        if (fCurrentShape->Paths()->HasItem(path)) {
                                command = new UnassignPathCommand(fCurrentShape, path);
                        } else {
                                VectorPath* paths[1];
                                paths[0] = path;
                                command = new AddPathsCommand(fCurrentShape->Paths(),
                                        paths, 1, false, fCurrentShape->Paths()->CountItems());
                        }
                        fCommandStack->Perform(command);
                        handled = true;
                }
        }

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


void
PathListView::MessageReceived(BMessage* message)
{
        switch (message->what) {
                case MSG_ADD:
                        if (fCommandStack != NULL) {
                                VectorPath* path;
                                AddPathsCommand* command;
                                new_path(fPathContainer, &path, &command);
                                fCommandStack->Perform(command);
                        }
                        break;

                case MSG_ADD_RECT:
                        if (fCommandStack != NULL) {
                                VectorPath* path;
                                AddPathsCommand* command;
                                new_path(fPathContainer, &path, &command);
                                if (path != NULL) {
                                        path->AddPoint(BPoint(16, 16));
                                        path->AddPoint(BPoint(16, 48));
                                        path->AddPoint(BPoint(48, 48));
                                        path->AddPoint(BPoint(48, 16));
                                        path->SetClosed(true);
                                }
                                fCommandStack->Perform(command);
                        }
                        break;

                case MSG_ADD_CIRCLE:
                        // TODO: ask for number of secions
                        if (fCommandStack != NULL) {
                                VectorPath* path;
                                AddPathsCommand* command;
                                new_path(fPathContainer, &path, &command);
                                if (path != NULL) {
                                        // add four control points defining a circle:
                                        //   a 
                                        // b   d
                                        //   c
                                        BPoint a(32, 16);
                                        BPoint b(16, 32);
                                        BPoint c(32, 48);
                                        BPoint d(48, 32);
                                        
                                        path->AddPoint(a);
                                        path->AddPoint(b);
                                        path->AddPoint(c);
                                        path->AddPoint(d);
                        
                                        path->SetClosed(true);
                        
                                        float controlDist = 0.552284 * 16;
                                        path->SetPoint(0, a, a + BPoint(controlDist, 0.0),
                                                                                 a + BPoint(-controlDist, 0.0), true);
                                        path->SetPoint(1, b, b + BPoint(0.0, -controlDist),
                                                                                 b + BPoint(0.0, controlDist), true);
                                        path->SetPoint(2, c, c + BPoint(-controlDist, 0.0),
                                                                                 c + BPoint(controlDist, 0.0), true);
                                        path->SetPoint(3, d, d + BPoint(0.0, controlDist),
                                                                                 d + BPoint(0.0, -controlDist), true);
                                }
                                fCommandStack->Perform(command);
                        }
                        break;

                case MSG_DUPLICATE:
                        if (fCommandStack != NULL) {
                                PathListItem* item = dynamic_cast<PathListItem*>(
                                        ItemAt(CurrentSelection(0)));
                                if (item == NULL)
                                        break;

                                VectorPath* path;
                                AddPathsCommand* command;
                                new_path(fPathContainer, &path, &command, item->path);
                                fCommandStack->Perform(command);
                        }
                        break;

                case MSG_REVERSE:
                        if (fCommandStack != NULL) {
                                PathListItem* item = dynamic_cast<PathListItem*>(
                                        ItemAt(CurrentSelection(0)));
                                if (item == NULL)
                                        break;

                                ReversePathCommand* command
                                        = new (nothrow) ReversePathCommand(item->path);
                                fCommandStack->Perform(command);
                        }
                        break;

                case MSG_CLEAN_UP:
                        if (fCommandStack != NULL) {
                                PathListItem* item = dynamic_cast<PathListItem*>(
                                        ItemAt(CurrentSelection(0)));
                                if (item == NULL)
                                        break;

                                CleanUpPathCommand* command
                                        = new (nothrow) CleanUpPathCommand(item->path);
                                fCommandStack->Perform(command);
                        }
                        break;

                case MSG_ROTATE_INDICES_CW:
                case MSG_ROTATE_INDICES_CCW:
                        if (fCommandStack != NULL) {
                                PathListItem* item = dynamic_cast<PathListItem*>(
                                        ItemAt(CurrentSelection(0)));
                                if (item == NULL)
                                        break;

                                RotatePathIndicesCommand* command
                                        = new (nothrow) RotatePathIndicesCommand(item->path,
                                        message->what == MSG_ROTATE_INDICES_CW);
                                fCommandStack->Perform(command);
                        }
                        break;

                case MSG_REMOVE:
                        RemoveSelected();
                        break;

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


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

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

        return B_OK;
}


bool
PathListView::InstantiateSelection(const BMessage* archive, int32 dropIndex)
{
        if (archive->what != PathListView::kSelectionArchiveCode
                || fCommandStack == NULL || fPathContainer == 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 paths;
        while (true) {
                BMessage pathArchive;
                if (archive->FindMessage("path", index, &pathArchive) != B_OK)
                        break;

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

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

                index++;
        }

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

        AddPathsCommand* command = new(nothrow) AddPathsCommand(fPathContainer,
                (VectorPath**)paths.Items(), count, true, dropIndex);
        if (command == NULL) {
                for (int32 i = 0; i < count; i++)
                        delete (VectorPath*)paths.ItemAtFast(i);
                return false;
        }

        fCommandStack->Perform(command);

        return true;
}


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

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

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

        MovePathsCommand* command = new (nothrow) MovePathsCommand(fPathContainer,
                paths, count, toIndex);
        if (command == NULL) {
                delete[] paths;
                return;
        }

        fCommandStack->Perform(command);
}


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

        int32 count = items.CountItems();
        VectorPath* paths[count];

        for (int32 i = 0; i < count; i++) {
                PathListItem* item
                        = dynamic_cast<PathListItem*>((BListItem*)items.ItemAtFast(i));
                paths[i] = item ? new (nothrow) VectorPath(*item->path) : NULL;
        }

        AddPathsCommand* command = new(nothrow) AddPathsCommand(fPathContainer,
                paths, count, true, toIndex);
        if (command == NULL) {
                for (int32 i = 0; i < count; i++)
                        delete paths[i];
                return;
        }

        fCommandStack->Perform(command);
}


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

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

        RemovePathsCommand* command = new (nothrow) RemovePathsCommand(
                fPathContainer, indices, count);

        fCommandStack->Perform(command);
}


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


int32
PathListView::IndexOfSelectable(Selectable* selectable) const
{
        VectorPath* path = dynamic_cast<VectorPath*>(selectable);
        if (path == NULL)
                return -1;

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

        return -1;
}


Selectable*
PathListView::SelectableFor(BListItem* item) const
{
        PathListItem* pathItem = dynamic_cast<PathListItem*>(item);
        if (pathItem != NULL)
                return pathItem->path;
        return NULL;
}


// #pragma mark -


void
PathListView::ItemAdded(VectorPath* path, 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 (_AddPath(path, index))
                Select(index);

        UnlockLooper();
}


void
PathListView::ItemRemoved(VectorPath* path)
{
        // 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 VectorPath objects
        _RemovePath(path);

        UnlockLooper();
}


// #pragma mark -


void
PathListView::SetPathContainer(Container<VectorPath>* container)
{
        if (fPathContainer == container)
                return;

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

        _MakeEmpty();

        fPathContainer = container;

        if (fPathContainer == NULL)
                return;

        fPathContainer->AddListener(this);

        // sync
//      if (!fPathContainer->ReadLock())
//              return;

        int32 count = fPathContainer->CountItems();
        for (int32 i = 0; i < count; i++)
                _AddPath(fPathContainer->ItemAtFast(i), i);

//      fPathContainer->ReadUnlock();
}


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

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

        fShapeContainer = container;

        if (fShapeContainer != NULL)
                fShapeContainer->AddListener(fShapePathListener);
}


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


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

        fAddItem = new BMenuItem(B_TRANSLATE("Add"),
                new BMessage(MSG_ADD));
        fAddRectItem = new BMenuItem(B_TRANSLATE("Add rect"),
                new BMessage(MSG_ADD_RECT));
        fAddCircleItem = new BMenuItem(B_TRANSLATE("Add circle"/*B_UTF8_ELLIPSIS*/),
                new BMessage(MSG_ADD_CIRCLE));
//      fAddArcItem = new BMenuItem("Add arc" B_UTF8_ELLIPSIS,
//              new BMessage(MSG_ADD_ARC));
        fDuplicateItem = new BMenuItem(B_TRANSLATE("Duplicate"),
                new BMessage(MSG_DUPLICATE));
        fReverseItem = new BMenuItem(B_TRANSLATE("Reverse"),
                new BMessage(MSG_REVERSE));
        fCleanUpItem = new BMenuItem(B_TRANSLATE("Clean up"),
                new BMessage(MSG_CLEAN_UP));
        fRotateIndicesRightItem = new BMenuItem(B_TRANSLATE("Rotate indices forwards"),
                new BMessage(MSG_ROTATE_INDICES_CCW), 'R');
        fRotateIndicesLeftItem = new BMenuItem(B_TRANSLATE("Rotate indices backwards"),
                new BMessage(MSG_ROTATE_INDICES_CW), 'R', B_SHIFT_KEY);
        fRemoveItem = new BMenuItem(B_TRANSLATE("Remove"),
                new BMessage(MSG_REMOVE));

        fMenu->AddItem(fAddItem);
        fMenu->AddItem(fAddRectItem);
        fMenu->AddItem(fAddCircleItem);
//      fMenu->AddItem(fAddArcItem);

        fMenu->AddSeparatorItem();

        fMenu->AddItem(fDuplicateItem);
        fMenu->AddItem(fReverseItem);
        fMenu->AddItem(fCleanUpItem);

        fMenu->AddSeparatorItem();

        fMenu->AddItem(fRotateIndicesRightItem);
        fMenu->AddItem(fRotateIndicesLeftItem);

        fMenu->AddSeparatorItem();

        fMenu->AddItem(fRemoveItem);

        fMenu->SetTargetForItems(this);

        _UpdateMenu();
}


void
PathListView::SetCurrentShape(Shape* shape)
{
        if (fCurrentShape == shape)
                return;

        fCurrentShape = dynamic_cast<PathSourceShape*>(shape);
        fShapePathListener->SetShape(fCurrentShape);

        _UpdateMarks();
}


// #pragma mark -


bool
PathListView::_AddPath(VectorPath* path, int32 index)
{
        if (path == NULL)
                return false;

        PathListItem* item = new(nothrow) PathListItem(path, this,
                fCurrentShape != NULL);
        if (item == NULL)
                return false;

        if (!AddItem(item, index)) {
                delete item;
                return false;
        }
        
        return true;
}


bool
PathListView::_RemovePath(VectorPath* path)
{
        PathListItem* item = _ItemForPath(path);
        if (item != NULL && RemoveItem(item)) {
                delete item;
                return true;
        }
        return false;
}


PathListItem*
PathListView::_ItemForPath(VectorPath* path) const
{
        int32 count = CountItems();
        for (int32 i = 0; i < count; i++) {
                 PathListItem* item = dynamic_cast<PathListItem*>(ItemAt(i));
                if (item == NULL)
                        continue;
                if (item->path == path)
                        return item;
        }
        return NULL;
}


// #pragma mark -


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

        Invalidate();
}


void
PathListView::_SetPathMarked(VectorPath* path, bool marked)
{
        PathListItem* item = _ItemForPath(path);
        if (item != NULL)
                item->SetMarked(marked);
}


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

        bool hasSelection = CurrentSelection(0) >= 0;

        fDuplicateItem->SetEnabled(hasSelection);
        fReverseItem->SetEnabled(hasSelection);
        fCleanUpItem->SetEnabled(hasSelection);
        fRotateIndicesLeftItem->SetEnabled(hasSelection);
        fRotateIndicesRightItem->SetEnabled(hasSelection);
        fRemoveItem->SetEnabled(hasSelection);
}