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