root/src/apps/icon-o-matic/transformable/TransformBox.cpp
/*
 * Copyright 2006-2009, Haiku.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *              Stephan Aßmus <superstippi@gmx.de>
 */

#include "TransformBox.h"

#include <stdio.h>

#include <agg_trans_affine.h>
#include <agg_math.h>

#include <View.h>

#include "support.h"

#include "TransformBoxStates.h"
#include "StateView.h"
#include "TransformCommand.h"


#define INSET 8.0


using namespace TransformBoxStates;


// constructor
TransformBox::TransformBox(StateView* view, BRect box)
        :
        ChannelTransform(),
        Manipulator(NULL),
        fOriginalBox(box),

        fLeftTop(box.LeftTop()),
        fRightTop(box.RightTop()),
        fLeftBottom(box.LeftBottom()),
        fRightBottom(box.RightBottom()),

        fPivot((fLeftTop.x + fRightBottom.x) / 2.0,
                (fLeftTop.y + fRightBottom.y) / 2.0),
        fPivotOffset(B_ORIGIN),
        fCurrentCommand(NULL),
        fCurrentState(NULL),

        fDragging(false),
        fMousePos(-10000.0, -10000.0),
        fModifiers(0),

        fNudging(false),

        fView(view),

        fDragLTState(new DragCornerState(this, DragCornerState::LEFT_TOP_CORNER)),
        fDragRTState(new DragCornerState(this, DragCornerState::RIGHT_TOP_CORNER)),
        fDragLBState(new DragCornerState(this, DragCornerState::LEFT_BOTTOM_CORNER)),
        fDragRBState(new DragCornerState(this, DragCornerState::RIGHT_BOTTOM_CORNER)),

        fDragLState(new DragSideState(this, DragSideState::LEFT_SIDE)),
        fDragRState(new DragSideState(this, DragSideState::RIGHT_SIDE)),
        fDragTState(new DragSideState(this, DragSideState::TOP_SIDE)),
        fDragBState(new DragSideState(this, DragSideState::BOTTOM_SIDE)),

        fRotateState(new RotateBoxState(this)),
        fTranslateState(new DragBoxState(this)),
        fOffsetCenterState(new OffsetCenterState(this))
{
}


// destructor
TransformBox::~TransformBox()
{
        _NotifyDeleted();

        delete fCurrentCommand;

        delete fDragLTState;
        delete fDragRTState;
        delete fDragLBState;
        delete fDragRBState;

        delete fDragLState;
        delete fDragRState;
        delete fDragTState;
        delete fDragBState;

        delete fRotateState;
        delete fTranslateState;
        delete fOffsetCenterState;
}


// Draw
void
TransformBox::Draw(BView* into, BRect updateRect)
{
        // convert to canvas view coordinates
        BPoint lt = fLeftTop;
        BPoint rt = fRightTop;
        BPoint lb = fLeftBottom;
        BPoint rb = fRightBottom;
        BPoint c = fPivot;

        TransformFromCanvas(lt);
        TransformFromCanvas(rt);
        TransformFromCanvas(lb);
        TransformFromCanvas(rb);
        TransformFromCanvas(c);

        into->SetDrawingMode(B_OP_COPY);
        into->SetHighColor(255, 255, 255, 255);
        into->SetLowColor(0, 0, 0, 255);
        _StrokeBWLine(into, lt, rt);
        _StrokeBWLine(into, rt, rb);
        _StrokeBWLine(into, rb, lb);
        _StrokeBWLine(into, lb, lt);

        double rotation = ViewSpaceRotation();
        _StrokeBWPoint(into, lt, rotation);
        _StrokeBWPoint(into, rt, rotation + 90.0);
        _StrokeBWPoint(into, rb, rotation + 180.0);
        _StrokeBWPoint(into, lb, rotation + 270.0);

        BRect cr(c, c);
        cr.InsetBy(-3.0, -3.0);
        into->StrokeEllipse(cr, B_SOLID_HIGH);
        cr.InsetBy(1.0, 1.0);
        into->StrokeEllipse(cr, B_SOLID_LOW);
        into->SetDrawingMode(B_OP_COPY);
}


// #pragma mark -


// MouseDown
bool
TransformBox::MouseDown(BPoint where)
{
        fView->FilterMouse(&where);
        TransformToCanvas(where);

        fDragging = true;
        if (fCurrentState) {
                fCurrentState->SetOrigin(where);

                delete fCurrentCommand;
                fCurrentCommand = MakeCommand(fCurrentState->ActionName());
        }

        return true;
}


