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

#include "PathManipulator.h"

#include <algorithm>
#include <float.h>
#include <stdio.h>
#include <vector>

#include <Catalog.h>
#include <Cursor.h>
#include <Locale.h>
#include <Message.h>
#include <MenuItem.h>
#include <PopUpMenu.h>
#include <StackOrHeapArray.h>
#include <Window.h>

#include "cursors.h"
#include "support.h"

#include "CanvasView.h"

#include "AddPointCommand.h"
#include "ChangePointCommand.h"
//#include "CloseCommand.h"
#include "InsertPointCommand.h"
#include "FlipPointsCommand.h"
//#include "NewPathCommand.h"
#include "NudgePointsCommand.h"
//#include "RemovePathCommand.h"
#include "RemovePointsCommand.h"
//#include "ReversePathCommand.h"
//#include "SelectPathCommand.h"
//#include "SelectPointsCommand.h"
#include "SplitPointsCommand.h"
#include "TransformPointsBox.h"


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "Icon-O-Matic-PathManipulator"
#define POINT_EXTEND 3.0
#define CONTROL_POINT_EXTEND 2.0
#define INSERT_DIST_THRESHOLD 7.0
#define MOVE_THRESHOLD 9.0


enum {
        UNDEFINED,

        NEW_PATH,

        ADD_POINT,
        INSERT_POINT,
        MOVE_POINT,
        MOVE_POINT_IN,
        MOVE_POINT_OUT,
        CLOSE_PATH,

        TOGGLE_SHARP,
        TOGGLE_SHARP_IN,
        TOGGLE_SHARP_OUT,

        REMOVE_POINT,
        REMOVE_POINT_IN,
        REMOVE_POINT_OUT,

        SELECT_POINTS,
        TRANSFORM_POINTS,
        TRANSLATE_POINTS,

        SELECT_SUB_PATH,
};

enum {
        MSG_TRANSFORM                           = 'strn',
        MSG_REMOVE_POINTS                       = 'srmp',
        MSG_UPDATE_SHAPE_UI                     = 'udsi',

        MSG_SPLIT_POINTS                        = 'splt',
        MSG_FLIP_POINTS                         = 'flip',
};

inline const char*
string_for_mode(uint32 mode)
{
        switch (mode) {
                case UNDEFINED:
                        return "UNDEFINED";
                case NEW_PATH:
                        return "NEW_PATH";
                case ADD_POINT:
                        return "ADD_POINT";
                case INSERT_POINT:
                        return "INSERT_POINT";
                case MOVE_POINT:
                        return "MOVE_POINT";
                case MOVE_POINT_IN:
                        return "MOVE_POINT_IN";
                case MOVE_POINT_OUT:
                        return "MOVE_POINT_OUT";
                case CLOSE_PATH:
                        return "CLOSE_PATH";
                case TOGGLE_SHARP:
                        return "TOGGLE_SHARP";
                case TOGGLE_SHARP_IN:
                        return "TOGGLE_SHARP_IN";
                case TOGGLE_SHARP_OUT:
                        return "TOGGLE_SHARP_OUT";
                case REMOVE_POINT:
                        return "REMOVE_POINT";
                case REMOVE_POINT_IN:
                        return "REMOVE_POINT_IN";
                case REMOVE_POINT_OUT:
                        return "REMOVE_POINT_OUT";
                case SELECT_POINTS:
                        return "SELECT_POINTS";
                case TRANSFORM_POINTS:
                        return "TRANSFORM_POINTS";
                case TRANSLATE_POINTS:
                        return "TRANSLATE_POINTS";
                case SELECT_SUB_PATH:
                        return "SELECT_SUB_PATH";
        }
        return "<unknown mode>";
}

// NOTE: this class extends std::vector<int32> since neither BList or
// BObjectList would suffice. The backing array of BList and BObjectList is a
// void* array. The Items function should return an int32 array. This is a
// problem since sizeof(void*) is not necessarily equal to sizeof(int32).
class PathManipulator::Selection : protected std::vector<int32>
{
public:
        inline Selection(int32 count = 20)
                : _inherited() { reserve(count); }
        inline ~Selection() {}

        inline void Add(int32 value)
        {
                if (value >= 0) {
                        // keep the list sorted
                        insert(std::upper_bound(begin(), end(), value), value);
                }
        }

        inline bool Remove(int32 value)
        {
                if (!Contains(value))
                        return false;
                erase(std::lower_bound(begin(), end(), value));
                return true;
        }

        inline bool Contains(int32 value) const
                { return std::binary_search(begin(), end(), value); }

        inline bool IsEmpty() const
                { return size() == 0; }

        inline int32 IndexAt(int32 index) const
                { return at(index); }

        inline void MakeEmpty()
                { clear(); }

        inline const int32* Items() const
                { return &(*this)[0]; }

        inline const int32 CountItems() const
                { return size(); }

        inline Selection& operator =(const Selection& other)
        {
                _inherited::operator=(other);
                return *this;
        }

        inline bool operator ==(const Selection& other)
                { return (_inherited)*this == (_inherited)other; }

        inline bool operator !=(const Selection& other)
                { return (_inherited)*this != (_inherited)other; }

private:
        typedef std::vector<int32> _inherited;
};


PathManipulator::PathManipulator(VectorPath* path)
        : Manipulator(NULL),
          fCanvasView(NULL),

          fCommandDown(false),
          fOptionDown(false),
          fShiftDown(false),
          fAltDown(false),

          fClickToClose(false),

          fMode(NEW_PATH),
          fFallBackMode(SELECT_POINTS),

          fMouseDown(false),

          fPath(path),
          fCurrentPathPoint(-1),

          fChangePointCommand(NULL),
          fInsertPointCommand(NULL),
          fAddPointCommand(NULL),

          fSelection(new Selection()),
          fOldSelection(new Selection()),
          fTransformBox(NULL),

          fNudgeOffset(0.0, 0.0),
          fLastNudgeTime(system_time()),
          fNudgeCommand(NULL)
{
        fPath->AcquireReference();
        fPath->AddListener(this);
        fPath->AddObserver(this);
}


