1/*
2    PPCanvasView_SelectionOutline.m
3
4    Copyright 2013-2018 Josh Freeman
5    http://www.twilightedge.com
6
7    This file is part of PikoPixel for Mac OS X and GNUstep.
8    PikoPixel is a graphical application for drawing & editing pixel-art images.
9
10    PikoPixel is free software: you can redistribute it and/or modify it under
11    the terms of the GNU Affero General Public License as published by the
12    Free Software Foundation, either version 3 of the License, or (at your
13    option) any later version approved for PikoPixel by its copyright holder (or
14    an authorized proxy).
15
16    PikoPixel is distributed in the hope that it will be useful, but WITHOUT ANY
17    WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
18    FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
19    details.
20
21    You should have received a copy of the GNU Affero General Public License
22    along with this program. If not, see <http://www.gnu.org/licenses/>.
23*/
24
25#import "PPCanvasView.h"
26
27#import "NSBitmapImageRep_PPUtilities.h"
28#import "NSBezierPath_PPUtilities.h"
29#import "PPGeometry.h"
30
31
32#define kSelectionOutlinePatternImageName           @"marching_ants_pattern"
33#define kSelectionOutlineAnimationTimerInterval     0.15f
34#define kSelectionOutlineAnimationPhaseInterval     1.0f
35
36
37static NSColor *gSelectionOutlinePatternColor = nil;
38static float gSelectionOutlinePatternWidth = 0.0f;
39
40
41@interface PPCanvasView (SelectionOutlinePrivateMethods)
42
43- (void) setupSelectionOutlineAnimationTimerForCurrentState;
44- (void) startSelectionOutlineAnimationTimer;
45- (void) stopSelectionOutlineAnimationTimer;
46- (void) selectionOutlineAnimationTimerDidFire: (NSTimer *) theTimer;
47
48- (void) setupSelectionOutlinePathsFromSelectionMask: (NSBitmapImageRep *) selectionMask
49            maskBounds: (NSRect) maskBounds;
50- (void) clearSelectionOutlinePaths;
51
52- (void) setupZoomedSelectionOutlinePath;
53- (void) clearZoomedSelectionOutlinePath;
54
55@end
56
57@implementation PPCanvasView (SelectionOutline)
58
59+ (void) initializeSelectionOutline
60{
61    NSImage *selectionOutlinePatternImage;
62
63    selectionOutlinePatternImage = [NSImage imageNamed: kSelectionOutlinePatternImageName];
64
65    gSelectionOutlinePatternColor =
66                        [[NSColor colorWithPatternImage: selectionOutlinePatternImage] retain];
67
68    gSelectionOutlinePatternWidth = [selectionOutlinePatternImage size].width;
69}
70
71- (bool) initSelectionOutlineMembers
72{
73    return YES;
74}
75
76- (void) deallocSelectionOutlineMembers
77{
78    [self stopSelectionOutlineAnimationTimer];
79
80    [self clearSelectionOutlinePaths];
81}
82
83- (void) setSelectionOutlineToMask: (NSBitmapImageRep *) selectionMask
84            maskBounds: (NSRect) maskBounds
85{
86    NSRect updateBounds = _zoomedSelectionOutlineDisplayBounds;
87
88    [self setupSelectionOutlinePathsFromSelectionMask: selectionMask
89            maskBounds: maskBounds];
90
91    updateBounds = NSUnionRect(updateBounds, _zoomedSelectionOutlineDisplayBounds);
92
93    [self setupSelectionOutlineAnimationTimerForCurrentState];
94
95    [self setNeedsDisplayInRect: updateBounds];
96}
97
98- (void) setShouldHideSelectionOutline: (bool) shouldHideSelectionOutline
99{
100    shouldHideSelectionOutline = (shouldHideSelectionOutline) ? YES : NO;
101
102    if (shouldHideSelectionOutline == _shouldHideSelectionOutline)
103    {
104        return;
105    }
106
107    _shouldHideSelectionOutline = shouldHideSelectionOutline;
108
109    [self setNeedsDisplayInRect: _zoomedSelectionOutlineDisplayBounds];
110}
111
112- (void) setShouldAnimateSelectionOutline: (bool) shouldAnimateSelectionOutline
113{
114    _shouldAnimateSelectionOutline = (shouldAnimateSelectionOutline) ? YES : NO;
115
116    [self setupSelectionOutlineAnimationTimerForCurrentState];
117}
118
119- (void) updateSelectionOutlineForCurrentVisibleCanvas
120{
121    [self setupZoomedSelectionOutlinePath];
122}
123
124- (void) drawSelectionOutline
125{
126    NSGraphicsContext *graphicsContext;
127
128    if (!_hasSelectionOutline || _shouldHideSelectionOutline)
129    {
130        return;
131    }
132
133    // the current implementation of the selection outline path allows the path to extend
134    // one pixel beyond the right & bottom edges of the visible canvas; as a workaround,
135    // set the clipping path to prevent drawing outside the canvas
136
137    [NSGraphicsContext saveGraphicsState];
138    [NSBezierPath clipRect: _offsetZoomedVisibleCanvasBounds];
139
140    [gSelectionOutlinePatternColor set];
141
142    graphicsContext = [NSGraphicsContext currentContext];
143
144    [graphicsContext setPatternPhase: _selectionOutlineTopRightAnimationPhase];
145    [_zoomedSelectionOutlineTopRightPath stroke];
146
147    [graphicsContext setPatternPhase: _selectionOutlineBottomLeftAnimationPhase];
148    [_zoomedSelectionOutlineBottomLeftPath stroke];
149
150    [NSGraphicsContext restoreGraphicsState];
151}
152
153#pragma mark Marching ants timer (animated selection outline)
154
155- (void) setupSelectionOutlineAnimationTimerForCurrentState
156{
157    bool shouldEnableSelectionOutlineAnimationTimer =
158                        (_hasSelectionOutline && _shouldAnimateSelectionOutline) ? YES : NO;
159
160    if (shouldEnableSelectionOutlineAnimationTimer)
161    {
162        if (!_selectionOutlineAnimationTimer)
163        {
164            [self startSelectionOutlineAnimationTimer];
165        }
166    }
167    else
168    {
169        if (_selectionOutlineAnimationTimer)
170        {
171            [self stopSelectionOutlineAnimationTimer];
172        }
173    }
174}
175
176- (void) startSelectionOutlineAnimationTimer
177{
178    if (_selectionOutlineAnimationTimer)
179        return;
180
181    _selectionOutlineAnimationTimer =
182            [[NSTimer scheduledTimerWithTimeInterval: kSelectionOutlineAnimationTimerInterval
183                        target: self
184                        selector: @selector(selectionOutlineAnimationTimerDidFire:)
185                        userInfo: nil
186                        repeats: YES]
187                    retain];
188}
189
190- (void) stopSelectionOutlineAnimationTimer
191{
192    if (!_selectionOutlineAnimationTimer)
193        return;
194
195    [_selectionOutlineAnimationTimer invalidate];
196    [_selectionOutlineAnimationTimer release];
197    _selectionOutlineAnimationTimer = nil;
198}
199
200- (void) selectionOutlineAnimationTimerDidFire: (NSTimer *) theTimer
201{
202    if (!_hasSelectionOutline || _shouldHideSelectionOutline
203        || !_shouldAnimateSelectionOutline)
204    {
205        return;
206    }
207
208    _selectionOutlineTopRightAnimationPhase.x += kSelectionOutlineAnimationPhaseInterval;
209
210    if (_selectionOutlineTopRightAnimationPhase.x >= gSelectionOutlinePatternWidth)
211    {
212        _selectionOutlineTopRightAnimationPhase.x = 0.0f;
213    }
214
215    _selectionOutlineBottomLeftAnimationPhase.x = -_selectionOutlineTopRightAnimationPhase.x;
216
217    [self setNeedsDisplayInRect: _zoomedSelectionOutlineDisplayBounds];
218}
219
220#pragma mark Private methods
221
222- (void) setupSelectionOutlinePathsFromSelectionMask: (NSBitmapImageRep *) selectionMask
223            maskBounds: (NSRect) maskBounds
224{
225    NSBezierPath *selectionOutlineTopRightPath, *selectionOutlineBottomLeftPath,
226                    *selectionOutlinePath;
227    NSRect selectionOutlinePathBounds;
228
229    [self clearSelectionOutlinePaths];
230
231    if (![selectionMask ppIsMaskBitmap])
232    {
233        return;
234    }
235
236    selectionOutlineTopRightPath = [NSBezierPath bezierPath];
237    selectionOutlineBottomLeftPath = [NSBezierPath bezierPath];
238
239    [NSBezierPath ppAppendOutlinePathsForMaskBitmap: selectionMask
240                    inBounds: maskBounds
241                    toTopRightBezierPath: selectionOutlineTopRightPath
242                    andBottomLeftBezierPath: selectionOutlineBottomLeftPath];
243
244    if ([selectionOutlineTopRightPath isEmpty])
245    {
246        return;
247    }
248
249    _selectionOutlineTopRightPath = [selectionOutlineTopRightPath retain];
250    _selectionOutlineBottomLeftPath = [selectionOutlineBottomLeftPath retain];
251
252    _hasSelectionOutline = YES;
253
254    // edge paths
255
256    selectionOutlinePathBounds = [_selectionOutlineTopRightPath bounds];
257
258    // right edge
259    if (NSMaxX(selectionOutlinePathBounds) >= [selectionMask pixelsWide])
260    {
261        selectionOutlinePath = [NSBezierPath bezierPath];
262
263        [selectionOutlinePath ppAppendRightEdgePathForMaskBitmap: selectionMask];
264
265        if (![selectionOutlinePath isEmpty])
266        {
267            _selectionOutlineRightEdgePath = [selectionOutlinePath retain];
268        }
269    }
270
271    // bottom edge
272    if (selectionOutlinePathBounds.origin.y < 1.0f)
273    {
274        selectionOutlinePath = [NSBezierPath bezierPath];
275
276        [selectionOutlinePath ppAppendBottomEdgePathForMaskBitmap: selectionMask];
277
278        if (![selectionOutlinePath isEmpty])
279        {
280            _selectionOutlineBottomEdgePath = [selectionOutlinePath retain];
281        }
282    }
283
284    [self setupZoomedSelectionOutlinePath];
285}
286
287- (void) clearSelectionOutlinePaths
288{
289    if (_selectionOutlineTopRightPath)
290    {
291        [_selectionOutlineTopRightPath release];
292        _selectionOutlineTopRightPath = nil;
293    }
294
295    if (_selectionOutlineBottomLeftPath)
296    {
297        [_selectionOutlineBottomLeftPath release];
298        _selectionOutlineBottomLeftPath = nil;
299    }
300
301    if (_selectionOutlineRightEdgePath)
302    {
303        [_selectionOutlineRightEdgePath release];
304        _selectionOutlineRightEdgePath = nil;
305    }
306
307    if (_selectionOutlineBottomEdgePath)
308    {
309        [_selectionOutlineBottomEdgePath release];
310        _selectionOutlineBottomEdgePath = nil;
311    }
312
313    [self clearZoomedSelectionOutlinePath];
314
315    _hasSelectionOutline = NO;
316}
317
318- (void) setupZoomedSelectionOutlinePath
319{
320    NSBezierPath *zoomedSelectionOutlineTopRightPath, *zoomedSelectionOutlineBottomLeftPath,
321                    *zoomedSelectionOutlineEdgePath;
322    NSAffineTransform *transform;
323
324    [self clearZoomedSelectionOutlinePath];
325
326    if (!_hasSelectionOutline)
327        return;
328
329    transform = [NSAffineTransform transform];
330    zoomedSelectionOutlineTopRightPath = [[_selectionOutlineTopRightPath copy] autorelease];
331    zoomedSelectionOutlineBottomLeftPath = [[_selectionOutlineBottomLeftPath copy] autorelease];
332
333    if (!transform || !zoomedSelectionOutlineTopRightPath
334        || !zoomedSelectionOutlineBottomLeftPath)
335    {
336        return;
337    }
338
339    [transform translateXBy: _canvasDrawingOffset.x + 0.5f
340                        yBy: _canvasDrawingOffset.y - 0.5f];
341
342    [transform scaleBy: _zoomFactor];
343
344    [zoomedSelectionOutlineTopRightPath transformUsingAffineTransform: transform];
345    [zoomedSelectionOutlineBottomLeftPath transformUsingAffineTransform: transform];
346
347    if (_selectionOutlineRightEdgePath)
348    {
349        transform = [NSAffineTransform transform];
350        zoomedSelectionOutlineEdgePath = [[_selectionOutlineRightEdgePath copy] autorelease];
351
352        if (transform && zoomedSelectionOutlineEdgePath)
353        {
354            [transform translateXBy: _canvasDrawingOffset.x - 0.5f
355                                yBy: _canvasDrawingOffset.y - 0.5f];
356
357            [transform scaleBy: _zoomFactor];
358
359            [zoomedSelectionOutlineEdgePath transformUsingAffineTransform: transform];
360
361            [zoomedSelectionOutlineTopRightPath appendBezierPath:
362                                                            zoomedSelectionOutlineEdgePath];
363        }
364    }
365
366    if (_selectionOutlineBottomEdgePath)
367    {
368        transform = [NSAffineTransform transform];
369        zoomedSelectionOutlineEdgePath = [[_selectionOutlineBottomEdgePath copy] autorelease];
370
371        if (transform && zoomedSelectionOutlineEdgePath)
372        {
373            [transform translateXBy: _canvasDrawingOffset.x + 0.5f
374                                yBy: _canvasDrawingOffset.y + 0.5f];
375
376            [transform scaleBy: _zoomFactor];
377
378            [zoomedSelectionOutlineEdgePath transformUsingAffineTransform: transform];
379
380            [zoomedSelectionOutlineBottomLeftPath appendBezierPath:
381                                                            zoomedSelectionOutlineEdgePath];
382        }
383    }
384
385    _zoomedSelectionOutlineTopRightPath = [zoomedSelectionOutlineTopRightPath retain];
386    _zoomedSelectionOutlineBottomLeftPath = [zoomedSelectionOutlineBottomLeftPath retain];
387
388    _zoomedSelectionOutlineDisplayBounds =
389            PPGeometry_PixelBoundsCoveredByRect([zoomedSelectionOutlineTopRightPath bounds]);
390}
391
392- (void) clearZoomedSelectionOutlinePath
393{
394    if (_zoomedSelectionOutlineTopRightPath)
395    {
396        [_zoomedSelectionOutlineTopRightPath release];
397        _zoomedSelectionOutlineTopRightPath = nil;
398    }
399
400    if (_zoomedSelectionOutlineBottomLeftPath)
401    {
402        [_zoomedSelectionOutlineBottomLeftPath release];
403        _zoomedSelectionOutlineBottomLeftPath = nil;
404    }
405
406    _zoomedSelectionOutlineDisplayBounds = NSZeroRect;
407}
408
409@end
410