Source/BoardView.m
author Jens Alfke <jens@mooseyard.com>
Mon Jul 07 15:47:42 2008 -0700 (2008-07-07)
changeset 12 4e567e11f45f
parent 10 6c78cc6bd7a6
child 14 4585c74d809c
permissions -rw-r--r--
Added new convenience methods for Game implementations.
     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 "Turn.h"
    28 #import "Player.h"
    29 #import "QuartzUtils.h"
    30 #import "GGBUtils.h"
    31 
    32 
    33 @interface BoardView ()
    34 - (void) _findDropTarget: (NSPoint)pos;
    35 @end
    36 
    37 
    38 @implementation BoardView
    39 
    40 
    41 @synthesize gameboard=_gameboard;
    42 
    43 
    44 - (void) dealloc
    45 {
    46     [_game release];
    47     [super dealloc];
    48 }
    49 
    50 
    51 - (void) _removeGameBoard
    52 {
    53     if( _gameboard ) {
    54         RemoveImmediately(_gameboard);
    55         _gameboard = nil;
    56     }
    57 }
    58 
    59 - (void) createGameBoard
    60 {
    61     [self _removeGameBoard];
    62     _gameboard = [[CALayer alloc] init];
    63     _gameboard.frame = [self gameBoardFrame];
    64     _gameboard.autoresizingMask = kCALayerMinXMargin | kCALayerMaxXMargin | kCALayerMinYMargin | kCALayerMaxYMargin;
    65 
    66     // Tell the game to set up the board:
    67     _game.board = _gameboard;
    68 
    69     [self.layer addSublayer: _gameboard];
    70     [_gameboard release];
    71 }
    72 
    73 
    74 - (void) reverseBoard
    75 {
    76     [_gameboard setValue: [NSNumber numberWithDouble: M_PI]
    77               forKeyPath: @"transform.rotation"];
    78 }
    79 
    80 
    81 - (Game*) game
    82 {
    83     return _game;
    84 }
    85 
    86 - (void) setGame: (Game*)game
    87 {
    88     if( game!=_game ) {
    89         setObj(&_game,game);
    90         [self createGameBoard];
    91     }
    92 }
    93 
    94 - (void) startGameNamed: (NSString*)gameClassName
    95 {
    96     Class gameClass = NSClassFromString(gameClassName);
    97     Game *game = [[gameClass alloc] init];
    98     if( game ) {
    99         self.game = game;
   100         [game release];
   101     }
   102 }
   103 
   104 
   105 - (BOOL) canMakeMove
   106 {
   107     return _game != nil
   108         && _game.currentPlayer.local
   109         && _game.currentTurn.status < kTurnComplete;
   110 }
   111 
   112 
   113 - (CGRect) gameBoardFrame
   114 {
   115     return self.layer.bounds;
   116 }
   117 
   118 
   119 - (void)resetCursorRects
   120 {
   121     [super resetCursorRects];
   122     if( self.canMakeMove )
   123         [self addCursorRect: self.bounds cursor: [NSCursor openHandCursor]];
   124 }
   125 
   126 
   127 - (IBAction) enterFullScreen: (id)sender
   128 {
   129     [self _removeGameBoard];
   130     if( self.isInFullScreenMode ) {
   131         [self exitFullScreenModeWithOptions: nil];
   132     } else {
   133         [self enterFullScreenMode: self.window.screen 
   134                       withOptions: nil];
   135     }
   136     [self createGameBoard];
   137 }
   138 
   139 
   140 - (void)viewWillStartLiveResize
   141 {
   142     [super viewWillStartLiveResize];
   143     _oldSize = self.frame.size;
   144 }
   145 
   146 - (void)setFrameSize:(NSSize)newSize
   147 {
   148     [super setFrameSize: newSize];
   149     if( _oldSize.width > 0.0f ) {
   150         CGAffineTransform xform = _gameboard.affineTransform;
   151         xform.a = xform.d = MIN(newSize.width,newSize.height)/MIN(_oldSize.width,_oldSize.height);
   152         _gameboard.affineTransform = xform;
   153     } else
   154         [self createGameBoard];
   155 }
   156 
   157 - (void)viewDidEndLiveResize
   158 {
   159     [super viewDidEndLiveResize];
   160     _oldSize.width = _oldSize.height = 0.0f;
   161     [self createGameBoard];
   162 }
   163 
   164 
   165 #pragma mark -
   166 #pragma mark KEY EVENTS:
   167 
   168 
   169 - (void) keyDown: (NSEvent*)ev
   170 {
   171     if( self.isInFullScreenMode ) {
   172         if( [ev.charactersIgnoringModifiers hasPrefix: @"\033"] )       // Esc key
   173             [self enterFullScreen: self];
   174     }
   175 }
   176 
   177 
   178 #pragma mark -
   179 #pragma mark HIT-TESTING:
   180 
   181 
   182 /** Converts a point from window coords, to this view's root layer's coords. */
   183 - (CGPoint) _convertPointFromWindowToLayer: (NSPoint)locationInWindow
   184 {
   185     NSPoint where = [self convertPoint: locationInWindow fromView: nil];    // convert to view coords
   186     return NSPointToCGPoint( [self convertPointToBase: where] );            // then to layer coords
   187 }
   188 
   189 
   190 // Hit-testing callbacks (to identify which layers caller is interested in):
   191 typedef BOOL (*LayerMatchCallback)(CALayer*);
   192 
   193 static BOOL layerIsBit( CALayer* layer )        {return [layer isKindOfClass: [Bit class]];}
   194 static BOOL layerIsBitHolder( CALayer* layer )  {return [layer conformsToProtocol: @protocol(BitHolder)];}
   195 static BOOL layerIsDropTarget( CALayer* layer ) {return [layer respondsToSelector: @selector(draggingEntered:)];}
   196 
   197 
   198 /** Locates the layer at a given point in window coords.
   199     If the leaf layer doesn't pass the layer-match callback, the nearest ancestor that does is returned.
   200     If outOffset is provided, the point's position relative to the layer is stored into it. */
   201 - (CALayer*) hitTestPoint: (NSPoint)locationInWindow
   202          forLayerMatching: (LayerMatchCallback)match
   203                    offset: (CGPoint*)outOffset
   204 {
   205     CGPoint where = [self _convertPointFromWindowToLayer: locationInWindow ];
   206     CALayer *layer = [_gameboard hitTest: where];
   207     while( layer ) {
   208         if( match(layer) ) {
   209             CGPoint bitPos = [self.layer convertPoint: layer.position 
   210                               fromLayer: layer.superlayer];
   211             if( outOffset )
   212                 *outOffset = CGPointMake( bitPos.x-where.x, bitPos.y-where.y);
   213             return layer;
   214         } else
   215             layer = layer.superlayer;
   216     }
   217     return nil;
   218 }
   219 
   220 
   221 #pragma mark -
   222 #pragma mark MOUSE CLICKS & DRAGS:
   223 
   224 
   225 - (void) mouseDown: (NSEvent*)ev
   226 {
   227     if( ! self.canMakeMove ) {
   228         NSBeep();
   229         return;
   230     }
   231     
   232     BOOL placing = NO;
   233     _dragStartPos = ev.locationInWindow;
   234     _dragBit = (Bit*) [self hitTestPoint: _dragStartPos
   235                         forLayerMatching: layerIsBit 
   236                                   offset: &_dragOffset];
   237     
   238     if( ! _dragBit ) {
   239         // If no bit was clicked, see if it's a BitHolder the game will let the user add a Bit to:
   240         id<BitHolder> holder = (id<BitHolder>) [self hitTestPoint: _dragStartPos
   241                                                  forLayerMatching: layerIsBitHolder
   242                                                            offset: NULL];
   243         if( holder ) {
   244             _dragBit = [_game bitToPlaceInHolder: holder];
   245             if( _dragBit ) {
   246                 _dragOffset.x = _dragOffset.y = 0;
   247                 if( _dragBit.superlayer==nil )
   248                     _dragBit.position = [self _convertPointFromWindowToLayer: _dragStartPos];
   249                 placing = YES;
   250             }
   251         }
   252     }
   253     
   254     if( ! _dragBit ) {
   255         Beep();
   256         return;
   257     }
   258     
   259     // Clicked on a Bit:
   260     _dragMoved = NO;
   261     _dropTarget = nil;
   262     _oldHolder = _dragBit.holder;
   263     // Ask holder's and game's permission before dragging:
   264     if( _oldHolder ) {
   265         _dragBit = [_oldHolder canDragBit: _dragBit];
   266         if( _dragBit && ! [_game canBit: _dragBit moveFrom: _oldHolder] ) {
   267             [_oldHolder cancelDragBit: _dragBit];
   268             _dragBit = nil;
   269         }
   270         if( ! _dragBit ) {
   271             _oldHolder = nil;
   272             NSBeep();
   273             return;
   274         }
   275     }
   276     
   277     // Start dragging:
   278     _oldSuperlayer = _dragBit.superlayer;
   279     _oldLayerIndex = [_oldSuperlayer.sublayers indexOfObjectIdenticalTo: _dragBit];
   280     _oldPos = _dragBit.position;
   281     ChangeSuperlayer(_dragBit, self.layer, self.layer.sublayers.count);
   282     _dragBit.pickedUp = YES;
   283     [[NSCursor closedHandCursor] push];
   284     
   285     if( placing ) {
   286         if( _oldSuperlayer )
   287             _dragBit.position = [self _convertPointFromWindowToLayer: _dragStartPos];
   288         _dragMoved = YES;
   289         [self _findDropTarget: _dragStartPos];
   290     }
   291 }
   292 
   293 
   294 - (void) mouseDragged: (NSEvent*)ev
   295 {
   296     if( _dragBit ) {
   297         // Get the mouse position, and see if we've moved 3 pixels since the mouseDown:
   298         NSPoint pos = ev.locationInWindow;
   299         if( fabs(pos.x-_dragStartPos.x)>=3 || fabs(pos.y-_dragStartPos.y)>=3 )
   300             _dragMoved = YES;
   301         
   302         // Move the _dragBit (without animation -- it's unnecessary and slows down responsiveness):
   303         CGPoint where = [self _convertPointFromWindowToLayer: pos];
   304         where.x += _dragOffset.x;
   305         where.y += _dragOffset.y;
   306         
   307         CGPoint newPos = [_dragBit.superlayer convertPoint: where fromLayer: self.layer];
   308 
   309         [CATransaction flush];
   310         [CATransaction begin];
   311         [CATransaction setValue:(id)kCFBooleanTrue
   312                          forKey:kCATransactionDisableActions];
   313         _dragBit.position = newPos;
   314         [CATransaction commit];
   315 
   316         // Find what it's over:
   317         [self _findDropTarget: pos];
   318     }
   319 }
   320 
   321 
   322 - (void) _findDropTarget: (NSPoint)locationInWindow
   323 {
   324     locationInWindow.x += _dragOffset.x;
   325     locationInWindow.y += _dragOffset.y;
   326     id<BitHolder> target = (id<BitHolder>) [self hitTestPoint: locationInWindow
   327                                              forLayerMatching: layerIsBitHolder
   328                                                        offset: NULL];
   329     if( target == _oldHolder )
   330         target = nil;
   331     if( target != _dropTarget ) {
   332         [_dropTarget willNotDropBit: _dragBit];
   333         _dropTarget.highlighted = NO;
   334         _dropTarget = nil;
   335     }
   336     if( target ) {
   337         CGPoint targetPos = [(CALayer*)target convertPoint: _dragBit.position
   338                                                  fromLayer: _dragBit.superlayer];
   339         if( [target canDropBit: _dragBit atPoint: targetPos]
   340            && [_game canBit: _dragBit moveFrom: _oldHolder to: target] ) {
   341             _dropTarget = target;
   342             _dropTarget.highlighted = YES;
   343         }
   344     }
   345 }
   346 
   347 
   348 - (void) mouseUp: (NSEvent*)ev
   349 {
   350     if( _dragBit ) {
   351         if( _dragMoved ) {
   352             // Update the drag tracking to the final mouse position:
   353             [self mouseDragged: ev];
   354             _dropTarget.highlighted = NO;
   355             _dragBit.pickedUp = NO;
   356 
   357             // Is the move legal?
   358             if( _dropTarget && [_dropTarget dropBit: _dragBit
   359                                             atPoint: [(CALayer*)_dropTarget convertPoint: _dragBit.position 
   360                                                                             fromLayer: _dragBit.superlayer]] ) {
   361                 // Yes, notify the interested parties:
   362                 [_oldHolder draggedBit: _dragBit to: _dropTarget];
   363                 [_game bit: _dragBit movedFrom: _oldHolder to: _dropTarget];
   364             } else {
   365                 // Nope, cancel:
   366                 [_dropTarget willNotDropBit: _dragBit];
   367                 if( _oldSuperlayer ) {
   368                     ChangeSuperlayer(_dragBit, _oldSuperlayer, _oldLayerIndex);
   369                     _dragBit.position = _oldPos;
   370                     [_oldHolder cancelDragBit: _dragBit];
   371                 } else {
   372                     [_dragBit removeFromSuperlayer];
   373                 }
   374             }
   375         } else {
   376             // Just a click, without a drag:
   377             _dropTarget.highlighted = NO;
   378             _dragBit.pickedUp = NO;
   379             ChangeSuperlayer(_dragBit, _oldSuperlayer, _oldLayerIndex);
   380             [_oldHolder cancelDragBit: _dragBit];
   381             if( ! [_game clickedBit: _dragBit] )
   382                 NSBeep();
   383         }
   384 
   385         _dropTarget = nil;
   386         _dragBit = nil;
   387         [NSCursor pop];
   388     }
   389 }
   390 
   391 
   392 #pragma mark -
   393 #pragma mark INCOMING DRAGS:
   394 
   395 
   396 // subroutine to call the target
   397 static int tell( id target, SEL selector, id arg, int defaultValue )
   398 {
   399     if( target && [target respondsToSelector: selector] )
   400         return (ssize_t) [target performSelector: selector withObject: arg];
   401     else
   402         return defaultValue;
   403 }
   404 
   405 
   406 - (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
   407 {
   408     _viewDropTarget = [self hitTestPoint: [sender draggingLocation]
   409                         forLayerMatching: layerIsDropTarget
   410                                   offset: NULL];
   411     _viewDropOp = _viewDropTarget ?[_viewDropTarget draggingEntered: sender] :NSDragOperationNone;
   412     return _viewDropOp;
   413 }
   414 
   415 - (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
   416 {
   417     CALayer *target = [self hitTestPoint: [sender draggingLocation]
   418                         forLayerMatching: layerIsDropTarget 
   419                                   offset: NULL];
   420     if( target == _viewDropTarget ) {
   421         if( _viewDropTarget )
   422             _viewDropOp = tell(_viewDropTarget,@selector(draggingUpdated:),sender,_viewDropOp);
   423     } else {
   424         tell(_viewDropTarget,@selector(draggingExited:),sender,0);
   425         _viewDropTarget = target;
   426         if( _viewDropTarget )
   427             _viewDropOp = [_viewDropTarget draggingEntered: sender];
   428         else
   429             _viewDropOp = NSDragOperationNone;
   430     }
   431     return _viewDropOp;
   432 }
   433 
   434 - (BOOL)wantsPeriodicDraggingUpdates
   435 {
   436     return (_viewDropTarget!=nil);
   437 }
   438 
   439 - (void)draggingExited:(id <NSDraggingInfo>)sender
   440 {
   441     tell(_viewDropTarget,@selector(draggingExited:),sender,0);
   442     _viewDropTarget = nil;
   443 }
   444 
   445 - (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
   446 {
   447     return tell(_viewDropTarget,@selector(prepareForDragOperation:),sender,YES);
   448 }
   449 
   450 - (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
   451 {
   452     return [_viewDropTarget performDragOperation: sender];
   453 }
   454 
   455 - (void)concludeDragOperation:(id <NSDraggingInfo>)sender
   456 {
   457     tell(_viewDropTarget,@selector(concludeDragOperation:),sender,0);
   458 }
   459 
   460 - (void)draggingEnded:(id <NSDraggingInfo>)sender
   461 {
   462     tell(_viewDropTarget,@selector(draggingEnded:),sender,0);
   463 }
   464 
   465 @end