PathManipulator::~PathManipulator()
{
        delete fChangePointCommand;
        delete fInsertPointCommand;
        delete fAddPointCommand;

        delete fSelection;
        delete fOldSelection;
        delete fTransformBox;

        delete fNudgeCommand;

        fPath->RemoveObserver(this);
        fPath->RemoveListener(this);
        fPath->ReleaseReference();
}


// #pragma mark -


class StrokePathIterator : public VectorPath::Iterator {
 public:
                                        StrokePathIterator(CanvasView* canvasView,
                                                                           BView* drawingView)
                                                : fCanvasView(canvasView),
                                                  fDrawingView(drawingView)
                                        {
                                                fDrawingView->SetHighColor(0, 0, 0, 255);
                                                fDrawingView->SetDrawingMode(B_OP_OVER);
                                        }
        virtual                 ~StrokePathIterator()
                                        {}

        virtual void    MoveTo(BPoint point)
                                        {
                                                fBlack = true;
                                                fSkip = false;
                                                fDrawingView->SetHighColor(0, 0, 0, 255);

                                                fCanvasView->ConvertFromCanvas(&point);
                                                fDrawingView->MovePenTo(point);
                                        }
        virtual void    LineTo(BPoint point)
                                        {
                                                fCanvasView->ConvertFromCanvas(&point);
                                                if (!fSkip) {
                                                        if (fBlack)
                                                                fDrawingView->SetHighColor(255, 255, 255, 255);
                                                        else
                                                                fDrawingView->SetHighColor(0, 0, 0, 255);
                                                        fBlack = !fBlack;

                                                        fDrawingView->StrokeLine(point);
                                                } else {
                                                        fDrawingView->MovePenTo(point);
                                                }
                                                fSkip = !fSkip;
                                        }

 private:
        CanvasView*             fCanvasView;
        BView*                  fDrawingView;
        bool                    fBlack;
        bool                    fSkip;
};


void
PathManipulator::Draw(BView* into, BRect updateRect)
{
        // draw the Bezier curve, but only if not "editing",
        // the path is actually on top all other modifiers
        // TODO: make this customizable in the GUI

        #if __HAIKU__
        uint32 flags = into->Flags();
        into->SetFlags(flags | B_SUBPIXEL_PRECISE);
        #endif // __HAIKU__

        StrokePathIterator iterator(fCanvasView, into);
        fPath->Iterate(&iterator, fCanvasView->ZoomLevel());

        #if __HAIKU__
        into->SetFlags(flags);
        #endif // __HAIKU__

        into->SetLowColor(0, 0, 0, 255);
        BPoint point;
        BPoint pointIn;
        BPoint pointOut;
        rgb_color focusColor = (rgb_color){ 255, 0, 0, 255 };
        rgb_color highlightColor = (rgb_color){ 60, 60, 255, 255 };
        for (int32 i = 0; fPath->GetPointsAt(i, point, pointIn, pointOut); i++) {
                bool highlight = fCurrentPathPoint == i;
                bool selected = fSelection->Contains(i);
                rgb_color normal = selected ? focusColor : (rgb_color){ 0, 0, 0, 255 };
                into->SetLowColor(normal);
                into->SetHighColor(255, 255, 255, 255);
                // convert to view coordinate space
                fCanvasView->ConvertFromCanvas(&point);
                fCanvasView->ConvertFromCanvas(&pointIn);
                fCanvasView->ConvertFromCanvas(&pointOut);
                // connect the points belonging to one control point
                into->SetDrawingMode(B_OP_INVERT);
                into->StrokeLine(point, pointIn);
                into->StrokeLine(point, pointOut);
                // draw main control point
                if (highlight && (fMode == MOVE_POINT ||
                                                  fMode == TOGGLE_SHARP ||
                                                  fMode == REMOVE_POINT ||
                                                  fMode == SELECT_POINTS ||
                                                  fMode == CLOSE_PATH)) {

                        into->SetLowColor(highlightColor);
                }

                into->SetDrawingMode(B_OP_COPY);
                BRect r(point, point);
                r.InsetBy(-POINT_EXTEND, -POINT_EXTEND);
                into->StrokeRect(r, B_SOLID_LOW);
                r.InsetBy(1.0, 1.0);
                into->FillRect(r, B_SOLID_HIGH);
                // draw in control point
                if (highlight && (fMode == MOVE_POINT_IN ||
                                                  fMode == TOGGLE_SHARP_IN ||
                                                  fMode == REMOVE_POINT_IN ||
                                                  fMode == SELECT_POINTS))
                        into->SetLowColor(highlightColor);
                else
                        into->SetLowColor(normal);
                if (selected) {
                        into->SetHighColor(220, 220, 220, 255);
                } else {
                        into->SetHighColor(170, 170, 170, 255);
                }
                if (pointIn != point) {
                        r.Set(pointIn.x - CONTROL_POINT_EXTEND, pointIn.y - CONTROL_POINT_EXTEND,
                                  pointIn.x + CONTROL_POINT_EXTEND, pointIn.y + CONTROL_POINT_EXTEND);
                        into->StrokeRect(r, B_SOLID_LOW);
                        r.InsetBy(1.0, 1.0);
                        into->FillRect(r, B_SOLID_HIGH);
                }
                // draw out control point
                if (highlight && (fMode == MOVE_POINT_OUT ||
                                                  fMode == TOGGLE_SHARP_OUT ||
                                                  fMode == REMOVE_POINT_OUT ||
                                                  fMode == SELECT_POINTS))
                        into->SetLowColor(highlightColor);
                else
                        into->SetLowColor(normal);
                if (pointOut != point) {
                        r.Set(pointOut.x - CONTROL_POINT_EXTEND, pointOut.y - CONTROL_POINT_EXTEND,
                                  pointOut.x + CONTROL_POINT_EXTEND, pointOut.y + CONTROL_POINT_EXTEND);
                        into->StrokeRect(r, B_SOLID_LOW);
                        r.InsetBy(1.0, 1.0);
                        into->FillRect(r, B_SOLID_HIGH);
                }
        }

        if (fTransformBox) {
                fTransformBox->Draw(into, updateRect);
        }
}


// #pragma mark -