// MouseMoved
void
TransformBox::MouseMoved(BPoint where)
{
        fView->FilterMouse(&where);
        TransformToCanvas(where);

        if (fMousePos != where) {
                fMousePos = where;
                if (fCurrentState) {
                        fCurrentState->DragTo(fMousePos, fModifiers);
                        fCurrentState->UpdateViewCursor(fView, fMousePos);
                }
        }
}


// MouseUp
Command*
TransformBox::MouseUp()
{
        fDragging = false;
        return FinishTransaction();
}


// MouseOver
bool
TransformBox::MouseOver(BPoint where)
{
        TransformToCanvas(where);

        fMousePos = where;
        fCurrentState = _DragStateFor(where, ZoomLevel());
        fCurrentState->UpdateViewCursor(fView, fMousePos);

        return true;
}


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


// #pragma mark -


// Bounds
BRect
TransformBox::Bounds()
{
        // convert from canvas view coordinates
        BPoint lt = fLeftTop;
        BPoint rt = fRightTop;
        BPoint lb = fLeftBottom;
        BPoint rb = fRightBottom;
        BPoint c = fPivot;

        TransformFromCanvas(lt);
        TransformFromCanvas(rt);
        TransformFromCanvas(lb);
        TransformFromCanvas(rb);
        TransformFromCanvas(c);

        BRect bounds;
        bounds.left = min5(lt.x, rt.x, lb.x, rb.x, c.x);
        bounds.top = min5(lt.y, rt.y, lb.y, rb.y, c.y);
        bounds.right = max5(lt.x, rt.x, lb.x, rb.x, c.x);
        bounds.bottom = max5(lt.y, rt.y, lb.y, rb.y, c.y);
        return bounds;
}


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


// #pragma mark -


// ModifiersChanged
void
TransformBox::ModifiersChanged(uint32 modifiers)
{
        fModifiers = modifiers;
        if (fDragging && fCurrentState) {
                fCurrentState->DragTo(fMousePos, fModifiers);
        }
}


// HandleKeyDown
bool
TransformBox::HandleKeyDown(uint32 key, uint32 modifiers, Command** _command)
{
        bool handled = true;
        BPoint translation(B_ORIGIN);

        float offset = 1.0;
        if (modifiers & B_SHIFT_KEY)
                offset /= ZoomLevel();

        switch (key) {
                case B_UP_ARROW:
                        translation.y = -offset;
                        break;
                case B_DOWN_ARROW:
                        translation.y = offset;
                        break;
                case B_LEFT_ARROW:
                        translation.x = -offset;
                        break;
                case B_RIGHT_ARROW:
                        translation.x = offset;
                        break;

                default:
                        handled = false;
                        break;
        }

        if (!handled)
                return false;

        if (!fCurrentCommand) {
                fCurrentCommand = MakeCommand("Translate");
        }

        TranslateBy(translation);

        return true;
}


// HandleKeyUp
bool
TransformBox::HandleKeyUp(uint32 key, uint32 modifiers, Command** _command)
{
        if (fCurrentCommand) {
                *_command = FinishTransaction();
                return true;
        }
        return false;
}


// UpdateCursor
bool
TransformBox::UpdateCursor()
{
        if (fCurrentState) {
                fCurrentState->UpdateViewCursor(fView, fMousePos);
                return true;
        }
        return false;
}


// #pragma mark -


// AttachedToView
void
TransformBox::AttachedToView(BView* view)
{
        view->Invalidate(Bounds().InsetByCopy(-INSET, -INSET));
}


// DetachedFromView
void
TransformBox::DetachedFromView(BView* view)
{
        view->Invalidate(Bounds().InsetByCopy(-INSET, -INSET));
}


// pragma mark -


// Update
void
TransformBox::Update(bool deep)
{
        // recalculate the points from the original box
        fLeftTop = fOriginalBox.LeftTop();
        fRightTop = fOriginalBox.RightTop();
        fLeftBottom = fOriginalBox.LeftBottom();
        fRightBottom = fOriginalBox.RightBottom();

        fPivot.x = (fLeftTop.x + fRightBottom.x) / 2.0;
        fPivot.y = (fLeftTop.y + fRightBottom.y) / 2.0;

        fPivot += fPivotOffset;

        // transform the points for display
        Transform(&fLeftTop);
        Transform(&fRightTop);
        Transform(&fLeftBottom);
        Transform(&fRightBottom);

        Transform(&fPivot);
}


