1/*
2    PPDocument_Selection.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 "PPDocument.h"
26
27#import "PPDocument_Notifications.h"
28#import "NSBitmapImageRep_PPUtilities.h"
29#import "NSColor_PPUtilities.h"
30#import "NSBezierPath_PPUtilities.h"
31#import "PPGeometry.h"
32
33
34@interface PPDocument (SelectionPrivateMethods)
35
36- (void) updateSelectionMaskWithBitmap: (NSBitmapImageRep *) bitmap atPoint: (NSPoint) origin;
37- (void) updateSelectionMaskWithTIFFData: (NSData *) tiffData atPoint: (NSPoint) origin;
38- (void) selectPath: (NSBezierPath *) path
39            selectionMode: (PPSelectionMode) selectionMode
40            shouldAntialias: (bool) shouldAntialias;
41- (bool) validateSelectionMode: (PPSelectionMode *) inOutSelectionMode;
42- (bool) selectionMaskIsNotEmpty;
43- (void) handleSelectionMaskUpdateInBounds: (NSRect) bounds
44            undoBitmap: (NSBitmapImageRep *) undoBitmap;
45
46- (NSString *) actionNameForSelectionMode: (PPSelectionMode) selectionMode;
47
48@end
49
50@implementation PPDocument (Selection)
51
52- (bool) setupSelectionMaskBitmapOfSize: (NSSize) maskSize
53{
54    if (PPGeometry_IsZeroSize(maskSize))
55    {
56        goto ERROR;
57    }
58
59    if (!_selectionMask
60        || !NSEqualSizes([_selectionMask ppSizeInPixels], maskSize))
61    {
62        NSBitmapImageRep *selectionMask = [NSBitmapImageRep ppMaskBitmapOfSize: maskSize];
63
64        if (!selectionMask)
65            goto ERROR;
66
67        [_selectionMask autorelease];   // use autorelease when releasing accessible members
68        _selectionMask = [selectionMask retain];
69    }
70    else
71    {
72        [_selectionMask ppClearBitmap];
73    }
74
75    _selectionBounds = NSZeroRect;
76    _hasSelection = NO;
77
78    return YES;
79
80ERROR:
81    return NO;
82}
83
84- (bool) hasSelection
85{
86    return _hasSelection;
87}
88
89- (NSRect) selectionBounds
90{
91    return _selectionBounds;
92}
93
94- (NSBitmapImageRep *) selectionMask
95{
96    return _selectionMask;
97}
98
99- (void) setSelectionMask: (NSBitmapImageRep *) selectionMask
100{
101    if (![selectionMask ppIsMaskBitmap]
102        || !NSEqualSizes([_selectionMask ppSizeInPixels], [selectionMask ppSizeInPixels]))
103    {
104        return;
105    }
106
107    [self updateSelectionMaskWithBitmap: selectionMask atPoint: NSZeroPoint];
108}
109
110- (void) setSelectionMaskAreaWithBitmap: (NSBitmapImageRep *) selectionMask
111                                atPoint: (NSPoint) origin
112{
113    if (![selectionMask ppIsMaskBitmap])
114    {
115        return;
116    }
117
118    [self updateSelectionMaskWithBitmap: selectionMask atPoint: origin];
119}
120
121- (void) selectRect: (NSRect) rect
122            selectionMode: (PPSelectionMode) selectionMode
123{
124    rect = PPGeometry_PixelCenteredRect(rect);
125
126    [self selectPath: [NSBezierPath bezierPathWithRect: rect]
127            selectionMode: selectionMode
128            shouldAntialias: NO];
129}
130
131- (void) selectPath: (NSBezierPath *) path
132            selectionMode: (PPSelectionMode) selectionMode
133{
134    [self selectPath: path
135            selectionMode: selectionMode
136            shouldAntialias: YES];
137}
138
139- (void) selectPixelsMatchingColorAtPoint: (NSPoint) point
140            colorMatchTolerance: (unsigned) colorMatchTolerance
141            pixelMatchingMode: (PPPixelMatchingMode) pixelMatchingMode
142            selectionMode: (PPSelectionMode) selectionMode
143{
144    NSBitmapImageRep *matchMask, *croppedMatchMask, *croppedSelectionMask;
145    NSRect matchMaskBounds;
146    bool matchMaskShouldIntersectSelectionMask;
147
148    if (![self validateSelectionMode: &selectionMode])
149    {
150        goto ERROR;
151    }
152
153    if ((selectionMode == kPPSelectionMode_Intersect)
154            || (selectionMode == kPPSelectionMode_Subtract))
155    {
156        matchMaskShouldIntersectSelectionMask =
157                    (_hasSelection && [_selectionMask ppMaskCoversPoint: point]) ? YES : NO;
158    }
159    else
160    {
161        matchMaskShouldIntersectSelectionMask = NO;
162    }
163
164    matchMask = [self maskForPixelsMatchingColorAtPoint: point
165                        colorMatchTolerance: colorMatchTolerance
166                        pixelMatchingMode: pixelMatchingMode
167                        shouldIntersectSelectionMask: matchMaskShouldIntersectSelectionMask];
168
169    if (!matchMask)
170        goto ERROR;
171
172    matchMaskBounds = [matchMask ppMaskBounds];
173
174    if (NSIsEmptyRect(matchMaskBounds))
175    {
176        goto ERROR;
177    }
178
179    if (selectionMode == kPPSelectionMode_Intersect)
180    {
181        if (_hasSelection && !matchMaskShouldIntersectSelectionMask)
182        {
183            // matchMask wasn't intersected with the selection mask during construction by
184            // the maskForPixelsMatchingColorAtPoint:... method, so intersect it manually here
185            [matchMask ppIntersectMaskWithMaskBitmap: _selectionMask];
186        }
187
188        selectionMode = kPPSelectionMode_Replace;
189    }
190
191    if (selectionMode == kPPSelectionMode_Replace)
192    {
193        if (_hasSelection)
194        {
195            matchMaskBounds = NSUnionRect(matchMaskBounds, _selectionBounds);
196        }
197    }
198
199    croppedMatchMask = [matchMask ppShallowDuplicateFromBounds: matchMaskBounds];
200
201    if (!croppedMatchMask)
202        goto ERROR;
203
204    if (selectionMode == kPPSelectionMode_Subtract)
205    {
206        croppedSelectionMask = [_selectionMask ppBitmapCroppedToBounds: matchMaskBounds];
207
208        if (!croppedSelectionMask)
209            goto ERROR;
210
211        [croppedSelectionMask ppSubtractMaskBitmap: croppedMatchMask];
212
213        croppedMatchMask = croppedSelectionMask;
214    }
215    else if (selectionMode == kPPSelectionMode_Add)
216    {
217        croppedSelectionMask = [_selectionMask ppShallowDuplicateFromBounds: matchMaskBounds];
218
219        if (!croppedSelectionMask)
220            goto ERROR;
221
222        [croppedMatchMask ppMergeMaskWithMaskBitmap: croppedSelectionMask];
223    }
224
225    [self updateSelectionMaskWithBitmap: croppedMatchMask
226            atPoint: matchMaskBounds.origin];
227
228    [[self undoManager] setActionName: [self actionNameForSelectionMode: selectionMode]];
229
230    return;
231
232ERROR:
233    return;
234}
235
236- (void) selectAll
237{
238    [self selectRect: _canvasFrame
239            selectionMode: kPPSelectionMode_Add];
240
241    [[self undoManager] setActionName: @"Select All"];
242}
243
244- (void) selectVisibleTargetPixels
245{
246    NSBitmapImageRep *visiblePixelsMask =
247                        [[self sourceBitmapForLayerOperationTarget: _layerOperationTarget]
248                                                    ppMaskBitmapForVisiblePixelsInImageBitmap];
249
250    if (!visiblePixelsMask)
251        return;
252
253    [self setSelectionMask: visiblePixelsMask];
254
255    [[self undoManager] setActionName: @"Select Visible Pixels"];
256}
257
258- (void) deselectAll
259{
260    if (!_hasSelection)
261        return;
262
263    [self selectRect: _selectionBounds
264            selectionMode: kPPSelectionMode_Subtract];
265
266    [[self undoManager] setActionName: @"Deselect All"];
267}
268
269- (void) deselectInvisibleTargetPixels
270{
271    NSBitmapImageRep *workingMask, *croppedSelectionMask;
272
273    if (!_hasSelection)
274        return;
275
276    // crop target bitmap to selection bounds & make a mask of its visible pixels
277    workingMask =
278        [[[self sourceBitmapForLayerOperationTarget: _layerOperationTarget]
279                                        ppShallowDuplicateFromBounds: _selectionBounds]
280                                                ppMaskBitmapForVisiblePixelsInImageBitmap];
281
282    if (!workingMask)
283        goto ERROR;
284
285    croppedSelectionMask = [_selectionMask ppShallowDuplicateFromBounds: _selectionBounds];
286
287    if (!croppedSelectionMask)
288        goto ERROR;
289
290    [workingMask ppIntersectMaskWithMaskBitmap: croppedSelectionMask];
291
292    if ([workingMask ppIsEqualToBitmap: croppedSelectionMask])
293    {
294        return;
295    }
296
297    [self setSelectionMaskAreaWithBitmap: workingMask atPoint: _selectionBounds.origin];
298
299    [[self undoManager] setActionName: @"Deselect Invisible Pixels"];
300
301    return;
302
303ERROR:
304    return;
305}
306
307- (void) invertSelection
308{
309    NSUndoManager *undoManager = [self undoManager];
310
311    [undoManager disableUndoRegistration];
312
313    if (!_hasSelection)
314    {
315        [self selectAll];
316    }
317    else
318    {
319        NSBitmapImageRep *invertedSelectionMask = [[_selectionMask copy] autorelease];
320
321        [invertedSelectionMask ppInvertMaskBitmap];
322
323        if (!invertedSelectionMask)
324            goto ERROR;
325
326        if ([invertedSelectionMask ppMaskIsNotEmpty])
327        {
328            [self setSelectionMask: invertedSelectionMask];
329        }
330        else
331        {
332            [self deselectAll];
333        }
334    }
335
336    [undoManager enableUndoRegistration];
337
338    [[undoManager prepareWithInvocationTarget: self] invertSelection];
339
340    if (![undoManager isUndoing] && ![undoManager isRedoing])
341    {
342        [undoManager setActionName: @"Invert Selection"];
343    }
344
345    return;
346
347ERROR:
348    [undoManager enableUndoRegistration];
349
350    return;
351}
352
353- (void) closeHolesInSelection
354{
355    NSBitmapImageRep *updatedMask;
356
357    if (!_hasSelection)
358        goto ERROR;
359
360    updatedMask = [_selectionMask ppBitmapCroppedToBounds: _selectionBounds];
361
362    if (!updatedMask)
363        goto ERROR;
364
365    [updatedMask ppCloseHolesInMaskBitmap];
366
367    [self setSelectionMaskAreaWithBitmap: updatedMask atPoint: _selectionBounds.origin];
368
369    [[self undoManager] setActionName: @"Close Holes in Selection"];
370
371    return;
372
373ERROR:
374    return;
375}
376
377- (PPDocument *) ppDocumentFromSelection
378{
379    PPDocument *ppDocument;
380
381    if (!_hasSelection || ![self layerOperationTargetHasEnabledLayer])
382    {
383        goto ERROR;
384    }
385
386    ppDocument = [[[PPDocument alloc] init] autorelease];
387
388    if (!ppDocument)
389        goto ERROR;
390
391    [ppDocument loadFromPPDocument: self];
392    [ppDocument cropToSelectionBounds];
393    [ppDocument removeNontargetLayers];
394
395    [[ppDocument undoManager] removeAllActions];
396
397    return ppDocument;
398
399ERROR:
400    return nil;
401}
402
403#pragma mark Private methods
404
405- (void) updateSelectionMaskWithBitmap: (NSBitmapImageRep *) bitmap atPoint: (NSPoint) origin
406{
407    NSRect updateRect;
408    NSBitmapImageRep *undoBitmap;
409
410    updateRect.origin = origin;
411    updateRect.size = [bitmap ppSizeInPixels];
412    updateRect = NSIntersectionRect(updateRect, _canvasFrame);
413
414    if (NSIsEmptyRect(updateRect))
415    {
416        return;
417    }
418
419    undoBitmap = [_selectionMask ppBitmapCroppedToBounds: updateRect];
420
421    [_selectionMask ppCopyFromBitmap: bitmap toPoint: origin];
422
423    [self handleSelectionMaskUpdateInBounds: updateRect undoBitmap: undoBitmap];
424}
425
426- (void) updateSelectionMaskWithTIFFData: (NSData *) tiffData atPoint: (NSPoint) origin
427{
428    if (!tiffData)
429        return;
430
431    [self updateSelectionMaskWithBitmap: [NSBitmapImageRep imageRepWithData: tiffData]
432            atPoint: origin];
433}
434
435- (void) selectPath: (NSBezierPath *) path
436            selectionMode: (PPSelectionMode) selectionMode
437            shouldAntialias: (bool) shouldAntialias
438{
439    NSRect pathBounds, updateBounds;
440    NSBitmapImageRep *undoBitmap;
441
442    if (![self validateSelectionMode: &selectionMode])
443    {
444        goto ERROR;
445    }
446
447    pathBounds = NSIntersectionRect(PPGeometry_PixelBoundsCoveredByRect([path bounds]),
448                                    _canvasFrame);
449
450    if (NSIsEmptyRect(pathBounds))
451    {
452        goto ERROR;
453    }
454
455    if (((selectionMode == kPPSelectionMode_Replace) && _hasSelection)
456        || (selectionMode == kPPSelectionMode_Intersect))
457    {
458        updateBounds = NSUnionRect(pathBounds, _selectionBounds);
459
460        undoBitmap = [_selectionMask ppBitmapCroppedToBounds: updateBounds];
461
462        [_selectionMask ppClearBitmapInBounds: updateBounds];
463    }
464    else
465    {
466        updateBounds = pathBounds;
467
468        undoBitmap = [_selectionMask ppBitmapCroppedToBounds: updateBounds];
469    }
470
471    [_selectionMask ppSetAsCurrentGraphicsContext];
472
473    if (selectionMode == kPPSelectionMode_Subtract)
474    {
475        [[NSColor ppMaskBitmapOffColor] set];
476    }
477    else
478    {
479        [[NSColor ppMaskBitmapOnColor] set];
480    }
481
482    if (shouldAntialias)
483    {
484        // antialiasing is necessary when filling a non-rectangular path, otherwise the fill
485        // will cover a larger area than the stroke (some curve edges will add a pixel);
486        // make sure to correct the antialiasing afterwards by thresholding the mask's pixel
487        // values to 0 & 255
488
489        [path ppAntialiasedFill];
490    }
491    else
492    {
493        [path fill];
494    }
495
496    [path stroke];
497
498    [_selectionMask ppRestoreGraphicsContext];
499
500    if (shouldAntialias)
501    {
502        [_selectionMask ppThresholdMaskBitmapPixelValuesInBounds: pathBounds];
503    }
504
505    if (selectionMode == kPPSelectionMode_Intersect)
506    {
507        NSBitmapImageRep *croppedSelectionMask;
508
509        croppedSelectionMask = [_selectionMask ppShallowDuplicateFromBounds: updateBounds];
510
511        // overwriting (intersecting) croppedSelectionMask also overwrites _selectionMask,
512        // since they share bitmapData (ShallowDuplicate)
513        [croppedSelectionMask ppIntersectMaskWithMaskBitmap: undoBitmap];
514    }
515
516    [self handleSelectionMaskUpdateInBounds: updateBounds
517                            undoBitmap: undoBitmap];
518
519    [[self undoManager] setActionName: [self actionNameForSelectionMode: selectionMode]];
520
521    return;
522
523ERROR:
524    return;
525}
526
527- (bool) validateSelectionMode: (PPSelectionMode *) inOutSelectionMode
528{
529    PPSelectionMode selectionMode;
530
531    if (!inOutSelectionMode)
532        goto ERROR;
533
534    selectionMode = *inOutSelectionMode;
535
536    if (!PPSelectionMode_IsValid(selectionMode))
537    {
538        goto ERROR;
539    }
540
541    if (!_hasSelection)
542    {
543        if ((selectionMode == kPPSelectionMode_Subtract)
544            || (selectionMode == kPPSelectionMode_Intersect))
545        {
546            goto ERROR;
547        }
548
549        selectionMode = kPPSelectionMode_Replace;
550    }
551
552    *inOutSelectionMode = selectionMode;
553
554    return YES;
555
556ERROR:
557    return NO;
558}
559
560- (bool) selectionMaskIsNotEmpty
561{
562    return [_selectionMask ppMaskIsNotEmpty];
563}
564
565- (void) handleSelectionMaskUpdateInBounds: (NSRect) bounds
566            undoBitmap: (NSBitmapImageRep *) undoBitmap
567{
568    NSUndoManager *undoManager;
569
570    _hasSelection = [self selectionMaskIsNotEmpty];
571
572    if (_hasSelection)
573    {
574        _selectionBounds =
575                    [_selectionMask ppMaskBoundsInRect: NSUnionRect(_selectionBounds, bounds)];
576    }
577    else
578    {
579        _selectionBounds = NSZeroRect;
580    }
581
582    [self postNotification_UpdatedSelection];
583
584    undoManager = [self undoManager];
585
586    [[undoManager prepareWithInvocationTarget: self]
587                                            updateSelectionMaskWithTIFFData:
588                                                            [undoBitmap ppCompressedTIFFData]
589                                            atPoint: bounds.origin];
590
591    if (![undoManager isUndoing] && ![undoManager isRedoing])
592    {
593        [undoManager setActionName: @"Selection"];
594    }
595}
596
597- (NSString *) actionNameForSelectionMode: (PPSelectionMode) selectionMode
598{
599    switch (selectionMode)
600    {
601        case kPPSelectionMode_Add:
602        {
603            return @"Add to Selection";
604        }
605        break;
606
607        case kPPSelectionMode_Subtract:
608        {
609            return @"Subtract from Selection";
610        }
611        break;
612
613        case kPPSelectionMode_Intersect:
614        {
615            return @"Intersect Selection";
616        }
617        break;
618
619        case kPPSelectionMode_Replace:
620        default:
621        {
622            return @"Make Selection";
623        }
624        break;
625    }
626}
627
628@end
629