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