// OffsetCenter
void
TransformBox::OffsetCenter(BPoint offset)
{
        if (offset != BPoint(0.0, 0.0)) {
                fPivotOffset += offset;
                Update(false);
        }
}


// Center
BPoint
TransformBox::Center() const
{
        return fPivot;
}


// SetBox
void
TransformBox::SetBox(BRect box)
{
        if (fOriginalBox != box) {
                fOriginalBox = box;
                Update(false);
        }
}


// FinishTransaction
Command*
TransformBox::FinishTransaction()
{
        Command* command = fCurrentCommand;
        if (fCurrentCommand) {
                fCurrentCommand->SetNewTransformation(Pivot(), Translation(),
                        LocalRotation(), LocalXScale(), LocalYScale());
                fCurrentCommand = NULL;
        }
        return command;
}


// NudgeBy
void
TransformBox::NudgeBy(BPoint offset)
{
        if (!fNudging && !fCurrentCommand) {
                fCurrentCommand = MakeCommand("Move");
                fNudging = true;
        }
        if (fNudging) {
                TranslateBy(offset);
        }
}


// FinishNudging
Command*
TransformBox::FinishNudging()
{
        fNudging = false;
        return FinishTransaction();
}


// TransformFromCanvas
void
TransformBox::TransformFromCanvas(BPoint& point) const
{
}


// TransformToCanvas
void
TransformBox::TransformToCanvas(BPoint& point) const
{
}


// ZoomLevel
float
TransformBox::ZoomLevel() const
{
        return 1.0;
}


// ViewSpaceRotation
double
TransformBox::ViewSpaceRotation() const
{
        // assume no inherited transformation
        return LocalRotation();
}


// #pragma mark -


// AddListener
bool
TransformBox::AddListener(TransformBoxListener* listener)
{
        if (listener && !fListeners.HasItem((void*)listener))
                return fListeners.AddItem((void*)listener);
        return false;
}


// RemoveListener
bool
TransformBox::RemoveListener(TransformBoxListener* listener)
{
        return fListeners.RemoveItem((void*)listener);
}


// #pragma mark -


// TODO: why another version?
// point_line_dist
float
point_line_dist(BPoint start, BPoint end, BPoint p, float radius)
{
        BRect r(min_c(start.x, end.x), min_c(start.y, end.y), max_c(start.x, end.x),
                max_c(start.y, end.y));
        r.InsetBy(-radius, -radius);
        if (r.Contains(p)) {
                return fabs(agg::calc_line_point_distance(start.x, start.y, end.x, end.y,
                        p.x, p.y));
        }

        return min_c(point_point_distance(start, p), point_point_distance(end, p));
}