bool
PathManipulator::MouseDown(BPoint where)
{
        fMouseDown = true;

        if (fMode == TRANSFORM_POINTS) {
                if (fTransformBox) {
                        fTransformBox->MouseDown(where);

//                      if (!fTransformBox->IsRotating())
//                              fCanvasView->SetAutoScrolling(true);
                }
                return true;
        }

        if (fMode == MOVE_POINT &&
                fSelection->CountItems() > 1 &&
                fSelection->Contains(fCurrentPathPoint)) {
                fMode = TRANSLATE_POINTS;
        }

        // apply the canvas view mouse filter depending on current mode
        if (fMode == ADD_POINT || fMode == TRANSLATE_POINTS)
                fCanvasView->FilterMouse(&where);

        BPoint canvasWhere = where;
        fCanvasView->ConvertToCanvas(&canvasWhere);

        // maybe we're changing some point, so we construct the
        // "ChangePointCommand" here so that the point is remembered
        // in its current state
        // apply the canvas view mouse filter depending on current mode
        delete fChangePointCommand;
        fChangePointCommand = NULL;
        switch (fMode) {
                case TOGGLE_SHARP:
                case TOGGLE_SHARP_IN:
                case TOGGLE_SHARP_OUT:
                case MOVE_POINT:
                case MOVE_POINT_IN:
                case MOVE_POINT_OUT:
                case REMOVE_POINT_IN:
                case REMOVE_POINT_OUT:
                        fChangePointCommand = new ChangePointCommand(fPath,
                                                                                                                 fCurrentPathPoint,
                                                                                                                 fSelection->Items(),
                                                                                                                 fSelection->CountItems());
                        _Select(fCurrentPathPoint, fShiftDown);
                        break;
        }

        // at this point we init doing something
        switch (fMode) {
                case ADD_POINT:
                        _AddPoint(canvasWhere);
                        break;
                case INSERT_POINT:
                        _InsertPoint(canvasWhere, fCurrentPathPoint);
                        break;

                case TOGGLE_SHARP:
                        _SetSharp(fCurrentPathPoint);
                        // continue by dragging out the _connected_ in/out points
                        break;
                case TOGGLE_SHARP_IN:
                        _SetInOutConnected(fCurrentPathPoint, false);
                        // continue by moving the "in" point
                        _SetMode(MOVE_POINT_IN);
                        break;
                case TOGGLE_SHARP_OUT:
                        _SetInOutConnected(fCurrentPathPoint, false);
                        // continue by moving the "out" point
                        _SetMode(MOVE_POINT_OUT);
                        break;

                case MOVE_POINT:
                case MOVE_POINT_IN:
                case MOVE_POINT_OUT:
                        // the right thing happens since "fCurrentPathPoint"
                        // points to the correct index
                        break;

                case CLOSE_PATH:
//                      SetClosed(true, true);
                        break;

                case REMOVE_POINT:
                        if (fPath->CountPoints() == 1) {
//                              fCanvasView->Perform(new RemovePathCommand(this, fPath));
                        } else {
                                fCanvasView->Perform(new RemovePointsCommand(fPath,
                                                                                                                         fCurrentPathPoint,
                                                                                                                         fSelection->Items(),
                                                                                                                         fSelection->CountItems()));
                                _RemovePoint(fCurrentPathPoint);
                        }
                        break;
                case REMOVE_POINT_IN:
                        _RemovePointIn(fCurrentPathPoint);
                        break;
                case REMOVE_POINT_OUT:
                        _RemovePointOut(fCurrentPathPoint);
                        break;

                case SELECT_POINTS: {
                        // TODO: this works so that you can deselect all points
                        // when clicking outside the path even if pressing shift
                        // in case the path is open... a better way would be
                        // to deselect all on mouse up, if the mouse has not moved
                        bool appendSelection;
                        if (fPath->IsClosed())
                                appendSelection = fShiftDown;
                        else
                                appendSelection = fShiftDown && fCurrentPathPoint >= 0;

                        if (!appendSelection) {
                                fSelection->MakeEmpty();
                                _UpdateSelection();
                        }
                        *fOldSelection = *fSelection;
                        if (fCurrentPathPoint >= 0) {
                                _Select(fCurrentPathPoint, appendSelection);
                        }
                        fCanvasView->BeginRectTracking(BRect(where, where),
                                B_TRACK_RECT_CORNER);
                        break;
                }
        }

        fTrackingStart = canvasWhere;
        // remember the subpixel position
        // so that MouseMoved() will work even before
        // the integer position becomes different
        fLastCanvasPos = where;
        fCanvasView->ConvertToCanvas(&fLastCanvasPos);

        // the reason to exclude the select mode
        // is that the BView rect tracking does not
        // scroll the rect starting point along with us
        // (since we're doing no real scrolling)
//      if (fMode != SELECT_POINTS)
//              fCanvasView->SetAutoScrolling(true);

        UpdateCursor();

        return true;
}


void
PathManipulator::MouseMoved(BPoint where)
{
        fCanvasView->FilterMouse(&where);
                // NOTE: only filter mouse coords in mouse moved, no other
                // mouse function
        BPoint canvasWhere = where;
        fCanvasView->ConvertToCanvas(&canvasWhere);

        // since the tablet is generating mouse moved messages
        // even if only the pressure changes (and not the actual mouse position)
        // we insert this additional check to prevent too much calculation
        if (fLastCanvasPos == canvasWhere)
                return;

        fLastCanvasPos = canvasWhere;

        if (fMode == TRANSFORM_POINTS) {
                if (fTransformBox) {
                        fTransformBox->MouseMoved(where);
                }
                return;
        }

        if (fMode == CLOSE_PATH) {
                // continue by moving the point
                _SetMode(MOVE_POINT);
                delete fChangePointCommand;
                fChangePointCommand = new ChangePointCommand(fPath,
                                                                                                         fCurrentPathPoint,
                                                                                                         fSelection->Items(),
                                                                                                         fSelection->CountItems());
        }

//      if (!fPrecise) {
//              float offset = fmod(fOutlineWidth, 2.0) / 2.0;
//              canvasWhere.point += BPoint(offset, offset);
//      }

        switch (fMode) {
                case ADD_POINT:
                case INSERT_POINT:
                case TOGGLE_SHARP:
                        // drag the "out" control point, mirror the "in" control point
                        fPath->SetPointOut(fCurrentPathPoint, canvasWhere, true);
                        break;
                case MOVE_POINT:
                        // drag all three control points at once
                        fPath->SetPoint(fCurrentPathPoint, canvasWhere);
                        break;
                case MOVE_POINT_IN:
                        // drag in control point
                        fPath->SetPointIn(fCurrentPathPoint, canvasWhere);
                        break;
                case MOVE_POINT_OUT:
                        // drag out control point
                        fPath->SetPointOut(fCurrentPathPoint, canvasWhere);
                        break;

                case SELECT_POINTS: {
                        // change the selection
                        BRect r;
                        r.left = min_c(fTrackingStart.x, canvasWhere.x);
                        r.top = min_c(fTrackingStart.y, canvasWhere.y);
                        r.right = max_c(fTrackingStart.x, canvasWhere.x);
                        r.bottom = max_c(fTrackingStart.y, canvasWhere.y);
                        _Select(r);
                        break;
                }

                case TRANSLATE_POINTS: {
                        BPoint offset = canvasWhere - fTrackingStart;
                        _Nudge(offset);
                        fTrackingStart = canvasWhere;
                        break;
                }
        }
}


