1/*
2 * Copyright 2006-2009, Stephan A��mus <superstippi@gmx.de>.
3 * Copyright 2023, Haiku, Inc.
4 * All rights reserved. Distributed under the terms of the MIT License.
5 *
6 * Authors:
7 *             Zardshard
8 */
9
10#include "PathManipulator.h"
11
12#include <algorithm>
13#include <float.h>
14#include <stdio.h>
15#include <vector>
16
17#include <Catalog.h>
18#include <Cursor.h>
19#include <Locale.h>
20#include <Message.h>
21#include <MenuItem.h>
22#include <PopUpMenu.h>
23#include <StackOrHeapArray.h>
24#include <Window.h>
25
26#include "cursors.h"
27#include "support.h"
28
29#include "CanvasView.h"
30
31#include "AddPointCommand.h"
32#include "ChangePointCommand.h"
33//#include "CloseCommand.h"
34#include "InsertPointCommand.h"
35#include "FlipPointsCommand.h"
36//#include "NewPathCommand.h"
37#include "NudgePointsCommand.h"
38//#include "RemovePathCommand.h"
39#include "RemovePointsCommand.h"
40//#include "ReversePathCommand.h"
41//#include "SelectPathCommand.h"
42//#include "SelectPointsCommand.h"
43#include "SplitPointsCommand.h"
44#include "TransformPointsBox.h"
45
46
47#undef B_TRANSLATION_CONTEXT
48#define B_TRANSLATION_CONTEXT "Icon-O-Matic-PathManipulator"
49#define POINT_EXTEND 3.0
50#define CONTROL_POINT_EXTEND 2.0
51#define INSERT_DIST_THRESHOLD 7.0
52#define MOVE_THRESHOLD 9.0
53
54
55enum {
56	UNDEFINED,
57
58	NEW_PATH,
59
60	ADD_POINT,
61	INSERT_POINT,
62	MOVE_POINT,
63	MOVE_POINT_IN,
64	MOVE_POINT_OUT,
65	CLOSE_PATH,
66
67	TOGGLE_SHARP,
68	TOGGLE_SHARP_IN,
69	TOGGLE_SHARP_OUT,
70
71	REMOVE_POINT,
72	REMOVE_POINT_IN,
73	REMOVE_POINT_OUT,
74
75	SELECT_POINTS,
76	TRANSFORM_POINTS,
77	TRANSLATE_POINTS,
78
79	SELECT_SUB_PATH,
80};
81
82enum {
83	MSG_TRANSFORM				= 'strn',
84	MSG_REMOVE_POINTS			= 'srmp',
85	MSG_UPDATE_SHAPE_UI			= 'udsi',
86
87	MSG_SPLIT_POINTS			= 'splt',
88	MSG_FLIP_POINTS				= 'flip',
89};
90
91inline const char*
92string_for_mode(uint32 mode)
93{
94	switch (mode) {
95		case UNDEFINED:
96			return "UNDEFINED";
97		case NEW_PATH:
98			return "NEW_PATH";
99		case ADD_POINT:
100			return "ADD_POINT";
101		case INSERT_POINT:
102			return "INSERT_POINT";
103		case MOVE_POINT:
104			return "MOVE_POINT";
105		case MOVE_POINT_IN:
106			return "MOVE_POINT_IN";
107		case MOVE_POINT_OUT:
108			return "MOVE_POINT_OUT";
109		case CLOSE_PATH:
110			return "CLOSE_PATH";
111		case TOGGLE_SHARP:
112			return "TOGGLE_SHARP";
113		case TOGGLE_SHARP_IN:
114			return "TOGGLE_SHARP_IN";
115		case TOGGLE_SHARP_OUT:
116			return "TOGGLE_SHARP_OUT";
117		case REMOVE_POINT:
118			return "REMOVE_POINT";
119		case REMOVE_POINT_IN:
120			return "REMOVE_POINT_IN";
121		case REMOVE_POINT_OUT:
122			return "REMOVE_POINT_OUT";
123		case SELECT_POINTS:
124			return "SELECT_POINTS";
125		case TRANSFORM_POINTS:
126			return "TRANSFORM_POINTS";
127		case TRANSLATE_POINTS:
128			return "TRANSLATE_POINTS";
129		case SELECT_SUB_PATH:
130			return "SELECT_SUB_PATH";
131	}
132	return "<unknown mode>";
133}
134
135// NOTE: this class extends std::vector<int32> since neither BList or
136// BObjectList would suffice. The backing array of BList and BObjectList is a
137// void* array. The Items function should return an int32 array. This is a
138// problem since sizeof(void*) is not necessarily equal to sizeof(int32).
139class PathManipulator::Selection : protected std::vector<int32>
140{
141public:
142	inline Selection(int32 count = 20)
143		: _inherited() { reserve(count); }
144	inline ~Selection() {}
145
146	inline void Add(int32 value)
147	{
148		if (value >= 0) {
149			// keep the list sorted
150			insert(std::upper_bound(begin(), end(), value), value);
151		}
152	}
153
154	inline bool Remove(int32 value)
155	{
156		if (!Contains(value))
157			return false;
158		erase(std::lower_bound(begin(), end(), value));
159		return true;
160	}
161
162	inline bool Contains(int32 value) const
163		{ return std::binary_search(begin(), end(), value); }
164
165	inline bool IsEmpty() const
166		{ return size() == 0; }
167
168	inline int32 IndexAt(int32 index) const
169		{ return at(index); }
170
171	inline void MakeEmpty()
172		{ clear(); }
173
174	inline const int32* Items() const
175		{ return &(*this)[0]; }
176
177	inline const int32 CountItems() const
178		{ return size(); }
179
180	inline Selection& operator =(const Selection& other)
181	{
182		_inherited::operator=(other);
183		return *this;
184	}
185
186	inline bool operator ==(const Selection& other)
187		{ return (_inherited)*this == (_inherited)other; }
188
189	inline bool operator !=(const Selection& other)
190		{ return (_inherited)*this != (_inherited)other; }
191
192private:
193	typedef std::vector<int32> _inherited;
194};
195
196
197PathManipulator::PathManipulator(VectorPath* path)
198	: Manipulator(NULL),
199	  fCanvasView(NULL),
200
201	  fCommandDown(false),
202	  fOptionDown(false),
203	  fShiftDown(false),
204	  fAltDown(false),
205
206	  fClickToClose(false),
207
208	  fMode(NEW_PATH),
209	  fFallBackMode(SELECT_POINTS),
210
211	  fMouseDown(false),
212
213	  fPath(path),
214	  fCurrentPathPoint(-1),
215
216	  fChangePointCommand(NULL),
217	  fInsertPointCommand(NULL),
218	  fAddPointCommand(NULL),
219
220	  fSelection(new Selection()),
221	  fOldSelection(new Selection()),
222	  fTransformBox(NULL),
223
224	  fNudgeOffset(0.0, 0.0),
225	  fLastNudgeTime(system_time()),
226	  fNudgeCommand(NULL)
227{
228	fPath->AcquireReference();
229	fPath->AddListener(this);
230	fPath->AddObserver(this);
231}
232
233
234PathManipulator::~PathManipulator()
235{
236	delete fChangePointCommand;
237	delete fInsertPointCommand;
238	delete fAddPointCommand;
239
240	delete fSelection;
241	delete fOldSelection;
242	delete fTransformBox;
243
244	delete fNudgeCommand;
245
246	fPath->RemoveObserver(this);
247	fPath->RemoveListener(this);
248	fPath->ReleaseReference();
249}
250
251
252// #pragma mark -
253
254
255class StrokePathIterator : public VectorPath::Iterator {
256 public:
257					StrokePathIterator(CanvasView* canvasView,
258									   BView* drawingView)
259						: fCanvasView(canvasView),
260						  fDrawingView(drawingView)
261					{
262						fDrawingView->SetHighColor(0, 0, 0, 255);
263						fDrawingView->SetDrawingMode(B_OP_OVER);
264					}
265	virtual			~StrokePathIterator()
266					{}
267
268	virtual	void	MoveTo(BPoint point)
269					{
270						fBlack = true;
271						fSkip = false;
272						fDrawingView->SetHighColor(0, 0, 0, 255);
273
274						fCanvasView->ConvertFromCanvas(&point);
275						fDrawingView->MovePenTo(point);
276					}
277	virtual	void	LineTo(BPoint point)
278					{
279						fCanvasView->ConvertFromCanvas(&point);
280						if (!fSkip) {
281							if (fBlack)
282								fDrawingView->SetHighColor(255, 255, 255, 255);
283							else
284								fDrawingView->SetHighColor(0, 0, 0, 255);
285							fBlack = !fBlack;
286
287							fDrawingView->StrokeLine(point);
288						} else {
289							fDrawingView->MovePenTo(point);
290						}
291						fSkip = !fSkip;
292					}
293
294 private:
295	CanvasView*		fCanvasView;
296	BView*			fDrawingView;
297	bool			fBlack;
298	bool			fSkip;
299};
300
301
302void
303PathManipulator::Draw(BView* into, BRect updateRect)
304{
305	// draw the Bezier curve, but only if not "editing",
306	// the path is actually on top all other modifiers
307	// TODO: make this customizable in the GUI
308
309	#if __HAIKU__
310	uint32 flags = into->Flags();
311	into->SetFlags(flags | B_SUBPIXEL_PRECISE);
312	#endif // __HAIKU__
313
314	StrokePathIterator iterator(fCanvasView, into);
315	fPath->Iterate(&iterator, fCanvasView->ZoomLevel());
316
317	#if __HAIKU__
318	into->SetFlags(flags);
319	#endif // __HAIKU__
320
321	into->SetLowColor(0, 0, 0, 255);
322	BPoint point;
323	BPoint pointIn;
324	BPoint pointOut;
325	rgb_color focusColor = (rgb_color){ 255, 0, 0, 255 };
326	rgb_color highlightColor = (rgb_color){ 60, 60, 255, 255 };
327	for (int32 i = 0; fPath->GetPointsAt(i, point, pointIn, pointOut); i++) {
328		bool highlight = fCurrentPathPoint == i;
329		bool selected = fSelection->Contains(i);
330		rgb_color normal = selected ? focusColor : (rgb_color){ 0, 0, 0, 255 };
331		into->SetLowColor(normal);
332		into->SetHighColor(255, 255, 255, 255);
333		// convert to view coordinate space
334		fCanvasView->ConvertFromCanvas(&point);
335		fCanvasView->ConvertFromCanvas(&pointIn);
336		fCanvasView->ConvertFromCanvas(&pointOut);
337		// connect the points belonging to one control point
338		into->SetDrawingMode(B_OP_INVERT);
339		into->StrokeLine(point, pointIn);
340		into->StrokeLine(point, pointOut);
341		// draw main control point
342		if (highlight && (fMode == MOVE_POINT ||
343						  fMode == TOGGLE_SHARP ||
344						  fMode == REMOVE_POINT ||
345						  fMode == SELECT_POINTS ||
346						  fMode == CLOSE_PATH)) {
347
348			into->SetLowColor(highlightColor);
349		}
350
351		into->SetDrawingMode(B_OP_COPY);
352		BRect r(point, point);
353		r.InsetBy(-POINT_EXTEND, -POINT_EXTEND);
354		into->StrokeRect(r, B_SOLID_LOW);
355		r.InsetBy(1.0, 1.0);
356		into->FillRect(r, B_SOLID_HIGH);
357		// draw in control point
358		if (highlight && (fMode == MOVE_POINT_IN ||
359						  fMode == TOGGLE_SHARP_IN ||
360						  fMode == REMOVE_POINT_IN ||
361						  fMode == SELECT_POINTS))
362			into->SetLowColor(highlightColor);
363		else
364			into->SetLowColor(normal);
365		if (selected) {
366			into->SetHighColor(220, 220, 220, 255);
367		} else {
368			into->SetHighColor(170, 170, 170, 255);
369		}
370		if (pointIn != point) {
371			r.Set(pointIn.x - CONTROL_POINT_EXTEND, pointIn.y - CONTROL_POINT_EXTEND,
372				  pointIn.x + CONTROL_POINT_EXTEND, pointIn.y + CONTROL_POINT_EXTEND);
373			into->StrokeRect(r, B_SOLID_LOW);
374			r.InsetBy(1.0, 1.0);
375			into->FillRect(r, B_SOLID_HIGH);
376		}
377		// draw out control point
378		if (highlight && (fMode == MOVE_POINT_OUT ||
379						  fMode == TOGGLE_SHARP_OUT ||
380						  fMode == REMOVE_POINT_OUT ||
381						  fMode == SELECT_POINTS))
382			into->SetLowColor(highlightColor);
383		else
384			into->SetLowColor(normal);
385		if (pointOut != point) {
386			r.Set(pointOut.x - CONTROL_POINT_EXTEND, pointOut.y - CONTROL_POINT_EXTEND,
387				  pointOut.x + CONTROL_POINT_EXTEND, pointOut.y + CONTROL_POINT_EXTEND);
388			into->StrokeRect(r, B_SOLID_LOW);
389			r.InsetBy(1.0, 1.0);
390			into->FillRect(r, B_SOLID_HIGH);
391		}
392	}
393
394	if (fTransformBox) {
395		fTransformBox->Draw(into, updateRect);
396	}
397}
398
399
400// #pragma mark -
401
402
403bool
404PathManipulator::MouseDown(BPoint where)
405{
406	fMouseDown = true;
407
408	if (fMode == TRANSFORM_POINTS) {
409		if (fTransformBox) {
410			fTransformBox->MouseDown(where);
411
412//			if (!fTransformBox->IsRotating())
413//				fCanvasView->SetAutoScrolling(true);
414		}
415		return true;
416	}
417
418	if (fMode == MOVE_POINT &&
419		fSelection->CountItems() > 1 &&
420		fSelection->Contains(fCurrentPathPoint)) {
421		fMode = TRANSLATE_POINTS;
422	}
423
424	// apply the canvas view mouse filter depending on current mode
425	if (fMode == ADD_POINT || fMode == TRANSLATE_POINTS)
426		fCanvasView->FilterMouse(&where);
427
428	BPoint canvasWhere = where;
429	fCanvasView->ConvertToCanvas(&canvasWhere);
430
431	// maybe we're changing some point, so we construct the
432	// "ChangePointCommand" here so that the point is remembered
433	// in its current state
434	// apply the canvas view mouse filter depending on current mode
435	delete fChangePointCommand;
436	fChangePointCommand = NULL;
437	switch (fMode) {
438		case TOGGLE_SHARP:
439		case TOGGLE_SHARP_IN:
440		case TOGGLE_SHARP_OUT:
441		case MOVE_POINT:
442		case MOVE_POINT_IN:
443		case MOVE_POINT_OUT:
444		case REMOVE_POINT_IN:
445		case REMOVE_POINT_OUT:
446			fChangePointCommand = new ChangePointCommand(fPath,
447														 fCurrentPathPoint,
448														 fSelection->Items(),
449														 fSelection->CountItems());
450			_Select(fCurrentPathPoint, fShiftDown);
451			break;
452	}
453
454	// at this point we init doing something
455	switch (fMode) {
456		case ADD_POINT:
457			_AddPoint(canvasWhere);
458			break;
459		case INSERT_POINT:
460			_InsertPoint(canvasWhere, fCurrentPathPoint);
461			break;
462
463		case TOGGLE_SHARP:
464			_SetSharp(fCurrentPathPoint);
465			// continue by dragging out the _connected_ in/out points
466			break;
467		case TOGGLE_SHARP_IN:
468			_SetInOutConnected(fCurrentPathPoint, false);
469			// continue by moving the "in" point
470			_SetMode(MOVE_POINT_IN);
471			break;
472		case TOGGLE_SHARP_OUT:
473			_SetInOutConnected(fCurrentPathPoint, false);
474			// continue by moving the "out" point
475			_SetMode(MOVE_POINT_OUT);
476			break;
477
478		case MOVE_POINT:
479		case MOVE_POINT_IN:
480		case MOVE_POINT_OUT:
481			// the right thing happens since "fCurrentPathPoint"
482			// points to the correct index
483			break;
484
485		case CLOSE_PATH:
486//			SetClosed(true, true);
487			break;
488
489		case REMOVE_POINT:
490			if (fPath->CountPoints() == 1) {
491//				fCanvasView->Perform(new RemovePathCommand(this, fPath));
492			} else {
493				fCanvasView->Perform(new RemovePointsCommand(fPath,
494															 fCurrentPathPoint,
495															 fSelection->Items(),
496															 fSelection->CountItems()));
497				_RemovePoint(fCurrentPathPoint);
498			}
499			break;
500		case REMOVE_POINT_IN:
501			_RemovePointIn(fCurrentPathPoint);
502			break;
503		case REMOVE_POINT_OUT:
504			_RemovePointOut(fCurrentPathPoint);
505			break;
506
507		case SELECT_POINTS: {
508			// TODO: this works so that you can deselect all points
509			// when clicking outside the path even if pressing shift
510			// in case the path is open... a better way would be
511			// to deselect all on mouse up, if the mouse has not moved
512			bool appendSelection;
513			if (fPath->IsClosed())
514				appendSelection = fShiftDown;
515			else
516				appendSelection = fShiftDown && fCurrentPathPoint >= 0;
517
518			if (!appendSelection) {
519				fSelection->MakeEmpty();
520				_UpdateSelection();
521			}
522			*fOldSelection = *fSelection;
523			if (fCurrentPathPoint >= 0) {
524				_Select(fCurrentPathPoint, appendSelection);
525			}
526			fCanvasView->BeginRectTracking(BRect(where, where),
527				B_TRACK_RECT_CORNER);
528			break;
529		}
530	}
531
532	fTrackingStart = canvasWhere;
533	// remember the subpixel position
534	// so that MouseMoved() will work even before
535	// the integer position becomes different
536	fLastCanvasPos = where;
537	fCanvasView->ConvertToCanvas(&fLastCanvasPos);
538
539	// the reason to exclude the select mode
540	// is that the BView rect tracking does not
541	// scroll the rect starting point along with us
542	// (since we're doing no real scrolling)
543//	if (fMode != SELECT_POINTS)
544//		fCanvasView->SetAutoScrolling(true);
545
546	UpdateCursor();
547
548	return true;
549}
550
551
552void
553PathManipulator::MouseMoved(BPoint where)
554{
555	fCanvasView->FilterMouse(&where);
556		// NOTE: only filter mouse coords in mouse moved, no other
557		// mouse function
558	BPoint canvasWhere = where;
559	fCanvasView->ConvertToCanvas(&canvasWhere);
560
561	// since the tablet is generating mouse moved messages
562	// even if only the pressure changes (and not the actual mouse position)
563	// we insert this additional check to prevent too much calculation
564	if (fLastCanvasPos == canvasWhere)
565		return;
566
567	fLastCanvasPos = canvasWhere;
568
569	if (fMode == TRANSFORM_POINTS) {
570		if (fTransformBox) {
571			fTransformBox->MouseMoved(where);
572		}
573		return;
574	}
575
576	if (fMode == CLOSE_PATH) {
577		// continue by moving the point
578		_SetMode(MOVE_POINT);
579		delete fChangePointCommand;
580		fChangePointCommand = new ChangePointCommand(fPath,
581													 fCurrentPathPoint,
582													 fSelection->Items(),
583													 fSelection->CountItems());
584	}
585
586//	if (!fPrecise) {
587//		float offset = fmod(fOutlineWidth, 2.0) / 2.0;
588//		canvasWhere.point += BPoint(offset, offset);
589//	}
590
591	switch (fMode) {
592		case ADD_POINT:
593		case INSERT_POINT:
594		case TOGGLE_SHARP:
595			// drag the "out" control point, mirror the "in" control point
596			fPath->SetPointOut(fCurrentPathPoint, canvasWhere, true);
597			break;
598		case MOVE_POINT:
599			// drag all three control points at once
600			fPath->SetPoint(fCurrentPathPoint, canvasWhere);
601			break;
602		case MOVE_POINT_IN:
603			// drag in control point
604			fPath->SetPointIn(fCurrentPathPoint, canvasWhere);
605			break;
606		case MOVE_POINT_OUT:
607			// drag out control point
608			fPath->SetPointOut(fCurrentPathPoint, canvasWhere);
609			break;
610
611		case SELECT_POINTS: {
612			// change the selection
613			BRect r;
614			r.left = min_c(fTrackingStart.x, canvasWhere.x);
615			r.top = min_c(fTrackingStart.y, canvasWhere.y);
616			r.right = max_c(fTrackingStart.x, canvasWhere.x);
617			r.bottom = max_c(fTrackingStart.y, canvasWhere.y);
618			_Select(r);
619			break;
620		}
621
622		case TRANSLATE_POINTS: {
623			BPoint offset = canvasWhere - fTrackingStart;
624			_Nudge(offset);
625			fTrackingStart = canvasWhere;
626			break;
627		}
628	}
629}
630
631
632Command*
633PathManipulator::MouseUp()
634{
635	// prevent carrying out actions more than once by only
636	// doing it if "fMouseDown" is true at the point of
637	// entering this function
638	if (!fMouseDown)
639		return NULL;
640	fMouseDown = false;
641
642	if (fMode == TRANSFORM_POINTS) {
643		if (fTransformBox) {
644			return fTransformBox->MouseUp();
645		}
646		return NULL;
647	}
648
649	Command* command = NULL;
650
651	switch (fMode) {
652
653		case ADD_POINT:
654			command = fAddPointCommand;
655			fAddPointCommand = NULL;
656			_SetMode(MOVE_POINT_OUT);
657			break;
658
659		case INSERT_POINT:
660			command = fInsertPointCommand;
661			fInsertPointCommand = NULL;
662			break;
663
664		case SELECT_POINTS:
665			if (*fSelection != *fOldSelection) {
666//				command = new SelectPointsCommand(this, fPath,
667//												  fOldSelection->Items(),
668//												  fOldSelection->CountItems(),
669//												  fSelection->Items(),
670//												  fSelection->CountItems()));
671			}
672			fCanvasView->EndRectTracking();
673			break;
674
675		case TOGGLE_SHARP:
676		case TOGGLE_SHARP_IN:
677		case TOGGLE_SHARP_OUT:
678		case MOVE_POINT:
679		case MOVE_POINT_IN:
680		case MOVE_POINT_OUT:
681		case REMOVE_POINT_IN:
682		case REMOVE_POINT_OUT:
683			command = fChangePointCommand;
684			fChangePointCommand = NULL;
685			break;
686
687		case TRANSLATE_POINTS:
688			if (!fNudgeCommand) {
689				// select just the point that was clicked
690				*fOldSelection = *fSelection;
691				if (fCurrentPathPoint >= 0) {
692					_Select(fCurrentPathPoint, fShiftDown);
693				}
694				if (*fSelection != *fOldSelection) {
695//					command = new SelectPointsCommand(this, fPath,
696//													  fOldSelection->Items(),
697//													  fOldSelection->CountItems(),
698//													  fSelection->Items(),
699//													  fSelection->CountItems()));
700				}
701			} else {
702				command = _FinishNudging();
703			}
704			break;
705	}
706
707	return command;
708}
709
710
711bool
712PathManipulator::MouseOver(BPoint where)
713{
714	if (fMode == TRANSFORM_POINTS) {
715		if (fTransformBox) {
716			return fTransformBox->MouseOver(where);
717		}
718		return false;
719	}
720
721	BPoint canvasWhere = where;
722	fCanvasView->ConvertToCanvas(&canvasWhere);
723
724	// since the tablet is generating mouse moved messages
725	// even if only the pressure changes (and not the actual mouse position)
726	// we insert this additional check to prevent too much calculation
727	if (fMouseDown && fLastCanvasPos == canvasWhere)
728		return false;
729
730	fLastCanvasPos = canvasWhere;
731
732	// hit testing
733	// (use a subpixel mouse pos)
734	fCanvasView->ConvertToCanvas(&where);
735	_SetModeForMousePos(where);
736
737	// TODO: always true?
738	return true;
739}
740
741
742bool
743PathManipulator::DoubleClicked(BPoint where)
744{
745	return false;
746}
747
748
749bool
750PathManipulator::ShowContextMenu(BPoint where)
751{
752	// Change the selection to the current point if it isn't currently
753	// selected. This could will only be chosen if the user right-clicked
754	// a path point directly.
755	if (fCurrentPathPoint >= 0 && !fSelection->Contains(fCurrentPathPoint)) {
756		fSelection->MakeEmpty();
757		_UpdateSelection();
758		*fOldSelection = *fSelection;
759		_Select(fCurrentPathPoint, false);
760	}
761
762	BPopUpMenu* menu = new BPopUpMenu("context menu", false, false);
763	BMessage* message;
764	BMenuItem* item;
765
766	bool hasSelection = fSelection->CountItems() > 0;
767
768	if (fCurrentPathPoint < 0) {
769		message = new BMessage(B_SELECT_ALL);
770		item = new BMenuItem(B_TRANSLATE("Select all"), message, 'A');
771		menu->AddItem(item);
772
773		menu->AddSeparatorItem();
774	}
775
776	message = new BMessage(MSG_TRANSFORM);
777	item = new BMenuItem(B_TRANSLATE("Transform"), message, 'T');
778	item->SetEnabled(hasSelection);
779	menu->AddItem(item);
780
781	message = new BMessage(MSG_SPLIT_POINTS);
782	item = new BMenuItem(B_TRANSLATE("Split"), message);
783	item->SetEnabled(hasSelection);
784	menu->AddItem(item);
785
786	message = new BMessage(MSG_FLIP_POINTS);
787	item = new BMenuItem(B_TRANSLATE("Flip"), message);
788	item->SetEnabled(hasSelection);
789	menu->AddItem(item);
790
791	message = new BMessage(MSG_REMOVE_POINTS);
792	item = new BMenuItem(B_TRANSLATE("Remove"), message);
793	item->SetEnabled(hasSelection);
794	menu->AddItem(item);
795
796	// go
797	menu->SetTargetForItems(fCanvasView);
798	menu->SetAsyncAutoDestruct(true);
799	menu->SetFont(be_plain_font);
800	where = fCanvasView->ConvertToScreen(where);
801	BRect mouseRect(where, where);
802	mouseRect.InsetBy(-10.0, -10.0);
803	where += BPoint(5.0, 5.0);
804	menu->Go(where, true, false, mouseRect, true);
805
806	return true;
807}
808
809
810// #pragma mark -
811
812
813BRect
814PathManipulator::Bounds()
815{
816	BRect r = _ControlPointRect();
817	fCanvasView->ConvertFromCanvas(&r);
818	return r;
819}
820
821
822BRect
823PathManipulator::TrackingBounds(BView* withinView)
824{
825	return withinView->Bounds();
826}
827
828
829// #pragma mark -
830
831
832bool
833PathManipulator::MessageReceived(BMessage* message, Command** _command)
834{
835	bool result = true;
836	switch (message->what) {
837		case MSG_TRANSFORM:
838			if (!fSelection->IsEmpty())
839				_SetMode(TRANSFORM_POINTS);
840			break;
841		case MSG_REMOVE_POINTS:
842			*_command = _Delete();
843			break;
844		case MSG_SPLIT_POINTS:
845			*_command = new SplitPointsCommand(fPath,
846											   fSelection->Items(),
847											   fSelection->CountItems());
848			break;
849		case MSG_FLIP_POINTS:
850			*_command = new FlipPointsCommand(fPath,
851											  fSelection->Items(),
852											  fSelection->CountItems());
853			break;
854		case B_SELECT_ALL: {
855			int32 count = fPath->CountPoints();
856			int32 indices[count];
857
858			for (int32 i = 0; i < count; i++)
859				indices[i] = i;
860
861			_Select(indices, count);
862			break;
863		}
864		default:
865			result = false;
866			break;
867	}
868	return result;
869}
870
871
872void
873PathManipulator::ModifiersChanged(uint32 modifiers)
874{
875	fCommandDown = modifiers & B_COMMAND_KEY;
876	fOptionDown = modifiers & B_CONTROL_KEY;
877	fShiftDown = modifiers & B_SHIFT_KEY;
878	fAltDown = modifiers & B_OPTION_KEY;
879
880	if (fTransformBox) {
881		fTransformBox->ModifiersChanged(modifiers);
882		return;
883	}
884	// reevaluate mode
885	if (!fMouseDown)
886		_SetModeForMousePos(fLastCanvasPos);
887}
888
889
890bool
891PathManipulator::HandleKeyDown(uint32 key, uint32 modifiers, Command** _command)
892{
893	bool result = true;
894
895	float nudgeDist = 1.0;
896	if (modifiers & B_SHIFT_KEY)
897		nudgeDist /= fCanvasView->ZoomLevel();
898
899	switch (key) {
900		// commit
901		case B_RETURN:
902			if (fTransformBox) {
903				_SetTransformBox(NULL);
904			}// else
905//				_Perform();
906			break;
907		// cancel
908		case B_ESCAPE:
909			if (fTransformBox) {
910				fTransformBox->Cancel();
911				_SetTransformBox(NULL);
912			} else if (fFallBackMode == NEW_PATH) {
913				fFallBackMode = SELECT_POINTS;
914				_SetTransformBox(NULL);
915			}// else
916//				_Cancel();
917			break;
918		case 't':
919		case 'T':
920			if (!fSelection->IsEmpty())
921				_SetMode(TRANSFORM_POINTS);
922			else
923				result = false;
924			break;
925		// nudging
926		case B_UP_ARROW:
927			_Nudge(BPoint(0.0, -nudgeDist));
928			break;
929		case B_DOWN_ARROW:
930			_Nudge(BPoint(0.0, nudgeDist));
931			break;
932		case B_LEFT_ARROW:
933			_Nudge(BPoint(-nudgeDist, 0.0));
934			break;
935		case B_RIGHT_ARROW:
936			_Nudge(BPoint(nudgeDist, 0.0));
937			break;
938
939		case B_DELETE:
940			if (!fSelection->IsEmpty())
941				*_command = _Delete();
942			else
943				result = false;
944			break;
945
946		default:
947			result = false;
948	}
949	return result;
950}
951
952
953bool
954PathManipulator::HandleKeyUp(uint32 key, uint32 modifiers, Command** _command)
955{
956	bool handled = true;
957	switch (key) {
958		// nudging
959		case B_UP_ARROW:
960		case B_DOWN_ARROW:
961		case B_LEFT_ARROW:
962		case B_RIGHT_ARROW:
963			*_command = _FinishNudging();
964			break;
965		default:
966			handled = false;
967			break;
968	}
969	return handled;
970}
971
972
973bool
974PathManipulator::UpdateCursor()
975{
976	if (fTransformBox)
977		return fTransformBox->UpdateCursor();
978
979	const uchar* cursorData;
980	switch (fMode) {
981		case ADD_POINT:
982			cursorData = kPathAddCursor;
983			break;
984		case INSERT_POINT:
985			cursorData = kPathInsertCursor;
986			break;
987		case MOVE_POINT:
988		case MOVE_POINT_IN:
989		case MOVE_POINT_OUT:
990		case TRANSLATE_POINTS:
991			cursorData = kPathMoveCursor;
992			break;
993		case CLOSE_PATH:
994			cursorData = kPathCloseCursor;
995			break;
996		case TOGGLE_SHARP:
997		case TOGGLE_SHARP_IN:
998		case TOGGLE_SHARP_OUT:
999			cursorData = kPathSharpCursor;
1000			break;
1001		case REMOVE_POINT:
1002		case REMOVE_POINT_IN:
1003		case REMOVE_POINT_OUT:
1004			cursorData = kPathRemoveCursor;
1005			break;
1006		case SELECT_POINTS:
1007			cursorData = kPathSelectCursor;
1008			break;
1009
1010		case SELECT_SUB_PATH:
1011			cursorData = B_HAND_CURSOR;
1012			break;
1013
1014		case UNDEFINED:
1015		default:
1016			cursorData = kStopCursor;
1017			break;
1018	}
1019	BCursor cursor(cursorData);
1020	fCanvasView->SetViewCursor(&cursor, true);
1021	fCanvasView->Sync();
1022
1023	return true;
1024}
1025
1026
1027void
1028PathManipulator::AttachedToView(BView* view)
1029{
1030	fCanvasView = dynamic_cast<CanvasView*>(view);
1031}
1032
1033
1034void
1035PathManipulator::DetachedFromView(BView* view)
1036{
1037	fCanvasView = NULL;
1038}
1039
1040
1041// #pragma mark -
1042
1043
1044void
1045PathManipulator::ObjectChanged(const Observable* object)
1046{
1047	// TODO: refine VectorPath listener interface and
1048	// implement more efficiently
1049	BRect currentBounds = _ControlPointRect();
1050	_InvalidateCanvas(currentBounds | fPreviousBounds);
1051	fPreviousBounds = currentBounds;
1052
1053	// reevaluate mode
1054	if (!fMouseDown && !fTransformBox)
1055		_SetModeForMousePos(fLastCanvasPos);
1056}
1057
1058
1059// #pragma mark -
1060
1061
1062void
1063PathManipulator::PointAdded(int32 index)
1064{
1065	ObjectChanged(fPath);
1066}
1067
1068
1069void
1070PathManipulator::PointRemoved(int32 index)
1071{
1072	fSelection->Remove(index);
1073	ObjectChanged(fPath);
1074}
1075
1076
1077void
1078PathManipulator::PointChanged(int32 index)
1079{
1080	ObjectChanged(fPath);
1081}
1082
1083
1084void
1085PathManipulator::PathChanged()
1086{
1087	ObjectChanged(fPath);
1088}
1089
1090
1091void
1092PathManipulator::PathClosedChanged()
1093{
1094	ObjectChanged(fPath);
1095}
1096
1097
1098void
1099PathManipulator::PathReversed()
1100{
1101	// reverse selection along with path
1102	int32 count = fSelection->CountItems();
1103	int32 pointCount = fPath->CountPoints();
1104	if (count > 0) {
1105		Selection temp;
1106		for (int32 i = 0; i < count; i++) {
1107			temp.Add((pointCount - 1) - fSelection->IndexAt(i));
1108		}
1109		*fSelection = temp;
1110	}
1111
1112	ObjectChanged(fPath);
1113}
1114
1115
1116// #pragma mark -
1117
1118
1119uint32
1120PathManipulator::ControlFlags() const
1121{
1122	uint32 flags = 0;
1123
1124//	flags |= SHAPE_UI_FLAGS_CAN_REVERSE_PATH;
1125//
1126//	if (!fSelection->IsEmpty())
1127//		flags |= SHAPE_UI_FLAGS_HAS_SELECTION;
1128//	if (fPath->CountPoints() > 1)
1129//		flags |= SHAPE_UI_FLAGS_CAN_CLOSE_PATH;
1130//	if (fPath->IsClosed())
1131//		flags |= SHAPE_UI_FLAGS_PATH_IS_CLOSED;
1132//	if (fTransformBox)
1133//		flags |= SHAPE_UI_FLAGS_IS_TRANSFORMING;
1134
1135	return flags;
1136}
1137
1138
1139void
1140PathManipulator::ReversePath()
1141{
1142	int32 count = fSelection->CountItems();
1143	int32 pointCount = fPath->CountPoints();
1144	if (count > 0) {
1145		Selection temp;
1146		for (int32 i = 0; i < count; i++) {
1147			temp.Add((pointCount - 1) - fSelection->IndexAt(i));
1148		}
1149		*fSelection = temp;
1150	}
1151	fPath->Reverse();
1152}
1153
1154
1155// #pragma mark -
1156
1157
1158void
1159PathManipulator::_SetMode(uint32 mode)
1160{
1161	if (fMode != mode) {
1162//printf("switching mode: %s -> %s\n", string_for_mode(fMode), string_for_mode(mode));
1163		fMode = mode;
1164
1165		if (fMode == TRANSFORM_POINTS) {
1166			_SetTransformBox(new TransformPointsBox(fCanvasView,
1167													this,
1168													fPath,
1169													fSelection->Items(),
1170													fSelection->CountItems()));
1171//			fCanvasView->Perform(new EnterTransformPointsCommand(this,
1172//														  fSelection->Items(),
1173//														  fSelection->CountItems()));
1174		} else {
1175			if (fTransformBox)
1176				_SetTransformBox(NULL);
1177		}
1178
1179		if (BWindow* window = fCanvasView->Window()) {
1180			window->PostMessage(MSG_UPDATE_SHAPE_UI);
1181		}
1182		UpdateCursor();
1183	}
1184}
1185
1186
1187void
1188PathManipulator::_SetTransformBox(TransformPointsBox* transformBox)
1189{
1190	if (fTransformBox == transformBox)
1191		return;
1192
1193	BRect dirty(LONG_MAX, LONG_MAX, LONG_MIN, LONG_MIN);
1194	if (fTransformBox) {
1195		// get rid of transform box display
1196		dirty = fTransformBox->Bounds();
1197		delete fTransformBox;
1198	}
1199
1200	fTransformBox = transformBox;
1201
1202	// TODO: this is weird, fMode should only be set in _SetMode, not
1203	// here as well, also this method could be called this way
1204	// _SetModeForMousePos -> _SetMode -> _SetTransformBox
1205	// and then below it does _SetModeForMousePos again...
1206	if (fTransformBox) {
1207		fTransformBox->MouseMoved(fLastCanvasPos);
1208		if (fMode != TRANSFORM_POINTS) {
1209			fMode = TRANSFORM_POINTS;
1210		}
1211		dirty = dirty | fTransformBox->Bounds();
1212	} else {
1213		if (fMode == TRANSFORM_POINTS) {
1214			_SetModeForMousePos(fLastCanvasPos);
1215		}
1216	}
1217
1218	if (dirty.IsValid()) {
1219		dirty.InsetBy(-8, -8);
1220		fCanvasView->Invalidate(dirty);
1221	}
1222}
1223
1224
1225void
1226PathManipulator::_AddPoint(BPoint where)
1227{
1228	if (fPath->AddPoint(where)) {
1229		fCurrentPathPoint = fPath->CountPoints() - 1;
1230
1231		delete fAddPointCommand;
1232		fAddPointCommand = new AddPointCommand(fPath, fCurrentPathPoint,
1233											   fSelection->Items(),
1234											   fSelection->CountItems());
1235
1236		_Select(fCurrentPathPoint, fShiftDown);
1237	}
1238}
1239
1240
1241BPoint
1242scale_point(BPoint a, BPoint b, float scale)
1243{
1244	return BPoint(a.x + (b.x - a.x) * scale,
1245				  a.y + (b.y - a.y) * scale);
1246}
1247
1248
1249void
1250PathManipulator::_InsertPoint(BPoint where, int32 index)
1251{
1252	double scale;
1253
1254	BPoint point;
1255	BPoint pointIn;
1256	BPoint pointOut;
1257
1258	BPoint previous;
1259	BPoint previousOut;
1260	BPoint next;
1261	BPoint nextIn;
1262
1263	if (fPath->FindBezierScale(index - 1, where, &scale)
1264		&& scale >= 0.0 && scale <= 1.0
1265		&& fPath->GetPoint(index - 1, scale, point)) {
1266
1267		fPath->GetPointAt(index - 1, previous);
1268		fPath->GetPointOutAt(index - 1, previousOut);
1269		fPath->GetPointAt(index, next);
1270		fPath->GetPointInAt(index, nextIn);
1271
1272		where = scale_point(previousOut, nextIn, scale);
1273
1274		previousOut = scale_point(previous, previousOut, scale);
1275		nextIn = scale_point(next, nextIn, 1 - scale);
1276		pointIn = scale_point(previousOut, where, scale);
1277		pointOut = scale_point(nextIn, where, 1 - scale);
1278
1279		if (fPath->AddPoint(point, index)) {
1280
1281			fPath->SetPointIn(index, pointIn);
1282			fPath->SetPointOut(index, pointOut);
1283
1284			delete fInsertPointCommand;
1285			fInsertPointCommand = new InsertPointCommand(fPath, index,
1286														 fSelection->Items(),
1287														 fSelection->CountItems());
1288
1289			fPath->SetPointOut(index - 1, previousOut);
1290			fPath->SetPointIn(index + 1, nextIn);
1291
1292			fCurrentPathPoint = index;
1293			_ShiftSelection(fCurrentPathPoint, 1);
1294			_Select(fCurrentPathPoint, fShiftDown);
1295		}
1296	}
1297}
1298
1299
1300void
1301PathManipulator::_SetInOutConnected(int32 index, bool connected)
1302{
1303	fPath->SetInOutConnected(index, connected);
1304}
1305
1306
1307void
1308PathManipulator::_SetSharp(int32 index)
1309{
1310	BPoint p;
1311	fPath->GetPointAt(index, p);
1312	fPath->SetPoint(index, p, p, p, true);
1313}
1314
1315
1316void
1317PathManipulator::_RemoveSelection()
1318{
1319	// NOTE: copy selection since removing points will
1320	// trigger notifications, and that will influence the
1321	// selection
1322	Selection selection = *fSelection;
1323	int32 count = selection.CountItems();
1324	for (int32 i = 0; i < count; i++) {
1325		if (!fPath->RemovePoint(selection.IndexAt(i) - i))
1326			break;
1327	}
1328
1329	fPath->SetClosed(fPath->IsClosed() && fPath->CountPoints() > 1);
1330
1331	fSelection->MakeEmpty();
1332}
1333
1334
1335void
1336PathManipulator::_RemovePoint(int32 index)
1337{
1338	if (fPath->RemovePoint(index)) {
1339		_Deselect(index);
1340		_ShiftSelection(index + 1, -1);
1341	}
1342}
1343
1344
1345void
1346PathManipulator::_RemovePointIn(int32 index)
1347{
1348	BPoint p;
1349	if (fPath->GetPointAt(index, p)) {
1350		fPath->SetPointIn(index, p);
1351		fPath->SetInOutConnected(index, false);
1352	}
1353}
1354
1355
1356void
1357PathManipulator::_RemovePointOut(int32 index)
1358{
1359	BPoint p;
1360	if (fPath->GetPointAt(index, p)) {
1361		fPath->SetPointOut(index, p);
1362		fPath->SetInOutConnected(index, false);
1363	}
1364}
1365
1366
1367Command*
1368PathManipulator::_Delete()
1369{
1370	Command* command = NULL;
1371	if (!fMouseDown) {
1372		// make sure we apply an on-going transformation before we proceed
1373		if (fTransformBox) {
1374			_SetTransformBox(NULL);
1375		}
1376
1377		if (fSelection->CountItems() == fPath->CountPoints()) {
1378//			command = new RemovePathCommand(fPath);
1379		} else {
1380			command = new RemovePointsCommand(fPath,
1381											  fSelection->Items(),
1382											  fSelection->CountItems());
1383			_RemoveSelection();
1384		}
1385
1386		_SetModeForMousePos(fLastCanvasPos);
1387	}
1388
1389	return command;
1390}
1391
1392
1393// #pragma mark -
1394
1395
1396void
1397PathManipulator::_Select(BRect r)
1398{
1399	BPoint p;
1400	BPoint pIn;
1401	BPoint pOut;
1402	int32 count = fPath->CountPoints();
1403	Selection temp;
1404	for (int32 i = 0; i < count && fPath->GetPointsAt(i, p, pIn, pOut); i++) {
1405		if (r.Contains(p) || r.Contains(pIn) || r.Contains(pOut)) {
1406			temp.Add(i);
1407		}
1408	}
1409	// merge old and new selection
1410	count = fOldSelection->CountItems();
1411	for (int32 i = 0; i < count; i++) {
1412		int32 index = fOldSelection->IndexAt(i);
1413		if (temp.Contains(index))
1414			temp.Remove(index);
1415		else
1416			temp.Add(index);
1417	}
1418	if (temp != *fSelection) {
1419		*fSelection = temp;
1420		_UpdateSelection();
1421	}
1422}
1423
1424
1425void
1426PathManipulator::_Select(int32 index, bool extend)
1427{
1428	if (!extend)
1429		fSelection->MakeEmpty();
1430	if (fSelection->Contains(index))
1431		fSelection->Remove(index);
1432	else
1433		fSelection->Add(index);
1434	// TODO: this can lead to unnecessary invalidation (maybe need to investigate)
1435	_UpdateSelection();
1436}
1437
1438
1439void
1440PathManipulator::_Select(const int32* indices, int32 count, bool extend)
1441{
1442	if (extend) {
1443		for (int32 i = 0; i < count; i++) {
1444			if (!fSelection->Contains(indices[i]))
1445				fSelection->Add(indices[i]);
1446		}
1447	} else {
1448		fSelection->MakeEmpty();
1449		for (int32 i = 0; i < count; i++) {
1450			fSelection->Add(indices[i]);
1451		}
1452	}
1453	_UpdateSelection();
1454}
1455
1456
1457void
1458PathManipulator::_Deselect(int32 index)
1459{
1460	if (fSelection->Contains(index)) {
1461		fSelection->Remove(index);
1462		_UpdateSelection();
1463	}
1464}
1465
1466
1467void
1468PathManipulator::_ShiftSelection(int32 startIndex, int32 direction)
1469{
1470	int32 count = fSelection->CountItems();
1471	if (count > 0) {
1472		for (int32 i = 0; i < count; i++) {
1473			int32 index = fSelection->IndexAt(i);
1474			if (index >= startIndex) {
1475				fSelection->Remove(index);
1476				fSelection->Add(index + direction);
1477			}
1478		}
1479	}
1480	_UpdateSelection();
1481}
1482
1483
1484bool
1485PathManipulator::_IsSelected(int32 index) const
1486{
1487	return fSelection->Contains(index);
1488}
1489
1490
1491// #pragma mark -
1492
1493
1494void
1495PathManipulator::_InvalidateCanvas(BRect rect) const
1496{
1497	// convert from canvas to view space
1498	fCanvasView->ConvertFromCanvas(&rect);
1499	fCanvasView->Invalidate(rect);
1500}
1501
1502
1503void
1504PathManipulator::_InvalidateHighlightPoints(int32 newIndex, uint32 newMode)
1505{
1506	BRect oldRect = _ControlPointRect(fCurrentPathPoint, fMode);
1507	BRect newRect = _ControlPointRect(newIndex, newMode);
1508	if (oldRect.IsValid())
1509		_InvalidateCanvas(oldRect);
1510	if (newRect.IsValid())
1511		_InvalidateCanvas(newRect);
1512}
1513
1514
1515void
1516PathManipulator::_UpdateSelection() const
1517{
1518	_InvalidateCanvas(_ControlPointRect());
1519	if (BWindow* window = fCanvasView->Window()) {
1520		window->PostMessage(MSG_UPDATE_SHAPE_UI);
1521	}
1522}
1523
1524
1525BRect
1526PathManipulator::_ControlPointRect() const
1527{
1528	BRect r = fPath->ControlPointBounds();
1529	r.InsetBy(-POINT_EXTEND, -POINT_EXTEND);
1530	return r;
1531}
1532
1533
1534BRect
1535PathManipulator::_ControlPointRect(int32 index, uint32 mode) const
1536{
1537	BRect rect(0.0, 0.0, -1.0, -1.0);
1538	if (index >= 0) {
1539		BPoint p, pIn, pOut;
1540		fPath->GetPointsAt(index, p, pIn, pOut);
1541		switch (mode) {
1542			case MOVE_POINT:
1543			case TOGGLE_SHARP:
1544			case REMOVE_POINT:
1545			case CLOSE_PATH:
1546				rect.Set(p.x, p.y, p.x, p.y);
1547				rect.InsetBy(-POINT_EXTEND, -POINT_EXTEND);
1548				break;
1549			case MOVE_POINT_IN:
1550			case TOGGLE_SHARP_IN:
1551			case REMOVE_POINT_IN:
1552				rect.Set(pIn.x, pIn.y, pIn.x, pIn.y);
1553				rect.InsetBy(-CONTROL_POINT_EXTEND, -CONTROL_POINT_EXTEND);
1554				break;
1555			case MOVE_POINT_OUT:
1556			case TOGGLE_SHARP_OUT:
1557			case REMOVE_POINT_OUT:
1558				rect.Set(pOut.x, pOut.y, pOut.x, pOut.y);
1559				rect.InsetBy(-CONTROL_POINT_EXTEND, -CONTROL_POINT_EXTEND);
1560				break;
1561			case SELECT_POINTS:
1562				rect.Set(min4(p.x, pIn.x, pOut.x, pOut.x),
1563						 min4(p.y, pIn.y, pOut.y, pOut.y),
1564						 max4(p.x, pIn.x, pOut.x, pOut.x),
1565						 max4(p.y, pIn.y, pOut.y, pOut.y));
1566				rect.InsetBy(-POINT_EXTEND, -POINT_EXTEND);
1567				break;
1568		}
1569	}
1570	return rect;
1571}
1572
1573
1574// #pragma mark -
1575
1576
1577void
1578PathManipulator::_SetModeForMousePos(BPoint where)
1579{
1580	uint32 mode = UNDEFINED;
1581	int32 index = -1;
1582
1583	float zoomLevel = fCanvasView->ZoomLevel();
1584
1585	// see if we're close enough at a control point
1586	BPoint point;
1587	BPoint pointIn;
1588	BPoint pointOut;
1589	for (int32 i = 0; fPath->GetPointsAt(i, point, pointIn, pointOut)
1590					  && mode == UNDEFINED; i++) {
1591
1592		float distM = point_point_distance(point, where) * zoomLevel;
1593		float distIn = point_point_distance(pointIn, where) * zoomLevel;
1594		float distOut = point_point_distance(pointOut, where) * zoomLevel;
1595
1596		if (distM < MOVE_THRESHOLD) {
1597			if (i == 0 && fClickToClose
1598				&& !fPath->IsClosed() && fPath->CountPoints() > 1) {
1599				mode = fCommandDown ? TOGGLE_SHARP :
1600							(fOptionDown ? REMOVE_POINT : CLOSE_PATH);
1601				index = i;
1602			} else {
1603				mode = fCommandDown ? TOGGLE_SHARP :
1604							(fOptionDown ? REMOVE_POINT : MOVE_POINT);
1605				index = i;
1606			}
1607		}
1608		if (distM - distIn > 0.00001
1609			&& distIn < MOVE_THRESHOLD) {
1610			mode = fCommandDown ? TOGGLE_SHARP_IN :
1611						(fOptionDown ? REMOVE_POINT_IN : MOVE_POINT_IN);
1612			index = i;
1613		}
1614		if (distIn - distOut > 0.00001
1615			&& distOut < distM && distOut < MOVE_THRESHOLD) {
1616			mode = fCommandDown ? TOGGLE_SHARP_OUT :
1617						(fOptionDown ? REMOVE_POINT_OUT : MOVE_POINT_OUT);
1618			index = i;
1619		}
1620	}
1621	// selection mode overrides any other mode,
1622	// but we need to check for it after we know
1623	// the index of the point under the mouse (code above)
1624	int32 pointCount = fPath->CountPoints();
1625	if (fShiftDown && pointCount > 0) {
1626		mode = SELECT_POINTS;
1627	}
1628
1629	// see if user wants to start new sub path
1630	if (fAltDown) {
1631		mode = NEW_PATH;
1632		index = -1;
1633	}
1634
1635	// see if we're close enough at a line
1636	if (mode == UNDEFINED) {
1637		float distance;
1638		if (fPath->GetDistance(where, &distance, &index)) {
1639			if (distance < (INSERT_DIST_THRESHOLD / zoomLevel)) {
1640				mode = INSERT_POINT;
1641			}
1642		} else {
1643			// restore index, since it was changed by call above
1644			index = fCurrentPathPoint;
1645		}
1646	}
1647
1648	// nope, still undefined mode, last fall back
1649	if (mode == UNDEFINED) {
1650		if (fFallBackMode == SELECT_POINTS) {
1651			if (fPath->IsClosed() && pointCount > 0) {
1652				mode = SELECT_POINTS;
1653				index = -1;
1654			} else {
1655				mode = ADD_POINT;
1656				index = pointCount - 1;
1657			}
1658		} else {
1659			// user had clicked "New Path" icon
1660			mode = fFallBackMode;
1661		}
1662	}
1663	// switch mode if necessary
1664	if (mode != fMode || index != fCurrentPathPoint) {
1665		// invalidate path display (to highlight the respective point)
1666		_InvalidateHighlightPoints(index, mode);
1667		_SetMode(mode);
1668		fCurrentPathPoint = index;
1669	}
1670}
1671
1672
1673// #pragma mark -
1674
1675
1676void
1677PathManipulator::_Nudge(BPoint direction)
1678{
1679	bigtime_t now = system_time();
1680	if (now - fLastNudgeTime > 500000) {
1681		fCanvasView->Perform(_FinishNudging());
1682	}
1683	fLastNudgeTime = now;
1684	fNudgeOffset += direction;
1685
1686	if (fTransformBox) {
1687		fTransformBox->NudgeBy(direction);
1688		return;
1689	}
1690
1691	if (!fNudgeCommand) {
1692
1693		bool fromSelection = !fSelection->IsEmpty();
1694
1695		int32 count = fromSelection ? fSelection->CountItems()
1696									: fPath->CountPoints();
1697		int32 indices[count];
1698		BStackOrHeapArray<control_point, 64> points(count);
1699
1700		// init indices and points
1701		for (int32 i = 0; i < count; i++) {
1702			indices[i] = fromSelection ? fSelection->IndexAt(i) : i;
1703			fPath->GetPointsAt(indices[i],
1704							   points[i].point,
1705							   points[i].point_in,
1706							   points[i].point_out,
1707							   &points[i].connected);
1708		}
1709
1710		fNudgeCommand = new NudgePointsCommand(fPath, indices, points, count);
1711
1712		fNudgeCommand->SetNewTranslation(fNudgeOffset);
1713		fNudgeCommand->Redo();
1714
1715	} else {
1716		fNudgeCommand->SetNewTranslation(fNudgeOffset);
1717		fNudgeCommand->Redo();
1718	}
1719
1720	if (!fMouseDown)
1721		_SetModeForMousePos(fLastCanvasPos);
1722}
1723
1724
1725Command*
1726PathManipulator::_FinishNudging()
1727{
1728	fNudgeOffset = BPoint(0.0, 0.0);
1729
1730	Command* command;
1731
1732	if (fTransformBox) {
1733		command = fTransformBox->FinishNudging();
1734	} else {
1735		command = fNudgeCommand;
1736		fNudgeCommand = NULL;
1737	}
1738
1739	return command;
1740}
1741