// _DragStateFor
//! where is expected in canvas view coordinates
DragState*
TransformBox::_DragStateFor(BPoint where, float canvasZoom)
{
        DragState* state = NULL;
        // convert to canvas zoom level
        //
        // the conversion is necessary, because the "hot regions"
        // around a point should be the same size no matter what
        // zoom level the canvas is displayed at

        float inset = INSET / canvasZoom;

        // priorities:
        // transformation center point has highest priority ?!?
        if (point_point_distance(where, fPivot) < inset)
                state = fOffsetCenterState;

        if (!state) {
                // next, the inner area of the box

                // for the following calculations
                // we can apply the inverse transformation to all points
                // this way we have to consider BRects only, not transformed polygons
                BPoint lt = fLeftTop;
                BPoint rb = fRightBottom;
                BPoint w = where;

                InverseTransform(&w);
                InverseTransform(&lt);
                InverseTransform(&rb);

                // next priority has the inside of the box
                BRect iR(lt, rb);
                float hInset = min_c(inset, max_c(0, (iR.Width() - inset) / 2.0));
                float vInset = min_c(inset, max_c(0, (iR.Height() - inset) / 2.0));

                iR.InsetBy(hInset, vInset);
                if (iR.Contains(w))
                        state = fTranslateState;
        }

        if (!state) {
                // next priority have the corners
                float dLT = point_point_distance(fLeftTop, where);
                float dRT = point_point_distance(fRightTop, where);
                float dLB = point_point_distance(fLeftBottom, where);
                float dRB = point_point_distance(fRightBottom, where);
                float d = min4(dLT, dRT, dLB, dRB);
                if (d < inset) {
                        if (d == dLT)
                                state = fDragLTState;
                        else if (d == dRT)
                                state = fDragRTState;
                        else if (d == dLB)
                                state = fDragLBState;
                        else if (d == dRB)
                                state = fDragRBState;
                }
        }

        if (!state) {
                // next priority have the sides
                float dL = point_line_dist(fLeftTop, fLeftBottom, where, inset);
                float dR = point_line_dist(fRightTop, fRightBottom, where, inset);
                float dT = point_line_dist(fLeftTop, fRightTop, where, inset);
                float dB = point_line_dist(fLeftBottom, fRightBottom, where, inset);
                float d = min4(dL, dR, dT, dB);
                if (d < inset) {
                        if (d == dL)
                                state = fDragLState;
                        else if (d == dR)
                                state = fDragRState;
                        else if (d == dT)
                                state = fDragTState;
                        else if (d == dB)
                                state = fDragBState;
                }
        }

        if (!state) {
                BPoint lt = fLeftTop;
                BPoint rb = fRightBottom;
                BPoint w = where;

                InverseTransform(&w);
                InverseTransform(&lt);
                InverseTransform(&rb);

                // check inside of the box again
                BRect iR(lt, rb);
                if (iR.Contains(w)) {
                        state = fTranslateState;
                } else {
                        // last priority has the rotate state
                        state = fRotateState;
                }
        }

        return state;
}


// _StrokeBWLine
void
TransformBox::_StrokeBWLine(BView* into, BPoint from, BPoint to) const
{
        // find out how to offset the second line optimally
        BPoint offset(0.0, 0.0);
        // first, do we have a more horizontal line or a more vertical line?
        float xDiff = to.x - from.x;
        float yDiff = to.y - from.y;
        if (fabs(xDiff) > fabs(yDiff)) {
                // horizontal
                if (xDiff > 0.0) {
                        offset.y = -1.0;
                } else {
                        offset.y = 1.0;
                }
        } else {
                // vertical
                if (yDiff < 0.0) {
                        offset.x = -1.0;
                } else {
                        offset.x = 1.0;
                }
        }
        // stroke two lines in high and low color of the view
        into->StrokeLine(from, to, B_SOLID_LOW);
        from += offset;
        to += offset;
        into->StrokeLine(from, to, B_SOLID_HIGH);
}


// _StrokeBWPoint
void
TransformBox::_StrokeBWPoint(BView* into, BPoint point, double angle) const
{
        double x = point.x;
        double y = point.y;

        double x1 = x;
        double y1 = y - 5.0;

        double x2 = x - 5.0;
        double y2 = y - 5.0;

        double x3 = x - 5.0;
        double y3 = y;

        agg::trans_affine m;

        double xOffset = -x;
        double yOffset = -y;

        agg::trans_affine_rotation r(angle * M_PI / 180.0);

        r.transform(&xOffset, &yOffset);
        xOffset = x + xOffset;
        yOffset = y + yOffset;

        m.multiply(r);
        m.multiply(agg::trans_affine_translation(xOffset, yOffset));

        m.transform(&x, &y);
        m.transform(&x1, &y1);
        m.transform(&x2, &y2);
        m.transform(&x3, &y3);

        BPoint p[4];
        p[0] = BPoint(x, y);
        p[1] = BPoint(x1, y1);
        p[2] = BPoint(x2, y2);
        p[3] = BPoint(x3, y3);

        into->FillPolygon(p, 4, B_SOLID_HIGH);

        into->StrokeLine(p[0], p[1], B_SOLID_LOW);
        into->StrokeLine(p[1], p[2], B_SOLID_LOW);
        into->StrokeLine(p[2], p[3], B_SOLID_LOW);
        into->StrokeLine(p[3], p[0], B_SOLID_LOW);
}


// #pragma mark -


// _NotifyDeleted
void
TransformBox::_NotifyDeleted() const
{
        BList listeners(fListeners);
        int32 count = listeners.CountItems();
        for (int32 i = 0; i < count; i++) {
                TransformBoxListener* listener
                        = (TransformBoxListener*)listeners.ItemAtFast(i);
                listener->TransformBoxDeleted(this);
        }
}