Command*
PathManipulator::MouseUp()
{
        // prevent carrying out actions more than once by only
        // doing it if "fMouseDown" is true at the point of
        // entering this function
        if (!fMouseDown)
                return NULL;
        fMouseDown = false;

        if (fMode == TRANSFORM_POINTS) {
                if (fTransformBox) {
                        return fTransformBox->MouseUp();
                }
                return NULL;
        }

        Command* command = NULL;

        switch (fMode) {

                case ADD_POINT:
                        command = fAddPointCommand;
                        fAddPointCommand = NULL;
                        _SetMode(MOVE_POINT_OUT);
                        break;

                case INSERT_POINT:
                        command = fInsertPointCommand;
                        fInsertPointCommand = NULL;
                        break;

                case SELECT_POINTS:
                        if (*fSelection != *fOldSelection) {
//                              command = new SelectPointsCommand(this, fPath,
//                                                                                                fOldSelection->Items(),
//                                                                                                fOldSelection->CountItems(),
//                                                                                                fSelection->Items(),
//                                                                                                fSelection->CountItems()));
                        }
                        fCanvasView->EndRectTracking();
                        break;

                case TOGGLE_SHARP:
                case TOGGLE_SHARP_IN:
                case TOGGLE_SHARP_OUT:
                case MOVE_POINT:
                case MOVE_POINT_IN:
                case MOVE_POINT_OUT:
                case REMOVE_POINT_IN:
                case REMOVE_POINT_OUT:
                        command = fChangePointCommand;
                        fChangePointCommand = NULL;
                        break;

                case TRANSLATE_POINTS:
                        if (!fNudgeCommand) {
                                // select just the point that was clicked
                                *fOldSelection = *fSelection;
                                if (fCurrentPathPoint >= 0) {
                                        _Select(fCurrentPathPoint, fShiftDown);
                                }
                                if (*fSelection != *fOldSelection) {
//                                      command = new SelectPointsCommand(this, fPath,
//                                                                                                        fOldSelection->Items(),
//                                                                                                        fOldSelection->CountItems(),
//                                                                                                        fSelection->Items(),
//                                                                                                        fSelection->CountItems()));
                                }
                        } else {
                                command = _FinishNudging();
                        }
                        break;
        }

        return command;
}


bool
PathManipulator::MouseOver(BPoint where)
{
        if (fMode == TRANSFORM_POINTS) {
                if (fTransformBox) {
                        return fTransformBox->MouseOver(where);
                }
                return false;
        }

        BPoint canvasWhere = where;
        fCanvasView->ConvertToCanvas(&canvasWhere);

        // since the tablet is generating mouse moved messages
        // even if only the pressure changes (and not the actual mouse position)
        // we insert this additional check to prevent too much calculation
        if (fMouseDown && fLastCanvasPos == canvasWhere)
                return false;

        fLastCanvasPos = canvasWhere;

        // hit testing
        // (use a subpixel mouse pos)
        fCanvasView->ConvertToCanvas(&where);
        _SetModeForMousePos(where);

        // TODO: always true?
        return true;
}


bool
PathManipulator::DoubleClicked(BPoint where)
{
        return false;
}


bool
PathManipulator::ShowContextMenu(BPoint where)
{
        // Change the selection to the current point if it isn't currently
        // selected. This could will only be chosen if the user right-clicked
        // a path point directly. 
        if (fCurrentPathPoint >= 0 && !fSelection->Contains(fCurrentPathPoint)) {
                fSelection->MakeEmpty();
                _UpdateSelection();
                *fOldSelection = *fSelection;
                _Select(fCurrentPathPoint, false);
        }

        BPopUpMenu* menu = new BPopUpMenu("context menu", false, false);
        BMessage* message;
        BMenuItem* item;

        bool hasSelection = fSelection->CountItems() > 0;

        if (fCurrentPathPoint < 0) {
                message = new BMessage(B_SELECT_ALL);
                item = new BMenuItem(B_TRANSLATE("Select all"), message, 'A');
                menu->AddItem(item);

                menu->AddSeparatorItem();
        }

        message = new BMessage(MSG_TRANSFORM);
        item = new BMenuItem(B_TRANSLATE("Transform"), message, 'T');
        item->SetEnabled(hasSelection);
        menu->AddItem(item);

        message = new BMessage(MSG_SPLIT_POINTS);
        item = new BMenuItem(B_TRANSLATE("Split"), message);
        item->SetEnabled(hasSelection);
        menu->AddItem(item);

        message = new BMessage(MSG_FLIP_POINTS);
        item = new BMenuItem(B_TRANSLATE("Flip"), message);
        item->SetEnabled(hasSelection);
        menu->AddItem(item);

        message = new BMessage(MSG_REMOVE_POINTS);
        item = new BMenuItem(B_TRANSLATE("Remove"), message);
        item->SetEnabled(hasSelection);
        menu->AddItem(item);

        // go
        menu->SetTargetForItems(fCanvasView);
        menu->SetAsyncAutoDestruct(true);
        menu->SetFont(be_plain_font);
        where = fCanvasView->ConvertToScreen(where);
        BRect mouseRect(where, where);
        mouseRect.InsetBy(-10.0, -10.0);
        where += BPoint(5.0, 5.0);
        menu->Go(where, true, false, mouseRect, true);

        return true;
}


