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