1/*
2    PPSamplerImageView.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 "PPSamplerImageView.h"
26
27#import "PPGeometry.h"
28#import "PPDocumentSamplerImage.h"
29#import "NSCursor_PPUtilities.h"
30#import "NSObject_PPUtilities.h"
31#import "PPCanvasView.h"
32#import "PPCursorManager.h"
33#import "NSColor_PPUtilities.h"
34#import "NSBitmapImageRep_PPUtilities.h"
35
36
37#define kMinAllowedValueForMinViewDimension         30
38#define kMaxAllowedValueForMaxViewDimension         1000
39
40
41#define kUIColor_SamplerBackgroundPattern                                               \
42            [NSColor ppDiagonalCheckerboardPatternColorWithBoxDimension: 2.0f           \
43                        color1: [NSColor ppSRGBColorWithWhite: 0.42f alpha: 1.0f]       \
44                        color2: [NSColor ppSRGBColorWithWhite: 0.53f alpha: 1.0f]]
45
46
47static NSColor *gSamplerBackgroundColor = nil;
48
49
50@interface PPSamplerImageView (PrivateMethods)
51
52- (void) addAsObserverForPPSamplerImageViewNotifications;
53- (void) removeAsObserverForPPSamplerImageViewNotifications;
54- (void) handlePPSamplerImageViewNotification_FrameDidChange: (NSNotification *) notification;
55
56- (void) resetImageBoundsTrackingRect;
57
58- (void) updateCursor;
59
60- (float) defaultScaleForCurrentImage;
61
62- (void) setupMinAndMaxScalesForImageFrame;
63
64- (void) setupDrawBoundsAndTrackingForImageAndViewFrames;
65
66- (void) setSamplerImageScaleWithImageDrawBoundsAndFrame;
67
68- (void) beginSamplingImageAtWindowPoint: (NSPoint) locationInWindow;
69- (void) continueSamplingImageAtWindowPoint: (NSPoint) locationInWindow;
70- (void) finishSamplingImage;
71
72- (NSColor *) colorAtWindowPoint: (NSPoint) windowLocation;
73
74- (void) setLastSampledColor: (NSColor *) color;
75
76- (void) notifyDelegateDidBrowseColor: (NSColor *) color;
77- (void) notifyDelegateDidSelectColor: (NSColor *) color;
78- (void) notifyDelegateDidCancelSelection;
79
80@end
81
82@implementation PPSamplerImageView
83
84+ (void) initialize
85{
86    if ([self class] != [PPSamplerImageView class])
87    {
88        return;
89    }
90
91    gSamplerBackgroundColor = [kUIColor_SamplerBackgroundPattern retain];
92}
93
94- (id) initWithFrame: (NSRect) frameRect
95{
96    self = [super initWithFrame: frameRect];
97
98    if (!self)
99        goto ERROR;
100
101    _minViewDimension = kMinAllowedValueForMinViewDimension;
102    _maxViewDimension = kMaxAllowedValueForMaxViewDimension;
103    _defaultImageDimension = kMinAllowedValueForMinViewDimension;
104
105    [self addAsObserverForPPSamplerImageViewNotifications];
106
107    return self;
108
109ERROR:
110    [self release];
111
112    return nil;
113}
114
115- (void) dealloc
116{
117    [self removeAsObserverForPPSamplerImageViewNotifications];
118
119    [_samplerImage release];
120
121    [_lastSampledColor release];
122
123    [super dealloc];
124}
125
126- (void) setSamplerImagePanelType: (PPSamplerImagePanelType) panelType
127            minViewDimension: (float) minViewDimension
128            maxViewDimension: (float) maxViewDimension
129            defaultImageDimension: (float) defaultImageDimension
130            delegate: (id) delegate
131{
132    if (minViewDimension < kMinAllowedValueForMinViewDimension)
133    {
134        minViewDimension = kMinAllowedValueForMinViewDimension;
135    }
136
137    if (maxViewDimension > kMaxAllowedValueForMaxViewDimension)
138    {
139        maxViewDimension = kMaxAllowedValueForMaxViewDimension;
140    }
141
142    if (maxViewDimension < minViewDimension)
143    {
144        maxViewDimension = minViewDimension;
145    }
146
147    if (defaultImageDimension < minViewDimension)
148    {
149        defaultImageDimension = minViewDimension;
150    }
151    else if (defaultImageDimension > maxViewDimension)
152    {
153        defaultImageDimension = maxViewDimension;
154    }
155
156    _panelType = panelType;
157    _minViewDimension = minViewDimension;
158    _maxViewDimension = maxViewDimension;
159    _defaultImageDimension = defaultImageDimension;
160    _delegate = delegate;
161
162    if (_samplerImage)
163    {
164        [self setupMinAndMaxScalesForImageFrame];
165    }
166}
167
168- (void) setSamplerImage: (PPDocumentSamplerImage *) samplerImage
169{
170    NSSize samplerImageSize;
171
172    if (_samplerImage == samplerImage)
173    {
174        return;
175    }
176
177    [_samplerImage release];
178    _samplerImage = [samplerImage retain];
179
180    samplerImageSize = (samplerImage) ? [samplerImage size] : NSZeroSize;
181
182    _imageFrame = PPGeometry_OriginRectOfSize(samplerImageSize);
183
184    [self setupMinAndMaxScalesForImageFrame];
185
186    [self setupDrawBoundsAndTrackingForImageAndViewFrames];
187}
188
189- (NSSize) viewSizeForResizingToProposedViewSize: (NSSize) proposedViewSize
190            resizableDirectionsMask: (unsigned) resizableDirectionsMask
191{
192    float horizontalScale = 0.0f, verticalScale = 0.0f, scale;
193    NSSize viewSize;
194
195    if (NSIsEmptyRect(_imageFrame))
196    {
197        goto ERROR;
198    }
199
200    if (!(resizableDirectionsMask & kPPResizableDirectionsMask_Both))
201    {
202        goto ERROR;
203    }
204
205    if (resizableDirectionsMask & kPPResizableDirectionsMask_Horizontal)
206    {
207        if (proposedViewSize.width >= _minViewDimension)
208        {
209            horizontalScale = proposedViewSize.width / _imageFrame.size.width;
210        }
211        else
212        {
213            if (proposedViewSize.width > _minViewDimension / 2.0f)
214            {
215                horizontalScale = (2.0f * proposedViewSize.width - _minViewDimension)
216                                    / _imageFrame.size.width;
217            }
218            else
219            {
220                horizontalScale = _minScaleForCurrentImage;
221            }
222        }
223    }
224
225    if (resizableDirectionsMask & kPPResizableDirectionsMask_Vertical)
226    {
227        if (proposedViewSize.height >= _minViewDimension)
228        {
229            verticalScale = proposedViewSize.height / _imageFrame.size.height;
230        }
231        else
232        {
233            if (proposedViewSize.height > _minViewDimension / 2.0f)
234            {
235                verticalScale = (2.0f * proposedViewSize.height - _minViewDimension)
236                                    / _imageFrame.size.height;
237            }
238            else
239            {
240                verticalScale = _minScaleForCurrentImage;
241            }
242        }
243    }
244
245    scale = MAX(horizontalScale, verticalScale);
246
247    if (scale > _maxScaleForCurrentImage)
248    {
249        scale = _maxScaleForCurrentImage;
250    }
251    else if (scale < _minScaleForCurrentImage)
252    {
253        scale = _minScaleForCurrentImage;
254    }
255
256    viewSize = PPGeometry_SizeScaledByFactorAndRoundedToIntegerValues(_imageFrame.size, scale);
257
258    if (viewSize.width < _minViewDimension)
259    {
260        viewSize.width = _minViewDimension;
261    }
262
263    if (viewSize.height < _minViewDimension)
264    {
265        viewSize.height = _minViewDimension;
266    }
267
268    return viewSize;
269
270ERROR:
271    return NSZeroSize;
272}
273
274- (NSSize) viewSizeForScaledCurrentSamplerImage
275{
276    float scale;
277    NSSize viewSize;
278
279    if (!_samplerImage)
280        goto ERROR;
281
282    scale = [_samplerImage scalingFactorForSamplerImagePanelType: _panelType];
283
284    if (scale < _minScaleForCurrentImage)
285    {
286        scale = (scale > 0.0f) ? _minScaleForCurrentImage : [self defaultScaleForCurrentImage];
287    }
288    else if (scale > _maxScaleForCurrentImage)
289    {
290        scale = _maxScaleForCurrentImage;
291    }
292
293    viewSize = PPGeometry_SizeScaledByFactorAndRoundedToIntegerValues(_imageFrame.size, scale);
294
295    if (viewSize.width < _minViewDimension)
296    {
297        viewSize.width = _minViewDimension;
298    }
299
300    if (viewSize.height < _minViewDimension)
301    {
302        viewSize.height = _minViewDimension;
303    }
304
305    return viewSize;
306
307ERROR:
308    return NSMakeSize(_minViewDimension, _minViewDimension);
309}
310
311- (void) setupMouseTracking
312{
313    [self resetImageBoundsTrackingRect];
314
315    [self updateCursor];
316}
317
318- (void) disableMouseTracking: (bool) shouldDisableTracking
319{
320    if (_mouseIsSamplingImage)
321    {
322        // disallow tracking while sampling
323        shouldDisableTracking = YES;
324    }
325    else
326    {
327        shouldDisableTracking = (shouldDisableTracking) ? YES : NO;
328    }
329
330    if (_disallowMouseTracking == shouldDisableTracking)
331    {
332        return;
333    }
334
335    _disallowMouseTracking = shouldDisableTracking;
336
337    [self setupMouseTracking];
338}
339
340- (bool) mouseIsSamplingImage
341{
342    return _mouseIsSamplingImage;
343}
344
345- (void) forceStopSamplingImage
346{
347    if (_mouseIsSamplingImage)
348    {
349        [self finishSamplingImage];
350    }
351}
352
353#pragma mark NSView overrides
354
355- (BOOL) acceptsFirstMouse: (NSEvent *) theEvent
356{
357    return YES;
358}
359
360- (void) drawRect: (NSRect) rect
361{
362    [[NSGraphicsContext currentContext] setImageInterpolation: NSImageInterpolationNone];
363
364    [gSamplerBackgroundColor set];
365    NSRectFill(_imageDrawBounds);
366
367    [[_samplerImage image] drawInRect: _imageDrawBounds
368                            fromRect: _imageFrame
369                            operation: NSCompositeSourceOver
370                            fraction: 1.0f];
371}
372
373- (void) mouseDown: (NSEvent *) theEvent
374{
375    [self beginSamplingImageAtWindowPoint: [theEvent locationInWindow]];
376}
377
378- (void) mouseDragged: (NSEvent *) theEvent
379{
380    [self continueSamplingImageAtWindowPoint: [theEvent locationInWindow]];
381}
382
383- (void) mouseUp: (NSEvent *) theEvent
384{
385    [self finishSamplingImage];
386}
387
388- (void) mouseEntered: (NSEvent *) theEvent
389{
390    NSTrackingRectTag trackingRectTag = [theEvent trackingNumber];
391
392    if (trackingRectTag == _imageBoundsTrackingRectTag)
393    {
394        if (!_mouseIsInsideImageBoundsTrackingRect)
395        {
396            _mouseIsInsideImageBoundsTrackingRect = YES;
397
398            [self updateCursor];
399        }
400    }
401    else
402    {
403        [super mouseEntered: theEvent];
404    }
405}
406
407- (void) mouseExited: (NSEvent *) theEvent
408{
409    NSTrackingRectTag trackingRectTag = [theEvent trackingNumber];
410
411    if (trackingRectTag == _imageBoundsTrackingRectTag)
412    {
413        if (_mouseIsInsideImageBoundsTrackingRect)
414        {
415            _mouseIsInsideImageBoundsTrackingRect = NO;
416
417            [self updateCursor];
418        }
419    }
420    else
421    {
422        [super mouseExited: theEvent];
423    }
424}
425
426#pragma mark PPSamplerImageView notifications
427
428- (void) addAsObserverForPPSamplerImageViewNotifications
429{
430    [self setPostsFrameChangedNotifications: YES];
431
432    [[NSNotificationCenter defaultCenter]
433                            addObserver: self
434                            selector:
435                                @selector(handlePPSamplerImageViewNotification_FrameDidChange:)
436                            name: NSViewFrameDidChangeNotification
437                            object: self];
438}
439
440- (void) removeAsObserverForPPSamplerImageViewNotifications
441{
442    [[NSNotificationCenter defaultCenter] removeObserver: self
443                                            name: NSViewFrameDidChangeNotification
444                                            object: self];
445}
446
447- (void) handlePPSamplerImageViewNotification_FrameDidChange: (NSNotification *) notification
448{
449    [self setupDrawBoundsAndTrackingForImageAndViewFrames];
450
451    [self setSamplerImageScaleWithImageDrawBoundsAndFrame];
452}
453
454#pragma mark Mouse tracking
455
456- (void) resetImageBoundsTrackingRect
457{
458    NSRect newTrackingRect = NSZeroRect;
459    bool mouseIsInsideNewTrackingRect = NO;
460
461    if ([[self window] isVisible] && !_disallowMouseTracking)
462    {
463        newTrackingRect = _imageDrawBounds;
464
465        if (!NSIsEmptyRect(newTrackingRect))
466        {
467            NSPoint mouseLocationInView =
468                        [self convertPoint: [[self window] mouseLocationOutsideOfEventStream]
469                                fromView: nil];
470
471            mouseIsInsideNewTrackingRect =
472                            (NSPointInRect(mouseLocationInView, newTrackingRect)) ? YES : NO;
473        }
474    }
475
476    if (!NSEqualRects(newTrackingRect, _imageBoundsTrackingRect))
477    {
478        if (_imageBoundsTrackingRectTag)
479        {
480            [self removeTrackingRect: _imageBoundsTrackingRectTag];
481            _imageBoundsTrackingRectTag = 0;
482
483            _imageBoundsTrackingRect = NSZeroRect;
484        }
485
486        if (!NSIsEmptyRect(newTrackingRect))
487        {
488            _imageBoundsTrackingRectTag = [self addTrackingRect: newTrackingRect
489                                                owner: self
490                                                userData: NULL
491                                                assumeInside: mouseIsInsideNewTrackingRect];
492
493            if (_imageBoundsTrackingRectTag)
494            {
495                _imageBoundsTrackingRect = newTrackingRect;
496            }
497            else
498            {
499                mouseIsInsideNewTrackingRect = NO;
500            }
501        }
502    }
503
504    _mouseIsInsideImageBoundsTrackingRect = mouseIsInsideNewTrackingRect;
505}
506
507#pragma mark Cursor updates
508
509- (void) updateCursor
510{
511    NSCursor *cursor;
512    PPCursorLevel cursorLevel;
513
514    cursor = (_mouseIsInsideImageBoundsTrackingRect || _mouseIsSamplingImage) ?
515                [NSCursor ppColorSamplerToolCursor] : nil;
516
517    cursorLevel = (_panelType == kPPSamplerImagePanelType_PopupPanel) ?
518                        kPPCursorLevel_PopupPanel : kPPCursorLevel_Panel;
519
520    [[PPCursorManager sharedManager] setCursor: cursor
521                                        atLevel: cursorLevel
522                                        isDraggingMouse: _mouseIsSamplingImage];
523}
524
525#pragma mark Private methods
526
527- (float) defaultScaleForCurrentImage
528{
529    float maxImageDimension;
530
531    if (!_samplerImage)
532        goto ERROR;
533
534    maxImageDimension = MAX(_imageFrame.size.width, _imageFrame.size.height);
535
536    return _defaultImageDimension / maxImageDimension;
537
538ERROR:
539    return 1.0f;
540}
541
542- (void) setupMinAndMaxScalesForImageFrame
543{
544    float maxImageDimension;
545
546    if (NSIsEmptyRect(_imageFrame))
547    {
548        return;
549    }
550
551    maxImageDimension = MAX(_imageFrame.size.width, _imageFrame.size.height);
552
553    _maxScaleForCurrentImage = _maxViewDimension / maxImageDimension;
554    _minScaleForCurrentImage = _minViewDimension / maxImageDimension;
555}
556
557- (void) setupDrawBoundsAndTrackingForImageAndViewFrames
558{
559    _imageDrawBounds = PPGeometry_ScaledBoundsForFrameOfSizeToFitFrameOfSize(_imageFrame.size,
560                                                                            [self bounds].size);
561
562    if ([[self window] isVisible])
563    {
564        [self setupMouseTracking];
565    }
566
567    [self setNeedsDisplay: YES];
568}
569
570- (void) setSamplerImageScaleWithImageDrawBoundsAndFrame
571{
572    float scale = (_imageDrawBounds.size.width / _imageFrame.size.width
573                    + _imageDrawBounds.size.height / _imageFrame.size.height)
574                        / 2.0f;
575
576    [_samplerImage setScalingFactor: scale forSamplerImagePanelType: _panelType];
577}
578
579- (void) beginSamplingImageAtWindowPoint: (NSPoint) locationInWindow
580{
581    NSPoint locationInView;
582    NSColor *sampledColor;
583
584    locationInView = [self convertPoint: locationInWindow fromView: nil];
585
586    if (!NSPointInRect(locationInView, _imageDrawBounds))
587    {
588        return;
589    }
590
591    _mouseIsSamplingImage = YES;
592    [self disableMouseTracking: YES];
593
594    sampledColor = [self colorAtWindowPoint: locationInWindow];
595
596    if (sampledColor)
597    {
598        [self notifyDelegateDidBrowseColor: sampledColor];
599    }
600
601    [self setLastSampledColor: sampledColor];
602}
603
604- (void) continueSamplingImageAtWindowPoint: (NSPoint) locationInWindow
605{
606    NSColor *sampledColor;
607
608    if (!_mouseIsSamplingImage)
609        return;
610
611    sampledColor = [self colorAtWindowPoint: locationInWindow];
612
613    if ((sampledColor && ![_lastSampledColor isEqual: sampledColor])
614        || (!sampledColor && _lastSampledColor))
615    {
616        [self notifyDelegateDidBrowseColor: sampledColor];
617
618        [self setLastSampledColor: sampledColor];
619    }
620}
621
622- (void) finishSamplingImage
623{
624    if (!_mouseIsSamplingImage)
625        return;
626
627    _mouseIsSamplingImage = NO;
628    [self disableMouseTracking: NO];
629
630    if (_lastSampledColor)
631    {
632        [self notifyDelegateDidSelectColor: _lastSampledColor];
633    }
634    else
635    {
636        [self notifyDelegateDidCancelSelection];
637    }
638
639    [self setLastSampledColor: nil];
640}
641
642- (NSColor *) colorAtWindowPoint: (NSPoint) windowLocation
643{
644    float horizontalScale, verticalScale;
645    NSPoint viewLocation, imageLocation;
646
647    horizontalScale = _imageDrawBounds.size.width / _imageFrame.size.width;
648    verticalScale = _imageDrawBounds.size.height / _imageFrame.size.height;
649
650    viewLocation = [self convertPoint: windowLocation fromView: nil];
651
652    if (!NSPointInRect(viewLocation, _imageDrawBounds))
653    {
654        goto ERROR;
655    }
656
657    imageLocation =
658        NSMakePoint(floorf((viewLocation.x - _imageDrawBounds.origin.x) / horizontalScale),
659                    floorf((viewLocation.y - _imageDrawBounds.origin.y) / verticalScale));
660
661    if (!NSPointInRect(imageLocation, _imageFrame))
662    {
663        goto ERROR;
664    }
665
666    return [[_samplerImage bitmap] ppImageColorAtPoint: imageLocation];
667
668ERROR:
669    return nil;
670}
671
672- (void) setLastSampledColor: (NSColor *) color
673{
674    if (_lastSampledColor == color)
675    {
676        return;
677    }
678
679    [_lastSampledColor release];
680    _lastSampledColor = [color retain];
681}
682
683#pragma mark Delegate notifiers
684
685- (void) notifyDelegateDidBrowseColor: (NSColor *) color
686{
687    if ([_delegate respondsToSelector: @selector(ppSamplerImageView:didBrowseColor:)])
688    {
689        [_delegate ppSamplerImageView: self didBrowseColor: color];
690    }
691}
692
693- (void) notifyDelegateDidSelectColor: (NSColor *) color
694{
695    if (!color)
696        return;
697
698    if ([_delegate respondsToSelector: @selector(ppSamplerImageView:didSelectColor:)])
699    {
700        [_delegate ppSamplerImageView: self didSelectColor: color];
701    }
702}
703
704- (void) notifyDelegateDidCancelSelection
705{
706    if ([_delegate respondsToSelector: @selector(ppSamplerImageViewDidCancelSelection:)])
707    {
708        [_delegate ppSamplerImageViewDidCancelSelection: self];
709    }
710}
711
712@end
713