// #pragma mark -


BRect
PathManipulator::Bounds()
{
        BRect r = _ControlPointRect();
        fCanvasView->ConvertFromCanvas(&r);
        return r;
}


BRect
PathManipulator::TrackingBounds(BView* withinView)
{
        return withinView->Bounds();
}


// #pragma mark -


bool
PathManipulator::MessageReceived(BMessage* message, Command** _command)
{
        bool result = true;
        switch (message->what) {
                case MSG_TRANSFORM:
                        if (!fSelection->IsEmpty())
                                _SetMode(TRANSFORM_POINTS);
                        break;
                case MSG_REMOVE_POINTS:
                        *_command = _Delete();
                        break;
                case MSG_SPLIT_POINTS:
                        *_command = new SplitPointsCommand(fPath,
                                                                                           fSelection->Items(),
                                                                                           fSelection->CountItems());
                        break;
                case MSG_FLIP_POINTS:
                        *_command = new FlipPointsCommand(fPath,
                                                                                          fSelection->Items(),
                                                                                          fSelection->CountItems());
                        break;
                case B_SELECT_ALL: {
                        int32 count = fPath->CountPoints();
                        int32 indices[count];

                        for (int32 i = 0; i < count; i++)
                                indices[i] = i;

                        _Select(indices, count);
                        break;
                }
                default:
                        result = false;
                        break;
        }
        return result;
}


void
PathManipulator::ModifiersChanged(uint32 modifiers)
{
        fCommandDown = modifiers & B_COMMAND_KEY;
        fOptionDown = modifiers & B_CONTROL_KEY;
        fShiftDown = modifiers & B_SHIFT_KEY;
        fAltDown = modifiers & B_OPTION_KEY;

        if (fTransformBox) {
                fTransformBox->ModifiersChanged(modifiers);
                return;
        }
        // reevaluate mode
        if (!fMouseDown)
                _SetModeForMousePos(fLastCanvasPos);
}


bool
PathManipulator::HandleKeyDown(uint32 key, uint32 modifiers, Command** _command)
{
        bool result = true;

        float nudgeDist = 1.0;
        if (modifiers & B_SHIFT_KEY)
                nudgeDist /= fCanvasView->ZoomLevel();

        switch (key) {
                // commit
                case B_RETURN:
                        if (fTransformBox) {
                                _SetTransformBox(NULL);
                        }// else
//                              _Perform();
                        break;
                // cancel
                case B_ESCAPE:
                        if (fTransformBox) {
                                fTransformBox->Cancel();
                                _SetTransformBox(NULL);
                        } else if (fFallBackMode == NEW_PATH) {
                                fFallBackMode = SELECT_POINTS;
                                _SetTransformBox(NULL);
                        }// else
//                              _Cancel();
                        break;
                case 't':
                case 'T':
                        if (!fSelection->IsEmpty())
                                _SetMode(TRANSFORM_POINTS);
                        else
                                result = false;
                        break;
                // nudging
                case B_UP_ARROW:
                        _Nudge(BPoint(0.0, -nudgeDist));
                        break;
                case B_DOWN_ARROW:
                        _Nudge(BPoint(0.0, nudgeDist));
                        break;
                case B_LEFT_ARROW:
                        _Nudge(BPoint(-nudgeDist, 0.0));
                        break;
                case B_RIGHT_ARROW:
                        _Nudge(BPoint(nudgeDist, 0.0));
                        break;

                case B_DELETE:
                        if (!fSelection->IsEmpty())
                                *_command = _Delete();
                        else
                                result = false;
                        break;

                default:
                        result = false;
        }
        return result;
}


bool
PathManipulator::HandleKeyUp(uint32 key, uint32 modifiers, Command** _command)
{
        bool handled = true;
        switch (key) {
                // nudging
                case B_UP_ARROW:
                case B_DOWN_ARROW:
                case B_LEFT_ARROW:
                case B_RIGHT_ARROW:
                        *_command = _FinishNudging();
                        break;
                default:
                        handled = false;
                        break;
        }
        return handled;
}


bool
PathManipulator::UpdateCursor()
{
        if (fTransformBox)
                return fTransformBox->UpdateCursor();

        const uchar* cursorData;
        switch (fMode) {
                case ADD_POINT:
                        cursorData = kPathAddCursor;
                        break;
                case INSERT_POINT:
                        cursorData = kPathInsertCursor;
                        break;
                case MOVE_POINT:
                case MOVE_POINT_IN:
                case MOVE_POINT_OUT:
                case TRANSLATE_POINTS:
                        cursorData = kPathMoveCursor;
                        break;
                case CLOSE_PATH:
                        cursorData = kPathCloseCursor;
                        break;
                case TOGGLE_SHARP:
                case TOGGLE_SHARP_IN:
                case TOGGLE_SHARP_OUT:
                        cursorData = kPathSharpCursor;
                        break;
                case REMOVE_POINT:
                case REMOVE_POINT_IN:
                case REMOVE_POINT_OUT:
                        cursorData = kPathRemoveCursor;
                        break;
                case SELECT_POINTS:
                        cursorData = kPathSelectCursor;
                        break;

                case SELECT_SUB_PATH:
                        cursorData = B_HAND_CURSOR;
                        break;

                case UNDEFINED:
                default:
                        cursorData = kStopCursor;
                        break;
        }
        BCursor cursor(cursorData);
        fCanvasView->SetViewCursor(&cursor, true);
        fCanvasView->Sync();

        return true;
}


void
PathManipulator::AttachedToView(BView* view)
{
        fCanvasView = dynamic_cast<CanvasView*>(view);
}


void
PathManipulator::DetachedFromView(BView* view)
{
        fCanvasView = NULL;
}


// #pragma mark -


void
PathManipulator::ObjectChanged(const Observable* object)
{
        // TODO: refine VectorPath listener interface and
        // implement more efficiently
        BRect currentBounds = _ControlPointRect();
        _InvalidateCanvas(currentBounds | fPreviousBounds);
        fPreviousBounds = currentBounds;

        // reevaluate mode
        if (!fMouseDown && !fTransformBox)
                _SetModeForMousePos(fLastCanvasPos);
}


// #pragma mark -


void
PathManipulator::PointAdded(int32 index)
{
        ObjectChanged(fPath);
}


