Source/BoardView.m
author Jens Alfke <jens@mooseyard.com>
Wed Mar 12 15:51:32 2008 -0700 (2008-03-12)
changeset 6 af9b2b929b03
parent 3 40d225cf9c43
child 9 a59acc683080
permissions -rw-r--r--
Fixed: An exception in the Go game if you mouse down on the board but then drag to the captured-pieces area and release the mouse.
     1 /*  This code is based on Apple's "GeekGameBoard" sample code, version 1.0.
     2     http://developer.apple.com/samplecode/GeekGameBoard/
     3     Copyright © 2007 Apple Inc. Copyright © 2008 Jens Alfke. All Rights Reserved.
     4 
     5     Redistribution and use in source and binary forms, with or without modification, are permitted
     6     provided that the following conditions are met:
     7 
     8     * Redistributions of source code must retain the above copyright notice, this list of conditions
     9       and the following disclaimer.
    10     * Redistributions in binary form must reproduce the above copyright notice, this list of
    11       conditions and the following disclaimer in the documentation and/or other materials provided
    12       with the distribution.
    13 
    14     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
    15     IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
    16     FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI-
    17     BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
    18     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
    19     PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
    20     CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 
    21     THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    22 */
    23 #import "BoardView.h"
    24 #import "Bit.h"
    25 #import "BitHolder.h"
    26 #import "Game.h"
    27 #import "QuartzUtils.h"
    28 #import "GGBUtils.h"
    29 
    30 
    31 @interface BoardView ()
    32 - (void) _findDropTarget: (NSPoint)pos;
    33 @end
    34 
    35 
    36 @implementation BoardView
    37 
    38 
    39 @synthesize game=_game, gameboard=_gameboard;
    40 
    41 
    42 - (void) dealloc
    43 {
    44     [_game release];
    45     [super dealloc];
    46 }
    47 
    48 
    49 - (void) startGameNamed: (NSString*)gameClassName
    50 {
    51     if( _gameboard ) {
    52         [_gameboard removeFromSuperlayer];
    53         _gameboard = nil;
    54     }
    55     _gameboard = [[CALayer alloc] init];
    56     _gameboard.frame = [self gameBoardFrame];
    57     _gameboard.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable;
    58     [self.layer addSublayer: _gameboard];
    59     [_gameboard release];
    60     
    61     Class gameClass = NSClassFromString(gameClassName);
    62     setObj(&_game, [[gameClass alloc] initWithBoard: _gameboard]);
    63 }
    64 
    65 
    66 - (CGRect) gameBoardFrame
    67 {
    68     return self.layer.bounds;
    69 }
    70 
    71 
    72 - (void)resetCursorRects
    73 {
    74     [super resetCursorRects];
    75     [self addCursorRect: self.bounds cursor: [NSCursor openHandCursor]];
    76 }
    77 
    78 
    79 - (IBAction) enterFullScreen: (id)sender
    80 {
    81     if( self.isInFullScreenMode ) {
    82         [self exitFullScreenModeWithOptions: nil];
    83     } else {
    84         [self enterFullScreenMode: self.window.screen 
    85                       withOptions: nil];
    86     }
    87 }
    88 
    89 
    90 #pragma mark -
    91 #pragma mark KEY EVENTS:
    92 
    93 
    94 - (void) keyDown: (NSEvent*)ev
    95 {
    96     if( self.isInFullScreenMode ) {
    97         if( [ev.charactersIgnoringModifiers hasPrefix: @"\033"] )       // Esc key
    98             [self enterFullScreen: self];
    99     }
   100 }
   101 
   102 
   103 #pragma mark -
   104 #pragma mark HIT-TESTING:
   105 
   106 
   107 /** Converts a point from window coords, to this view's root layer's coords. */
   108 - (CGPoint) _convertPointFromWindowToLayer: (NSPoint)locationInWindow
   109 {
   110     NSPoint where = [self convertPoint: locationInWindow fromView: nil];    // convert to view coords
   111     return NSPointToCGPoint( [self convertPointToBase: where] );            // then to layer coords
   112 }
   113 
   114 
   115 // Hit-testing callbacks (to identify which layers caller is interested in):
   116 typedef BOOL (*LayerMatchCallback)(CALayer*);
   117 
   118 static BOOL layerIsBit( CALayer* layer )        {return [layer isKindOfClass: [Bit class]];}
   119 static BOOL layerIsBitHolder( CALayer* layer )  {return [layer conformsToProtocol: @protocol(BitHolder)];}
   120 static BOOL layerIsDropTarget( CALayer* layer ) {return [layer respondsToSelector: @selector(draggingEntered:)];}
   121 
   122 
   123 /** Locates the layer at a given point in window coords.
   124     If the leaf layer doesn't pass the layer-match callback, the nearest ancestor that does is returned.
   125     If outOffset is provided, the point's position relative to the layer is stored into it. */
   126 - (CALayer*) hitTestPoint: (NSPoint)locationInWindow
   127          forLayerMatching: (LayerMatchCallback)match
   128                    offset: (CGPoint*)outOffset
   129 {
   130     CGPoint where = [self _convertPointFromWindowToLayer: locationInWindow ];
   131     CALayer *layer = [_gameboard hitTest: where];
   132     while( layer ) {
   133         if( match(layer) ) {
   134             CGPoint bitPos = [self.layer convertPoint: layer.position 
   135                               fromLayer: layer.superlayer];
   136             if( outOffset )
   137                 *outOffset = CGPointMake( bitPos.x-where.x, bitPos.y-where.y);
   138             return layer;
   139         } else
   140             layer = layer.superlayer;
   141     }
   142     return nil;
   143 }
   144 
   145 
   146 #pragma mark -
   147 #pragma mark MOUSE CLICKS & DRAGS:
   148 
   149 
   150 - (void) mouseDown: (NSEvent*)ev
   151 {
   152     BOOL placing = NO;
   153     _dragStartPos = ev.locationInWindow;
   154     _dragBit = (Bit*) [self hitTestPoint: _dragStartPos
   155                         forLayerMatching: layerIsBit 
   156                                   offset: &_dragOffset];
   157     
   158     if( ! _dragBit ) {
   159         // If no bit was clicked, see if it's a BitHolder the game will let the user add a Bit to:
   160         id<BitHolder> holder = (id<BitHolder>) [self hitTestPoint: _dragStartPos
   161                                                  forLayerMatching: layerIsBitHolder
   162                                                            offset: NULL];
   163         if( holder ) {
   164             _dragBit = [_game bitToPlaceInHolder: holder];
   165             if( _dragBit ) {
   166                 _dragOffset.x = _dragOffset.y = 0;
   167                 if( _dragBit.superlayer==nil )
   168                     _dragBit.position = [self _convertPointFromWindowToLayer: _dragStartPos];
   169                 placing = YES;
   170             }
   171         }
   172     }
   173     
   174     if( ! _dragBit ) {
   175         Beep();
   176         return;
   177     }
   178     
   179     // Clicked on a Bit:
   180     _dragMoved = NO;
   181     _dropTarget = nil;
   182     _oldHolder = _dragBit.holder;
   183     // Ask holder's and game's permission before dragging:
   184     if( _oldHolder ) {
   185         _dragBit = [_oldHolder canDragBit: _dragBit];
   186         if( _dragBit && ! [_game canBit: _dragBit moveFrom: _oldHolder] ) {
   187             [_oldHolder cancelDragBit: _dragBit];
   188             _dragBit = nil;
   189         }
   190         if( ! _dragBit ) {
   191             _oldHolder = nil;
   192             NSBeep();
   193             return;
   194         }
   195     }
   196     
   197     // Start dragging:
   198     _oldSuperlayer = _dragBit.superlayer;
   199     _oldLayerIndex = [_oldSuperlayer.sublayers indexOfObjectIdenticalTo: _dragBit];
   200     _oldPos = _dragBit.position;
   201     ChangeSuperlayer(_dragBit, self.layer, self.layer.sublayers.count);
   202     _dragBit.pickedUp = YES;
   203     [[NSCursor closedHandCursor] push];
   204     
   205     if( placing ) {
   206         if( _oldSuperlayer )
   207             _dragBit.position = [self _convertPointFromWindowToLayer: _dragStartPos];
   208         _dragMoved = YES;
   209         [self _findDropTarget: _dragStartPos];
   210     }
   211 }
   212 
   213 
   214 - (void) mouseDragged: (NSEvent*)ev
   215 {
   216     if( _dragBit ) {
   217         // Get the mouse position, and see if we've moved 3 pixels since the mouseDown:
   218         NSPoint pos = ev.locationInWindow;
   219         if( fabs(pos.x-_dragStartPos.x)>=3 || fabs(pos.y-_dragStartPos.y)>=3 )
   220             _dragMoved = YES;
   221         
   222         // Move the _dragBit (without animation -- it's unnecessary and slows down responsiveness):
   223         CGPoint where = [self _convertPointFromWindowToLayer: pos];
   224         where.x += _dragOffset.x;
   225         where.y += _dragOffset.y;
   226         
   227         CGPoint newPos = [_dragBit.superlayer convertPoint: where fromLayer: self.layer];
   228 
   229         [CATransaction flush];
   230         [CATransaction begin];
   231         [CATransaction setValue:(id)kCFBooleanTrue
   232                          forKey:kCATransactionDisableActions];
   233         _dragBit.position = newPos;
   234         [CATransaction commit];
   235 
   236         // Find what it's over:
   237         [self _findDropTarget: pos];
   238     }
   239 }
   240 
   241 
   242 - (void) _findDropTarget: (NSPoint)locationInWindow
   243 {
   244     locationInWindow.x += _dragOffset.x;
   245     locationInWindow.y += _dragOffset.y;
   246     id<BitHolder> target = (id<BitHolder>) [self hitTestPoint: locationInWindow
   247                                              forLayerMatching: layerIsBitHolder
   248                                                        offset: NULL];
   249     if( target == _oldHolder )
   250         target = nil;
   251     if( target != _dropTarget ) {
   252         [_dropTarget willNotDropBit: _dragBit];
   253         _dropTarget.highlighted = NO;
   254         _dropTarget = nil;
   255     }
   256     if( target ) {
   257         CGPoint targetPos = [(CALayer*)target convertPoint: _dragBit.position
   258                                                  fromLayer: _dragBit.superlayer];
   259         if( [target canDropBit: _dragBit atPoint: targetPos]
   260            && [_game canBit: _dragBit moveFrom: _oldHolder to: target] ) {
   261             _dropTarget = target;
   262             _dropTarget.highlighted = YES;
   263         }
   264     }
   265 }
   266 
   267 
   268 - (void) mouseUp: (NSEvent*)ev
   269 {
   270     if( _dragBit ) {
   271         if( _dragMoved ) {
   272             // Update the drag tracking to the final mouse position:
   273             [self mouseDragged: ev];
   274             _dropTarget.highlighted = NO;
   275             _dragBit.pickedUp = NO;
   276 
   277             // Is the move legal?
   278             if( _dropTarget && [_dropTarget dropBit: _dragBit
   279                                             atPoint: [(CALayer*)_dropTarget convertPoint: _dragBit.position 
   280                                                                             fromLayer: _dragBit.superlayer]] ) {
   281                 // Yes, notify the interested parties:
   282                 [_oldHolder draggedBit: _dragBit to: _dropTarget];
   283                 [_game bit: _dragBit movedFrom: _oldHolder to: _dropTarget];
   284             } else {
   285                 // Nope, cancel:
   286                 [_dropTarget willNotDropBit: _dragBit];
   287                 if( _oldSuperlayer ) {
   288                     ChangeSuperlayer(_dragBit, _oldSuperlayer, _oldLayerIndex);
   289                     _dragBit.position = _oldPos;
   290                     [_oldHolder cancelDragBit: _dragBit];
   291                 } else {
   292                     [_dragBit removeFromSuperlayer];
   293                 }
   294             }
   295         } else {
   296             // Just a click, without a drag:
   297             _dropTarget.highlighted = NO;
   298             _dragBit.pickedUp = NO;
   299             ChangeSuperlayer(_dragBit, _oldSuperlayer, _oldLayerIndex);
   300             [_oldHolder cancelDragBit: _dragBit];
   301             if( ! [_game clickedBit: _dragBit] )
   302                 NSBeep();
   303         }
   304         _dropTarget = nil;
   305         _dragBit = nil;
   306         [NSCursor pop];
   307     }
   308 }
   309 
   310 
   311 #pragma mark -
   312 #pragma mark INCOMING DRAGS:
   313 
   314 
   315 // subroutine to call the target
   316 static int tell( id target, SEL selector, id arg, int defaultValue )
   317 {
   318     if( target && [target respondsToSelector: selector] )
   319         return (ssize_t) [target performSelector: selector withObject: arg];
   320     else
   321         return defaultValue;
   322 }
   323 
   324 
   325 - (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
   326 {
   327     _viewDropTarget = [self hitTestPoint: [sender draggingLocation]
   328                         forLayerMatching: layerIsDropTarget
   329                                   offset: NULL];
   330     _viewDropOp = _viewDropTarget ?[_viewDropTarget draggingEntered: sender] :NSDragOperationNone;
   331     return _viewDropOp;
   332 }
   333 
   334 - (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
   335 {
   336     CALayer *target = [self hitTestPoint: [sender draggingLocation]
   337                         forLayerMatching: layerIsDropTarget 
   338                                   offset: NULL];
   339     if( target == _viewDropTarget ) {
   340         if( _viewDropTarget )
   341             _viewDropOp = tell(_viewDropTarget,@selector(draggingUpdated:),sender,_viewDropOp);
   342     } else {
   343         tell(_viewDropTarget,@selector(draggingExited:),sender,0);
   344         _viewDropTarget = target;
   345         if( _viewDropTarget )
   346             _viewDropOp = [_viewDropTarget draggingEntered: sender];
   347         else
   348             _viewDropOp = NSDragOperationNone;
   349     }
   350     return _viewDropOp;
   351 }
   352 
   353 - (BOOL)wantsPeriodicDraggingUpdates
   354 {
   355     return (_viewDropTarget!=nil);
   356 }
   357 
   358 - (void)draggingExited:(id <NSDraggingInfo>)sender
   359 {
   360     tell(_viewDropTarget,@selector(draggingExited:),sender,0);
   361     _viewDropTarget = nil;
   362 }
   363 
   364 - (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
   365 {
   366     return tell(_viewDropTarget,@selector(prepareForDragOperation:),sender,YES);
   367 }
   368 
   369 - (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
   370 {
   371     return [_viewDropTarget performDragOperation: sender];
   372 }
   373 
   374 - (void)concludeDragOperation:(id <NSDraggingInfo>)sender
   375 {
   376     tell(_viewDropTarget,@selector(concludeDragOperation:),sender,0);
   377 }
   378 
   379 - (void)draggingEnded:(id <NSDraggingInfo>)sender
   380 {
   381     tell(_viewDropTarget,@selector(draggingEnded:),sender,0);
   382 }
   383 
   384 @end