1/*
2 * Copyright 2006-2007, 2011, Stephan Aßmus <superstippi@gmx.de>
3 * All rights reserved. Distributed under the terms of the MIT License.
4 */
5
6
7#include "CanvasView.h"
8
9#include <Bitmap.h>
10#include <Cursor.h>
11#include <Message.h>
12#include <Region.h>
13#include <Window.h>
14
15#include <stdio.h>
16
17#include "cursors.h"
18#include "ui_defines.h"
19
20#include "CommandStack.h"
21#include "IconRenderer.h"
22
23
24using std::nothrow;
25
26
27CanvasView::CanvasView(BRect frame)
28	:
29	StateView(frame, "canvas view", B_FOLLOW_ALL,
30		B_WILL_DRAW | B_FRAME_EVENTS),
31	fBitmap(new BBitmap(BRect(0, 0, 63, 63), 0, B_RGB32)),
32	fBackground(new BBitmap(BRect(0, 0, 63, 63), 0, B_RGB32)),
33	fIcon(NULL),
34	fRenderer(new IconRenderer(fBitmap)),
35	fDirtyIconArea(fBitmap->Bounds()),
36
37	fCanvasOrigin(0.0, 0.0),
38	fZoomLevel(8.0),
39
40	fSpaceHeldDown(false),
41	fInScrollTo(false),
42	fScrollTracking(false),
43	fScrollTrackingStart(0.0, 0.0),
44
45	fMouseFilterMode(SNAPPING_OFF)
46{
47	_MakeBackground();
48	fRenderer->SetBackground(fBackground);
49}
50
51
52CanvasView::~CanvasView()
53{
54	SetIcon(NULL);
55	delete fRenderer;
56	delete fBitmap;
57	delete fBackground;
58}
59
60
61// #pragma mark -
62
63
64void
65CanvasView::AttachedToWindow()
66{
67	StateView::AttachedToWindow();
68
69	SetViewColor(B_TRANSPARENT_COLOR);
70	SetLowColor(kStripesHigh);
71	SetHighColor(kStripesLow);
72
73	// init data rect for scrolling and center bitmap in the view
74	BRect dataRect = _LayoutCanvas();
75	SetDataRect(dataRect);
76	BRect bounds(Bounds());
77	BPoint dataRectCenter((dataRect.left + dataRect.right) / 2,
78		(dataRect.top + dataRect.bottom) / 2);
79	BPoint boundsCenter((bounds.left + bounds.right) / 2,
80		(bounds.top + bounds.bottom) / 2);
81	BPoint offset = ScrollOffset();
82	offset.x = roundf(offset.x + dataRectCenter.x - boundsCenter.x);
83	offset.y = roundf(offset.y + dataRectCenter.y - boundsCenter.y);
84	SetScrollOffset(offset);
85}
86
87
88void
89CanvasView::FrameResized(float width, float height)
90{
91	// keep canvas centered
92	BPoint oldCanvasOrigin = fCanvasOrigin;
93	SetDataRect(_LayoutCanvas());
94	if (oldCanvasOrigin != fCanvasOrigin)
95		Invalidate();
96}
97
98
99void
100CanvasView::Draw(BRect updateRect)
101{
102	_DrawInto(this, updateRect);
103}
104
105
106// #pragma mark -
107
108
109void
110CanvasView::MouseDown(BPoint where)
111{
112	if (!IsFocus())
113		MakeFocus(true);
114
115	int32 buttons;
116	if (Window()->CurrentMessage()->FindInt32("buttons", &buttons) < B_OK)
117		buttons = 0;
118
119	// handle clicks of the third mouse button ourselves (panning),
120	// otherwise have StateView handle it (normal clicks)
121	if (fSpaceHeldDown || (buttons & B_TERTIARY_MOUSE_BUTTON) != 0) {
122		// switch into scrolling mode and update cursor
123		fScrollTracking = true;
124		where.x = roundf(where.x);
125		where.y = roundf(where.y);
126		fScrollOffsetStart = ScrollOffset();
127		fScrollTrackingStart = where - fScrollOffsetStart;
128		_UpdateToolCursor();
129		SetMouseEventMask(B_POINTER_EVENTS,
130			B_LOCK_WINDOW_FOCUS | B_SUSPEND_VIEW_FOCUS);
131	} else {
132		StateView::MouseDown(where);
133	}
134}
135
136
137void
138CanvasView::MouseUp(BPoint where)
139{
140	if (fScrollTracking) {
141		// stop scroll tracking and update cursor
142		fScrollTracking = false;
143		_UpdateToolCursor();
144		// update StateView mouse position
145		uint32 transit = Bounds().Contains(where) ?
146			B_INSIDE_VIEW : B_OUTSIDE_VIEW;
147		StateView::MouseMoved(where, transit, NULL);
148	} else {
149		StateView::MouseUp(where);
150	}
151}
152
153
154void
155CanvasView::MouseMoved(BPoint where, uint32 transit, const BMessage* dragMessage)
156{
157	if (fScrollTracking) {
158		uint32 buttons;
159		GetMouse(&where, &buttons, false);
160		if (!buttons) {
161			MouseUp(where);
162			return;
163		}
164		where.x = roundf(where.x);
165		where.y = roundf(where.y);
166		where -= ScrollOffset();
167		BPoint offset = where - fScrollTrackingStart;
168		SetScrollOffset(fScrollOffsetStart - offset);
169	} else {
170		// normal mouse movement handled by StateView
171		if (!fSpaceHeldDown)
172			StateView::MouseMoved(where, transit, dragMessage);
173	}
174}
175
176
177void
178CanvasView::FilterMouse(BPoint* where) const
179{
180	switch (fMouseFilterMode) {
181
182		case SNAPPING_64:
183			ConvertToCanvas(where);
184			where->x = floorf(where->x + 0.5);
185			where->y = floorf(where->y + 0.5);
186			ConvertFromCanvas(where);
187			break;
188
189		case SNAPPING_32:
190			ConvertToCanvas(where);
191			where->x /= 2.0;
192			where->y /= 2.0;
193			where->x = floorf(where->x + 0.5);
194			where->y = floorf(where->y + 0.5);
195			where->x *= 2.0;
196			where->y *= 2.0;
197			ConvertFromCanvas(where);
198			break;
199
200		case SNAPPING_16:
201			ConvertToCanvas(where);
202			where->x /= 4.0;
203			where->y /= 4.0;
204			where->x = floorf(where->x + 0.5);
205			where->y = floorf(where->y + 0.5);
206			where->x *= 4.0;
207			where->y *= 4.0;
208			ConvertFromCanvas(where);
209			break;
210
211		case SNAPPING_OFF:
212		default:
213			break;
214	}
215}
216
217
218bool
219CanvasView::MouseWheelChanged(BPoint where, float x, float y)
220{
221	if (!Bounds().Contains(where))
222		return false;
223
224	if (y > 0.0) {
225		_SetZoom(_NextZoomOutLevel(fZoomLevel), true);
226		return true;
227	} else if (y < 0.0) {
228		_SetZoom(_NextZoomInLevel(fZoomLevel), true);
229		return true;
230	}
231	return false;
232}
233
234
235// #pragma mark -
236
237
238void
239CanvasView::SetScrollOffset(BPoint newOffset)
240{
241	if (fInScrollTo)
242		return;
243
244	fInScrollTo = true;
245
246	newOffset = ValidScrollOffsetFor(newOffset);
247	if (!fScrollTracking) {
248		BPoint mouseOffset = newOffset - ScrollOffset();
249		MouseMoved(fMouseInfo.position + mouseOffset, fMouseInfo.transit,
250			NULL);
251	}
252
253	Scrollable::SetScrollOffset(newOffset);
254
255	fInScrollTo = false;
256}
257
258
259void
260CanvasView::ScrollOffsetChanged(BPoint oldOffset, BPoint newOffset)
261{
262	BPoint offset = newOffset - oldOffset;
263
264	if (offset == B_ORIGIN) {
265		// prevent circular code (MouseMoved might call ScrollBy...)
266		return;
267	}
268
269	ScrollBy(offset.x, offset.y);
270}
271
272
273void
274CanvasView::VisibleSizeChanged(float oldWidth, float oldHeight, float newWidth,
275	float newHeight)
276{
277	BRect dataRect(_LayoutCanvas());
278	SetDataRect(dataRect);
279}
280
281
282// #pragma mark -
283
284
285void
286CanvasView::AreaInvalidated(const BRect& area)
287{
288	if (fDirtyIconArea.Contains(area))
289		return;
290
291	fDirtyIconArea = fDirtyIconArea | area;
292
293	BRect viewArea(area);
294	ConvertFromCanvas(&viewArea);
295	Invalidate(viewArea);
296}
297
298
299// #pragma mark -
300
301
302void
303CanvasView::SetIcon(Icon* icon)
304{
305	if (fIcon == icon)
306		return;
307
308	if (fIcon)
309		fIcon->RemoveListener(this);
310
311	fIcon = icon;
312	fRenderer->SetIcon(icon);
313
314	if (fIcon)
315		fIcon->AddListener(this);
316}
317
318
319void
320CanvasView::SetMouseFilterMode(uint32 mode)
321{
322	if (fMouseFilterMode == mode)
323		return;
324
325	fMouseFilterMode = mode;
326	Invalidate(_CanvasRect());
327}
328
329
330void
331CanvasView::ConvertFromCanvas(BPoint* point) const
332{
333	point->x = point->x * fZoomLevel + fCanvasOrigin.x;
334	point->y = point->y * fZoomLevel + fCanvasOrigin.y;
335}
336
337
338void
339CanvasView::ConvertToCanvas(BPoint* point) const
340{
341	point->x = (point->x - fCanvasOrigin.x) / fZoomLevel;
342	point->y = (point->y - fCanvasOrigin.y) / fZoomLevel;
343}
344
345
346void
347CanvasView::ConvertFromCanvas(BRect* r) const
348{
349	r->left = r->left * fZoomLevel + fCanvasOrigin.x;
350	r->top = r->top * fZoomLevel + fCanvasOrigin.y;
351	r->right++;
352	r->bottom++;
353	r->right = r->right * fZoomLevel + fCanvasOrigin.x;
354	r->bottom = r->bottom * fZoomLevel + fCanvasOrigin.y;
355	r->right--;
356	r->bottom--;
357}
358
359
360void
361CanvasView::ConvertToCanvas(BRect* r) const
362{
363	r->left = (r->left - fCanvasOrigin.x) / fZoomLevel;
364	r->top = (r->top - fCanvasOrigin.y) / fZoomLevel;
365	r->right = (r->right - fCanvasOrigin.x) / fZoomLevel;
366	r->bottom = (r->bottom - fCanvasOrigin.y) / fZoomLevel;
367}
368
369
370// #pragma mark -
371
372
373bool
374CanvasView::_HandleKeyDown(uint32 key, uint32 modifiers)
375{
376	switch (key) {
377		case 'z':
378		case 'y':
379			if (modifiers & B_SHIFT_KEY)
380				CommandStack()->Redo();
381			else
382				CommandStack()->Undo();
383			break;
384
385		case '+':
386			_SetZoom(_NextZoomInLevel(fZoomLevel));
387			break;
388		case '-':
389			_SetZoom(_NextZoomOutLevel(fZoomLevel));
390			break;
391
392		case B_SPACE:
393			fSpaceHeldDown = true;
394			_UpdateToolCursor();
395			break;
396
397		default:
398			return StateView::_HandleKeyDown(key, modifiers);
399	}
400
401	return true;
402}
403
404
405bool
406CanvasView::_HandleKeyUp(uint32 key, uint32 modifiers)
407{
408	switch (key) {
409		case B_SPACE:
410			fSpaceHeldDown = false;
411			_UpdateToolCursor();
412			break;
413
414		default:
415			return StateView::_HandleKeyUp(key, modifiers);
416	}
417
418	return true;
419}
420
421
422BRect
423CanvasView::_CanvasRect() const
424{
425	BRect r;
426	if (fBitmap == NULL)
427		return r;
428	r = fBitmap->Bounds();
429	ConvertFromCanvas(&r);
430	return r;
431}
432
433
434void
435CanvasView::_DrawInto(BView* view, BRect updateRect)
436{
437	if (fDirtyIconArea.IsValid()) {
438		fRenderer->Render(fDirtyIconArea);
439		fDirtyIconArea.Set(LONG_MAX, LONG_MAX, LONG_MIN, LONG_MIN);
440	}
441
442	// icon
443	BRect canvas(_CanvasRect());
444	view->DrawBitmap(fBitmap, fBitmap->Bounds(), canvas);
445
446	// grid
447	int32 gridLines = 0;
448	int32 scale = 1;
449	switch (fMouseFilterMode) {
450		case SNAPPING_64:
451			gridLines = 63;
452			break;
453		case SNAPPING_32:
454			gridLines = 31;
455			scale = 2;
456			break;
457		case SNAPPING_16:
458			gridLines = 15;
459			scale = 4;
460			break;
461		case SNAPPING_OFF:
462		default:
463			break;
464	}
465	view->SetDrawingMode(B_OP_BLEND);
466	for (int32 i = 1; i <= gridLines; i++) {
467		BPoint cross(i * scale, i * scale);
468		ConvertFromCanvas(&cross);
469		view->StrokeLine(BPoint(canvas.left, cross.y),
470						 BPoint(canvas.right, cross.y));
471		view->StrokeLine(BPoint(cross.x, canvas.top),
472						 BPoint(cross.x, canvas.bottom));
473	}
474	view->SetDrawingMode(B_OP_COPY);
475
476	// outside icon
477	BRegion outside(Bounds() & updateRect);
478	outside.Exclude(canvas);
479	view->FillRegion(&outside, kStripes);
480
481	StateView::Draw(view, updateRect);
482}
483
484
485void
486CanvasView::_MakeBackground()
487{
488	uint8* row = (uint8*)fBackground->Bits();
489	uint32 bpr = fBackground->BytesPerRow();
490	uint32 width = fBackground->Bounds().IntegerWidth() + 1;
491	uint32 height = fBackground->Bounds().IntegerHeight() + 1;
492
493	const GammaTable& lut = fRenderer->GammaTable();
494	uint8 redLow = lut.dir(kAlphaLow.red);
495	uint8 greenLow = lut.dir(kAlphaLow.blue);
496	uint8 blueLow = lut.dir(kAlphaLow.green);
497	uint8 redHigh = lut.dir(kAlphaHigh.red);
498	uint8 greenHigh = lut.dir(kAlphaHigh.blue);
499	uint8 blueHigh = lut.dir(kAlphaHigh.green);
500
501	for (uint32 y = 0; y < height; y++) {
502		uint8* p = row;
503		for (uint32 x = 0; x < width; x++) {
504			p[3] = 255;
505			if (x % 8 >= 4) {
506				if (y % 8 >= 4) {
507					p[0] = blueLow;
508					p[1] = greenLow;
509					p[2] = redLow;
510				} else {
511					p[0] = blueHigh;
512					p[1] = greenHigh;
513					p[2] = redHigh;
514				}
515			} else {
516				if (y % 8 >= 4) {
517					p[0] = blueHigh;
518					p[1] = greenHigh;
519					p[2] = redHigh;
520				} else {
521					p[0] = blueLow;
522					p[1] = greenLow;
523					p[2] = redLow;
524				}
525			}
526			p += 4;
527		}
528		row += bpr;
529	}
530}
531
532
533void
534CanvasView::_UpdateToolCursor()
535{
536	if (fIcon) {
537		if (fScrollTracking || fSpaceHeldDown) {
538			// indicate scrolling mode
539			const uchar* cursorData = fScrollTracking ? kGrabCursor : kHandCursor;
540			BCursor cursor(cursorData);
541			SetViewCursor(&cursor, true);
542		} else {
543			// pass on to current state of StateView
544			UpdateStateCursor();
545		}
546	} else {
547		BCursor cursor(kStopCursor);
548		SetViewCursor(&cursor, true);
549	}
550}
551
552
553// #pragma mark -
554
555
556double
557CanvasView::_NextZoomInLevel(double zoom) const
558{
559	if (zoom < 1)
560		return 1;
561	if (zoom < 1.5)
562		return 1.5;
563	if (zoom < 2)
564		return 2;
565	if (zoom < 3)
566		return 3;
567	if (zoom < 4)
568		return 4;
569	if (zoom < 6)
570		return 6;
571	if (zoom < 8)
572		return 8;
573	if (zoom < 16)
574		return 16;
575	if (zoom < 32)
576		return 32;
577	return 64;
578}
579
580
581double
582CanvasView::_NextZoomOutLevel(double zoom) const
583{
584	if (zoom > 32)
585		return 32;
586	if (zoom > 16)
587		return 16;
588	if (zoom > 8)
589		return 8;
590	if (zoom > 6)
591		return 6;
592	if (zoom > 4)
593		return 4;
594	if (zoom > 3)
595		return 3;
596	if (zoom > 2)
597		return 2;
598	if (zoom > 1.5)
599		return 1.5;
600	return 1;
601}
602
603
604void
605CanvasView::_SetZoom(double zoomLevel, bool mouseIsAnchor)
606{
607	if (fZoomLevel == zoomLevel)
608		return;
609
610	BPoint anchor;
611	if (mouseIsAnchor) {
612		// zoom into mouse position
613		anchor = MouseInfo()->position;
614	} else {
615		// zoom into center of view
616		BRect bounds(Bounds());
617		anchor.x = (bounds.left + bounds.right + 1) / 2.0;
618		anchor.y = (bounds.top + bounds.bottom + 1) / 2.0;
619	}
620
621	BPoint canvasAnchor = anchor;
622	ConvertToCanvas(&canvasAnchor);
623
624	fZoomLevel = zoomLevel;
625	BRect dataRect = _LayoutCanvas();
626
627	ConvertFromCanvas(&canvasAnchor);
628
629	BPoint offset = ScrollOffset();
630	offset.x = roundf(offset.x + canvasAnchor.x - anchor.x);
631	offset.y = roundf(offset.y + canvasAnchor.y - anchor.y);
632
633	Invalidate();
634
635	SetDataRectAndScrollOffset(dataRect, offset);
636}
637
638
639BRect
640CanvasView::_LayoutCanvas()
641{
642	// size of zoomed bitmap
643	BRect r(_CanvasRect());
644	r.OffsetTo(B_ORIGIN);
645
646	// ask current view state to extend size
647	// TODO: Ask StateViewState to extend bounds...
648	BRect stateBounds = r; //ViewStateBounds();
649
650	// resize for empty area around bitmap
651	// (the size we want, but might still be much smaller than view)
652	r.InsetBy(-50, -50);
653
654	// center data rect in bounds
655	BRect bounds(Bounds());
656	if (bounds.Width() > r.Width())
657		r.InsetBy(-ceilf((bounds.Width() - r.Width()) / 2), 0);
658	if (bounds.Height() > r.Height())
659		r.InsetBy(0, -ceilf((bounds.Height() - r.Height()) / 2));
660
661	if (stateBounds.IsValid()) {
662		stateBounds.InsetBy(-20, -20);
663		r = r | stateBounds;
664	}
665
666	return r;
667}
668
669