void
PathManipulator::PointRemoved(int32 index)
{
        fSelection->Remove(index);
        ObjectChanged(fPath);
}


void
PathManipulator::PointChanged(int32 index)
{
        ObjectChanged(fPath);
}


void
PathManipulator::PathChanged()
{
        ObjectChanged(fPath);
}


void
PathManipulator::PathClosedChanged()
{
        ObjectChanged(fPath);
}


void
PathManipulator::PathReversed()
{
        // reverse selection along with path
        int32 count = fSelection->CountItems();
        int32 pointCount = fPath->CountPoints();
        if (count > 0) {
                Selection temp;
                for (int32 i = 0; i < count; i++) {
                        temp.Add((pointCount - 1) - fSelection->IndexAt(i));
                }
                *fSelection = temp;
        }

        ObjectChanged(fPath);
}


// #pragma mark -


uint32
PathManipulator::ControlFlags() const
{
        uint32 flags = 0;

//      flags |= SHAPE_UI_FLAGS_CAN_REVERSE_PATH;
//
//      if (!fSelection->IsEmpty())
//              flags |= SHAPE_UI_FLAGS_HAS_SELECTION;
//      if (fPath->CountPoints() > 1)
//              flags |= SHAPE_UI_FLAGS_CAN_CLOSE_PATH;
//      if (fPath->IsClosed())
//              flags |= SHAPE_UI_FLAGS_PATH_IS_CLOSED;
//      if (fTransformBox)
//              flags |= SHAPE_UI_FLAGS_IS_TRANSFORMING;

        return flags;
}


void
PathManipulator::ReversePath()
{
        int32 count = fSelection->CountItems();
        int32 pointCount = fPath->CountPoints();
        if (count > 0) {
                Selection temp;
                for (int32 i = 0; i < count; i++) {
                        temp.Add((pointCount - 1) - fSelection->IndexAt(i));
                }
                *fSelection = temp;
        }
        fPath->Reverse();
}


// #pragma mark -


void
PathManipulator::_SetMode(uint32 mode)
{
        if (fMode != mode) {
//printf("switching mode: %s -> %s\n", string_for_mode(fMode), string_for_mode(mode));
                fMode = mode;

                if (fMode == TRANSFORM_POINTS) {
                        _SetTransformBox(new TransformPointsBox(fCanvasView,
                                                                                                        this,
                                                                                                        fPath,
                                                                                                        fSelection->Items(),
                                                                                                        fSelection->CountItems()));
//                      fCanvasView->Perform(new EnterTransformPointsCommand(this,
//                                                                                                                fSelection->Items(),
//                                                                                                                fSelection->CountItems()));
                } else {
                        if (fTransformBox)
                                _SetTransformBox(NULL);
                }

                if (BWindow* window = fCanvasView->Window()) {
                        window->PostMessage(MSG_UPDATE_SHAPE_UI);
                }
                UpdateCursor();
        }
}


void
PathManipulator::_SetTransformBox(TransformPointsBox* transformBox)
{
        if (fTransformBox == transformBox)
                return;

        BRect dirty(LONG_MAX, LONG_MAX, LONG_MIN, LONG_MIN);
        if (fTransformBox) {
                // get rid of transform box display
                dirty = fTransformBox->Bounds();
                delete fTransformBox;
        }

        fTransformBox = transformBox;

        // TODO: this is weird, fMode should only be set in _SetMode, not
        // here as well, also this method could be called this way
        // _SetModeForMousePos -> _SetMode -> _SetTransformBox
        // and then below it does _SetModeForMousePos again...
        if (fTransformBox) {
                fTransformBox->MouseMoved(fLastCanvasPos);
                if (fMode != TRANSFORM_POINTS) {
                        fMode = TRANSFORM_POINTS;
                }
                dirty = dirty | fTransformBox->Bounds();
        } else {
                if (fMode == TRANSFORM_POINTS) {
                        _SetModeForMousePos(fLastCanvasPos);
                }
        }

        if (dirty.IsValid()) {
                dirty.InsetBy(-8, -8);
                fCanvasView->Invalidate(dirty);
        }
}


void
PathManipulator::_AddPoint(BPoint where)
{
        if (fPath->AddPoint(where)) {
                fCurrentPathPoint = fPath->CountPoints() - 1;

                delete fAddPointCommand;
                fAddPointCommand = new AddPointCommand(fPath, fCurrentPathPoint,
                                                                                           fSelection->Items(),
                                                                                           fSelection->CountItems());

                _Select(fCurrentPathPoint, fShiftDown);
        }
}


BPoint
scale_point(BPoint a, BPoint b, float scale)
{
        return BPoint(a.x + (b.x - a.x) * scale,
                                  a.y + (b.y - a.y) * scale);
}


void
PathManipulator::_InsertPoint(BPoint where, int32 index)
{
        double scale;

        BPoint point;
        BPoint pointIn;
        BPoint pointOut;

        BPoint previous;
        BPoint previousOut;
        BPoint next;
        BPoint nextIn;

        if (fPath->FindBezierScale(index - 1, where, &scale)
                && scale >= 0.0 && scale <= 1.0
                && fPath->GetPoint(index - 1, scale, point)) {

                fPath->GetPointAt(index - 1, previous);
                fPath->GetPointOutAt(index - 1, previousOut);
                fPath->GetPointAt(index, next);
                fPath->GetPointInAt(index, nextIn);

                where = scale_point(previousOut, nextIn, scale);

                previousOut = scale_point(previous, previousOut, scale);
                nextIn = scale_point(next, nextIn, 1 - scale);
                pointIn = scale_point(previousOut, where, scale);
                pointOut = scale_point(nextIn, where, 1 - scale);
                
                if (fPath->AddPoint(point, index)) {

                        fPath->SetPointIn(index, pointIn);
                        fPath->SetPointOut(index, pointOut);

                        delete fInsertPointCommand;
                        fInsertPointCommand = new InsertPointCommand(fPath, index,
                                                                                                                 fSelection->Items(),
                                                                                                                 fSelection->CountItems());

                        fPath->SetPointOut(index - 1, previousOut);
                        fPath->SetPointIn(index + 1, nextIn);

                        fCurrentPathPoint = index;
                        _ShiftSelection(fCurrentPathPoint, 1);
                        _Select(fCurrentPathPoint, fShiftDown);
                }
        }
}


void
PathManipulator::_SetInOutConnected(int32 index, bool connected)
{
        fPath->SetInOutConnected(index, connected);
}


void
PathManipulator::_SetSharp(int32 index)
{
        BPoint p;
        fPath->GetPointAt(index, p);
        fPath->SetPoint(index, p, p, p, true);
}


void
PathManipulator::_RemoveSelection()
{
        // NOTE: copy selection since removing points will
        // trigger notifications, and that will influence the
        // selection
        Selection selection = *fSelection;
        int32 count = selection.CountItems();
        for (int32 i = 0; i < count; i++) {
                if (!fPath->RemovePoint(selection.IndexAt(i) - i))
                        break;
        }

        fPath->SetClosed(fPath->IsClosed() && fPath->CountPoints() > 1);

        fSelection->MakeEmpty();
}


void
PathManipulator::_RemovePoint(int32 index)
{
        if (fPath->RemovePoint(index)) {
                _Deselect(index);
                _ShiftSelection(index + 1, -1);
        }
}


void
PathManipulator::_RemovePointIn(int32 index)
{
        BPoint p;
        if (fPath->GetPointAt(index, p)) {
                fPath->SetPointIn(index, p);
                fPath->SetInOutConnected(index, false);
        }
}


void
PathManipulator::_RemovePointOut(int32 index)
{
        BPoint p;
        if (fPath->GetPointAt(index, p)) {
                fPath->SetPointOut(index, p);
                fPath->SetInOutConnected(index, false);
        }
}


Command*
PathManipulator::_Delete()
{
        Command* command = NULL;
        if (!fMouseDown) {
                // make sure we apply an on-going transformation before we proceed
                if (fTransformBox) {
                        _SetTransformBox(NULL);
                }

                if (fSelection->CountItems() == fPath->CountPoints()) {
//                      command = new RemovePathCommand(fPath);
                } else {
                        command = new RemovePointsCommand(fPath,
                                                                                          fSelection->Items(),
                                                                                          fSelection->CountItems());
                        _RemoveSelection();
                }

                _SetModeForMousePos(fLastCanvasPos);
        }

        return command;
}


// #pragma mark -


void
PathManipulator::_Select(BRect r)
{
        BPoint p;
        BPoint pIn;
        BPoint pOut;
        int32 count = fPath->CountPoints();
        Selection temp;
        for (int32 i = 0; i < count && fPath->GetPointsAt(i, p, pIn, pOut); i++) {
                if (r.Contains(p) || r.Contains(pIn) || r.Contains(pOut)) {
                        temp.Add(i);
                }
        }
        // merge old and new selection
        count = fOldSelection->CountItems();
        for (int32 i = 0; i < count; i++) {
                int32 index = fOldSelection->IndexAt(i);
                if (temp.Contains(index))
                        temp.Remove(index);
                else
                        temp.Add(index);
        }
        if (temp != *fSelection) {
                *fSelection = temp;
                _UpdateSelection();
        }
}


void
PathManipulator::_Select(int32 index, bool extend)
{
        if (!extend)
                fSelection->MakeEmpty();
        if (fSelection->Contains(index))
                fSelection->Remove(index);
        else
                fSelection->Add(index);
        // TODO: this can lead to unnecessary invalidation (maybe need to investigate)
        _UpdateSelection();
}


void
PathManipulator::_Select(const int32* indices, int32 count, bool extend)
{
        if (extend) {
                for (int32 i = 0; i < count; i++) {
                        if (!fSelection->Contains(indices[i]))
                                fSelection->Add(indices[i]);
                }
        } else {
                fSelection->MakeEmpty();
                for (int32 i = 0; i < count; i++) {
                        fSelection->Add(indices[i]);
                }
        }
        _UpdateSelection();
}


void
PathManipulator::_Deselect(int32 index)
{
        if (fSelection->Contains(index)) {
                fSelection->Remove(index);
                _UpdateSelection();
        }
}


void
PathManipulator::_ShiftSelection(int32 startIndex, int32 direction)
{
        int32 count = fSelection->CountItems();
        if (count > 0) {
                for (int32 i = 0; i < count; i++) {
                        int32 index = fSelection->IndexAt(i);
                        if (index >= startIndex) {
                                fSelection->Remove(index);
                                fSelection->Add(index + direction);
                        }
                }
        }
        _UpdateSelection();
}


bool
PathManipulator::_IsSelected(int32 index) const
{
        return fSelection->Contains(index);
}


// #pragma mark -


void
PathManipulator::_InvalidateCanvas(BRect rect) const
{
        // convert from canvas to view space
        fCanvasView->ConvertFromCanvas(&rect);
        fCanvasView->Invalidate(rect);
}


void
PathManipulator::_InvalidateHighlightPoints(int32 newIndex, uint32 newMode)
{
        BRect oldRect = _ControlPointRect(fCurrentPathPoint, fMode);
        BRect newRect = _ControlPointRect(newIndex, newMode);
        if (oldRect.IsValid())
                _InvalidateCanvas(oldRect);
        if (newRect.IsValid())
                _InvalidateCanvas(newRect);
}


void
PathManipulator::_UpdateSelection() const
{
        _InvalidateCanvas(_ControlPointRect());
        if (BWindow* window = fCanvasView->Window()) {
                window->PostMessage(MSG_UPDATE_SHAPE_UI);
        }
}


BRect
PathManipulator::_ControlPointRect() const
{
        BRect r = fPath->ControlPointBounds();
        r.InsetBy(-POINT_EXTEND, -POINT_EXTEND);
        return r; 
}


BRect
PathManipulator::_ControlPointRect(int32 index, uint32 mode) const
{
        BRect rect(0.0, 0.0, -1.0, -1.0);
        if (index >= 0) {
                BPoint p, pIn, pOut;
                fPath->GetPointsAt(index, p, pIn, pOut);
                switch (mode) {
                        case MOVE_POINT:
                        case TOGGLE_SHARP:
                        case REMOVE_POINT:
                        case CLOSE_PATH:
                                rect.Set(p.x, p.y, p.x, p.y);
                                rect.InsetBy(-POINT_EXTEND, -POINT_EXTEND);
                                break;
                        case MOVE_POINT_IN:
                        case TOGGLE_SHARP_IN:
                        case REMOVE_POINT_IN:
                                rect.Set(pIn.x, pIn.y, pIn.x, pIn.y);
                                rect.InsetBy(-CONTROL_POINT_EXTEND, -CONTROL_POINT_EXTEND);
                                break;
                        case MOVE_POINT_OUT:
                        case TOGGLE_SHARP_OUT:
                        case REMOVE_POINT_OUT:
                                rect.Set(pOut.x, pOut.y, pOut.x, pOut.y);
                                rect.InsetBy(-CONTROL_POINT_EXTEND, -CONTROL_POINT_EXTEND);
                                break;
                        case SELECT_POINTS:
                                rect.Set(min4(p.x, pIn.x, pOut.x, pOut.x),
                                                 min4(p.y, pIn.y, pOut.y, pOut.y),
                                                 max4(p.x, pIn.x, pOut.x, pOut.x),
                                                 max4(p.y, pIn.y, pOut.y, pOut.y));
                                rect.InsetBy(-POINT_EXTEND, -POINT_EXTEND);
                                break;
                }
        }
        return rect;
}


// #pragma mark -


void
PathManipulator::_SetModeForMousePos(BPoint where)
{
        uint32 mode = UNDEFINED;
        int32 index = -1;

        float zoomLevel = fCanvasView->ZoomLevel();

        // see if we're close enough at a control point
        BPoint point;
        BPoint pointIn;
        BPoint pointOut;
        for (int32 i = 0; fPath->GetPointsAt(i, point, pointIn, pointOut)
                                          && mode == UNDEFINED; i++) {

                float distM = point_point_distance(point, where) * zoomLevel;
                float distIn = point_point_distance(pointIn, where) * zoomLevel;
                float distOut = point_point_distance(pointOut, where) * zoomLevel;
                
                if (distM < MOVE_THRESHOLD) {
                        if (i == 0 && fClickToClose
                                && !fPath->IsClosed() && fPath->CountPoints() > 1) {
                                mode = fCommandDown ? TOGGLE_SHARP :
                                                        (fOptionDown ? REMOVE_POINT : CLOSE_PATH);
                                index = i;
                        } else {
                                mode = fCommandDown ? TOGGLE_SHARP :
                                                        (fOptionDown ? REMOVE_POINT : MOVE_POINT);
                                index = i;
                        }
                }
                if (distM - distIn > 0.00001
                        && distIn < MOVE_THRESHOLD) {
                        mode = fCommandDown ? TOGGLE_SHARP_IN : 
                                                (fOptionDown ? REMOVE_POINT_IN : MOVE_POINT_IN);
                        index = i;
                }
                if (distIn - distOut > 0.00001
                        && distOut < distM && distOut < MOVE_THRESHOLD) {
                        mode = fCommandDown ? TOGGLE_SHARP_OUT :
                                                (fOptionDown ? REMOVE_POINT_OUT : MOVE_POINT_OUT);
                        index = i;
                }
        }
        // selection mode overrides any other mode,
        // but we need to check for it after we know
        // the index of the point under the mouse (code above)
        int32 pointCount = fPath->CountPoints();
        if (fShiftDown && pointCount > 0) {
                mode = SELECT_POINTS;
        }

        // see if user wants to start new sub path
        if (fAltDown) {
                mode = NEW_PATH;
                index = -1;
        }

        // see if we're close enough at a line
        if (mode == UNDEFINED) {
                float distance;
                if (fPath->GetDistance(where, &distance, &index)) {
                        if (distance < (INSERT_DIST_THRESHOLD / zoomLevel)) {
                                mode = INSERT_POINT;
                        }
                } else {
                        // restore index, since it was changed by call above
                        index = fCurrentPathPoint;
                }
        }

        // nope, still undefined mode, last fall back
        if (mode == UNDEFINED) {
                if (fFallBackMode == SELECT_POINTS) {
                        if (fPath->IsClosed() && pointCount > 0) {
                                mode = SELECT_POINTS;
                                index = -1;
                        } else {
                                mode = ADD_POINT;
                                index = pointCount - 1;
                        }
                } else {
                        // user had clicked "New Path" icon
                        mode = fFallBackMode;
                }
        }
        // switch mode if necessary
        if (mode != fMode || index != fCurrentPathPoint) {
                // invalidate path display (to highlight the respective point)
                _InvalidateHighlightPoints(index, mode);
                _SetMode(mode);
                fCurrentPathPoint = index;
        }
}


// #pragma mark -


void
PathManipulator::_Nudge(BPoint direction)
{
        bigtime_t now = system_time();
        if (now - fLastNudgeTime > 500000) {
                fCanvasView->Perform(_FinishNudging());
        }
        fLastNudgeTime = now;
        fNudgeOffset += direction;

        if (fTransformBox) {
                fTransformBox->NudgeBy(direction);
                return;
        }

        if (!fNudgeCommand) {

                bool fromSelection = !fSelection->IsEmpty();

                int32 count = fromSelection ? fSelection->CountItems()
                                                                        : fPath->CountPoints();
                int32 indices[count];
                BStackOrHeapArray<control_point, 64> points(count);

                // init indices and points
                for (int32 i = 0; i < count; i++) {
                        indices[i] = fromSelection ? fSelection->IndexAt(i) : i;
                        fPath->GetPointsAt(indices[i],
                                                           points[i].point,
                                                           points[i].point_in,
                                                           points[i].point_out,
                                                           &points[i].connected);
                }

                fNudgeCommand = new NudgePointsCommand(fPath, indices, points, count);

                fNudgeCommand->SetNewTranslation(fNudgeOffset);
                fNudgeCommand->Redo();

        } else {
                fNudgeCommand->SetNewTranslation(fNudgeOffset);
                fNudgeCommand->Redo();
        }

        if (!fMouseDown)
                _SetModeForMousePos(fLastCanvasPos);
}


Command*
PathManipulator::_FinishNudging()
{
        fNudgeOffset = BPoint(0.0, 0.0);

        Command* command;

        if (fTransformBox) {
                command = fTransformBox->FinishNudging();
        } else {
                command = fNudgeCommand;
                fNudgeCommand = NULL;
        }

        return command;
}