Source/BoardView.m
author snej@snej.local
Sun Jan 11 00:02:27 2009 -0800 (2009-01-11)
changeset 26 e7a464fb6d39
parent 23 efe5d4523a23
permissions -rw-r--r--
Added ball graphics to Xcode project, which fixes the Go game. Fixed Tic-Tac-Toe. Fixed an exception in the demo app when changing games.
     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+Protected.h"
    27 #import "Turn.h"
    28 #import "Player.h"
    29 #import "QuartzUtils.h"
    30 #import "GGBUtils.h"
    31 
    32 
    33 #define kMaxPerspective 0.965   // 55 degrees
    34 
    35 
    36 @interface BoardView ()
    37 - (void) _findDropTarget: (NSPoint)pos;
    38 @end
    39 
    40 
    41 @implementation BoardView
    42 
    43 
    44 @synthesize table=_table, gameBoardInset=_gameBoardInset;
    45 
    46 
    47 - (void) dealloc
    48 {
    49     [_game release];
    50     [super dealloc];
    51 }
    52 
    53 
    54 #pragma mark -
    55 #pragma mark PERSPECTIVE:
    56 
    57 
    58 - (void) _applyPerspective
    59 {
    60     CATransform3D t;
    61     if( fabs(_perspective) >= M_PI/180 ) {
    62         CGSize size = self.layer.bounds.size;
    63         t = CATransform3DMakeTranslation(-size.width/2, -size.height/4, 0);
    64         t = CATransform3DConcat(t, CATransform3DMakeRotation(-_perspective, 1,0,0));
    65         
    66         CATransform3D pers = CATransform3DIdentity;
    67         pers.m34 = 1.0/-2000;
    68         t = CATransform3DConcat(t, pers);
    69         t = CATransform3DConcat(t, CATransform3DMakeTranslation(size.width/2, 
    70                                                                 size.height*(0.25 + 0.05*sin(2*_perspective)),
    71                                                                 0));
    72         self.layer.borderWidth = 3;
    73     } else {
    74         t = CATransform3DIdentity;
    75         self.layer.borderWidth = 0;
    76     }
    77     self.layer.transform = t;
    78 }    
    79 
    80 - (CGFloat) perspective {return _perspective;}
    81 
    82 - (void) setPerspective: (CGFloat)p
    83 {
    84     p = MAX(0.0, MIN(kMaxPerspective, p));
    85     if( p != _perspective ) {
    86         _perspective = p;
    87         [self _applyPerspective];
    88         _game.tablePerspectiveAngle = p;
    89     }
    90 }
    91 
    92 - (IBAction) tiltUp: (id)sender     {self.perspective -= M_PI/40;}
    93 - (IBAction) tiltDown: (id)sender   {self.perspective += M_PI/40;}
    94 
    95 
    96 #pragma mark -
    97 #pragma mark GAME BOARD:
    98 
    99 
   100 - (void) _removeGameBoard
   101 {
   102     if( _table ) {
   103         RemoveImmediately(_table);
   104         _table = nil;
   105     }
   106 }
   107 
   108 - (void) createGameBoard
   109 {
   110     [self _removeGameBoard];
   111     _table = [[CALayer alloc] init];
   112     _table.frame = [self gameBoardFrame];
   113     _table.autoresizingMask = kCALayerMinXMargin | kCALayerMaxXMargin | kCALayerMinYMargin | kCALayerMaxYMargin;
   114     
   115     // Tell the game to set up the board:
   116     _game.tablePerspectiveAngle = _perspective;
   117     _game.table = _table;
   118 
   119     [self.layer addSublayer: _table];
   120     [_table release];
   121 }
   122 
   123 
   124 - (Game*) game
   125 {
   126     return _game;
   127 }
   128 
   129 - (void) setGame: (Game*)game
   130 {
   131     if( game!=_game ) {
   132         _game.table = nil;
   133         setObj(&_game,game);
   134         [self createGameBoard];
   135     }
   136 }
   137 
   138 - (void) startGameNamed: (NSString*)gameClassName
   139 {
   140     Class gameClass = NSClassFromString(gameClassName);
   141     Game *game = [[gameClass alloc] init];
   142     if( game ) {
   143         self.game = game;
   144         [game release];
   145     }
   146 }
   147 
   148 
   149 #pragma mark -
   150 #pragma mark VIEW MANIPULATION:
   151 
   152 
   153 - (CGRect) gameBoardFrame
   154 {
   155     return CGRectInset(self.layer.bounds, _gameBoardInset.width,_gameBoardInset.height);
   156 }
   157 
   158 
   159 - (void)resetCursorRects
   160 {
   161     [super resetCursorRects];
   162     if( _game.okToMove )
   163         [self addCursorRect: self.bounds cursor: [NSCursor openHandCursor]];
   164 }
   165 
   166 
   167 - (NSView*) fullScreenView
   168 {
   169     return _fullScreenView ?: self;
   170 }
   171 
   172 - (IBAction) enterFullScreen: (id)sender
   173 {
   174     //[self _removeGameBoard];
   175     if( self.fullScreenView.isInFullScreenMode ) {
   176         [self.fullScreenView exitFullScreenModeWithOptions: nil];
   177     } else {
   178         [self.fullScreenView enterFullScreenMode: self.window.screen 
   179                                      withOptions: nil];
   180     }
   181     //[self createGameBoard];
   182 }
   183 
   184 
   185 - (void)viewWillStartLiveResize
   186 {
   187     [super viewWillStartLiveResize];
   188     _oldSize = self.frame.size;
   189 }
   190 
   191 - (void)setFrameSize:(NSSize)newSize
   192 {
   193     [super setFrameSize: newSize];
   194     if( _oldSize.width > 0.0f ) {
   195         CGAffineTransform xform = _table.affineTransform;
   196         xform.a = xform.d = MIN(newSize.width,newSize.height)/MIN(_oldSize.width,_oldSize.height);
   197         BeginDisableAnimations();
   198         [self _applyPerspective];
   199         _table.affineTransform = xform;
   200         EndDisableAnimations();
   201     } else
   202         [self createGameBoard];
   203 }
   204 
   205 - (void)viewDidEndLiveResize
   206 {
   207     [super viewDidEndLiveResize];
   208     _oldSize.width = _oldSize.height = 0.0f;
   209     [self createGameBoard];
   210 }
   211 
   212 
   213 #pragma mark -
   214 #pragma mark KEY EVENTS:
   215 
   216 
   217 - (BOOL) performKeyEquivalent: (NSEvent*)ev
   218 {
   219     if( [ev.charactersIgnoringModifiers hasPrefix: @"\033"] ) {       // Esc key
   220         if( self.fullScreenView.isInFullScreenMode ) {
   221             [self performSelector: @selector(enterFullScreen:) withObject: nil afterDelay: 0.0];
   222             // without the delayed-perform, NSWindow crashes right after this method returns!
   223             return YES;
   224         }
   225     }
   226     return NO;
   227 }
   228 
   229 
   230 #pragma mark -
   231 #pragma mark HIT-TESTING:
   232 
   233 
   234 /** Converts a point from window coords, to this view's root layer's coords. */
   235 - (CGPoint) _convertPointFromWindowToLayer: (NSPoint)locationInWindow
   236 {
   237     NSPoint where = [self convertPoint: locationInWindow fromView: nil];    // convert to view coords
   238     where = [self convertPointToBase: where];                               // then to layer base coords
   239     return [self.layer convertPoint: NSPointToCGPoint(where)                // then to transformed layer coords
   240                           fromLayer: self.layer.superlayer];
   241 }
   242 
   243 
   244 // Hit-testing callbacks (to identify which layers caller is interested in):
   245 typedef BOOL (*LayerMatchCallback)(CALayer*);
   246 
   247 static BOOL layerIsBit( CALayer* layer )        {return [layer isKindOfClass: [Bit class]];}
   248 static BOOL layerIsBitHolder( CALayer* layer )  {return [layer conformsToProtocol: @protocol(BitHolder)];}
   249 static BOOL layerIsDropTarget( CALayer* layer ) {return [layer respondsToSelector: @selector(draggingEntered:)];}
   250 
   251 
   252 /** Locates the layer at a given point in window coords.
   253     If the leaf layer doesn't pass the layer-match callback, the nearest ancestor that does is returned.
   254     If outOffset is provided, the point's position relative to the layer is stored into it. */
   255 - (CALayer*) hitTestPoint: (NSPoint)locationInWindow
   256          forLayerMatching: (LayerMatchCallback)match
   257                    offset: (CGPoint*)outOffset
   258 {
   259     CGPoint where = [self _convertPointFromWindowToLayer: locationInWindow ];
   260     CALayer *layer = [_table hitTest: where];
   261     while( layer ) {
   262         if( match(layer) ) {
   263             CGPoint bitPos = [self.layer convertPoint: layer.position 
   264                               fromLayer: layer.superlayer];
   265             if( outOffset )
   266                 *outOffset = CGPointMake( bitPos.x-where.x, bitPos.y-where.y);
   267             return layer;
   268         } else
   269             layer = layer.superlayer;
   270     }
   271     return nil;
   272 }
   273 
   274 
   275 #pragma mark -
   276 #pragma mark MOUSE CLICKS & DRAGS:
   277 
   278 
   279 - (void) mouseDown: (NSEvent*)ev
   280 {
   281     if( ! _game.okToMove ) {
   282         NSBeep();
   283         return;
   284     }
   285     
   286     BOOL placing = NO;
   287     _dragStartPos = ev.locationInWindow;
   288     _dragBit = (Bit*) [self hitTestPoint: _dragStartPos
   289                         forLayerMatching: layerIsBit 
   290                                   offset: &_dragOffset];
   291     
   292     if( ! _dragBit ) {
   293         // If no bit was clicked, see if it's a BitHolder the game will let the user add a Bit to:
   294         id<BitHolder> holder = (id<BitHolder>) [self hitTestPoint: _dragStartPos
   295                                                  forLayerMatching: layerIsBitHolder
   296                                                            offset: NULL];
   297         if( holder ) {
   298             _dragBit = [_game bitToPlaceInHolder: holder];
   299             if( _dragBit ) {
   300                 _dragOffset.x = _dragOffset.y = 0;
   301                 if( _dragBit.superlayer==nil )
   302                     _dragBit.position = [self _convertPointFromWindowToLayer: _dragStartPos];
   303                 placing = YES;
   304             }
   305         }
   306     }
   307     
   308     if( ! _dragBit ) {
   309         Beep();
   310         return;
   311     }
   312     
   313     // Clicked on a Bit:
   314     _dragMoved = NO;
   315     _dropTarget = nil;
   316     _oldHolder = _dragBit.holder;
   317     // Ask holder's and game's permission before dragging:
   318     if( _oldHolder ) {
   319         _dragBit = [_oldHolder canDragBit: _dragBit];
   320         if( _dragBit && ! [_game canBit: _dragBit moveFrom: _oldHolder] ) {
   321             [_oldHolder cancelDragBit: _dragBit];
   322             _dragBit = nil;
   323         }
   324         if( ! _dragBit ) {
   325             _oldHolder = nil;
   326             NSBeep();
   327             return;
   328         }
   329     }
   330     
   331     // Start dragging:
   332     _oldSuperlayer = _dragBit.superlayer;
   333     _oldLayerIndex = [_oldSuperlayer.sublayers indexOfObjectIdenticalTo: _dragBit];
   334     _oldPos = _dragBit.position;
   335     ChangeSuperlayer(_dragBit, self.layer, self.layer.sublayers.count);
   336     _dragBit.pickedUp = YES;
   337     [[NSCursor closedHandCursor] push];
   338     
   339     if( placing ) {
   340         if( _oldSuperlayer )
   341             _dragBit.position = [self _convertPointFromWindowToLayer: _dragStartPos];
   342         _dragMoved = YES;
   343         [self _findDropTarget: _dragStartPos];
   344     }
   345 }
   346 
   347 
   348 - (void) mouseDragged: (NSEvent*)ev
   349 {
   350     if( _dragBit ) {
   351         // Get the mouse position, and see if we've moved 3 pixels since the mouseDown:
   352         NSPoint pos = ev.locationInWindow;
   353         if( fabs(pos.x-_dragStartPos.x)>=3 || fabs(pos.y-_dragStartPos.y)>=3 )
   354             _dragMoved = YES;
   355         
   356         // Move the _dragBit (without animation -- it's unnecessary and slows down responsiveness):
   357         CGPoint where = [self _convertPointFromWindowToLayer: pos];
   358         where.x += _dragOffset.x;
   359         where.y += _dragOffset.y;
   360         
   361         CGPoint newPos = [_dragBit.superlayer convertPoint: where fromLayer: self.layer];
   362 
   363         [CATransaction flush];
   364         [CATransaction begin];
   365         [CATransaction setValue:(id)kCFBooleanTrue
   366                          forKey:kCATransactionDisableActions];
   367         _dragBit.position = newPos;
   368         [CATransaction commit];
   369 
   370         // Find what it's over:
   371         [self _findDropTarget: pos];
   372     }
   373 }
   374 
   375 
   376 - (void) _findDropTarget: (NSPoint)locationInWindow
   377 {
   378     locationInWindow.x += _dragOffset.x;
   379     locationInWindow.y += _dragOffset.y;
   380     id<BitHolder> target = (id<BitHolder>) [self hitTestPoint: locationInWindow
   381                                              forLayerMatching: layerIsBitHolder
   382                                                        offset: NULL];
   383     if( target == _oldHolder )
   384         target = nil;
   385     if( target != _dropTarget ) {
   386         [_dropTarget willNotDropBit: _dragBit];
   387         _dropTarget.highlighted = NO;
   388         _dropTarget = nil;
   389     }
   390     if( target ) {
   391         CGPoint targetPos = [(CALayer*)target convertPoint: _dragBit.position
   392                                                  fromLayer: _dragBit.superlayer];
   393         if( [target canDropBit: _dragBit atPoint: targetPos]
   394            && [_game canBit: _dragBit moveFrom: _oldHolder to: target] ) {
   395             _dropTarget = target;
   396             _dropTarget.highlighted = YES;
   397         }
   398     }
   399 }
   400 
   401 
   402 - (void) mouseUp: (NSEvent*)ev
   403 {
   404     if( _dragBit ) {
   405         if( _dragMoved ) {
   406             // Update the drag tracking to the final mouse position:
   407             [self mouseDragged: ev];
   408             _dropTarget.highlighted = NO;
   409             _dragBit.pickedUp = NO;
   410 
   411             // Is the move legal?
   412             if( _dropTarget && [_dropTarget dropBit: _dragBit
   413                                             atPoint: [(CALayer*)_dropTarget convertPoint: _dragBit.position 
   414                                                                             fromLayer: _dragBit.superlayer]] ) {
   415                 // Yes, notify the interested parties:
   416                 [_oldHolder draggedBit: _dragBit to: _dropTarget];
   417                 [_game bit: _dragBit movedFrom: _oldHolder to: _dropTarget];
   418             } else {
   419                 // Nope, cancel:
   420                 [_dropTarget willNotDropBit: _dragBit];
   421                 if( _oldSuperlayer ) {
   422                     ChangeSuperlayer(_dragBit, _oldSuperlayer, _oldLayerIndex);
   423                     _dragBit.position = _oldPos;
   424                     [_oldHolder cancelDragBit: _dragBit];
   425                 } else {
   426                     [_dragBit removeFromSuperlayer];
   427                 }
   428             }
   429         } else {
   430             // Just a click, without a drag:
   431             _dropTarget.highlighted = NO;
   432             _dragBit.pickedUp = NO;
   433             ChangeSuperlayer(_dragBit, _oldSuperlayer, _oldLayerIndex);
   434             [_oldHolder cancelDragBit: _dragBit];
   435             if( ! [_game clickedBit: _dragBit] )
   436                 NSBeep();
   437         }
   438 
   439         _dropTarget = nil;
   440         _dragBit = nil;
   441         [NSCursor pop];
   442     }
   443 }
   444 
   445 
   446 - (void)scrollWheel:(NSEvent *)e
   447 {
   448     self.perspective += e.deltaY * M_PI/180;
   449     //Log(@"Perspective = %2.0f degrees (%5.3f radians)", self.perspective*180/M_PI, self.perspective);
   450 }
   451 
   452 
   453 #pragma mark -
   454 #pragma mark INCOMING DRAGS:
   455 
   456 
   457 // subroutine to call the target
   458 static int tell( id target, SEL selector, id arg, int defaultValue )
   459 {
   460     if( target && [target respondsToSelector: selector] )
   461         return (ssize_t) [target performSelector: selector withObject: arg];
   462     else
   463         return defaultValue;
   464 }
   465 
   466 
   467 - (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
   468 {
   469     _viewDropTarget = [self hitTestPoint: [sender draggingLocation]
   470                         forLayerMatching: layerIsDropTarget
   471                                   offset: NULL];
   472     _viewDropOp = _viewDropTarget ?[_viewDropTarget draggingEntered: sender] :NSDragOperationNone;
   473     return _viewDropOp;
   474 }
   475 
   476 - (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
   477 {
   478     CALayer *target = [self hitTestPoint: [sender draggingLocation]
   479                         forLayerMatching: layerIsDropTarget 
   480                                   offset: NULL];
   481     if( target == _viewDropTarget ) {
   482         if( _viewDropTarget )
   483             _viewDropOp = tell(_viewDropTarget,@selector(draggingUpdated:),sender,_viewDropOp);
   484     } else {
   485         tell(_viewDropTarget,@selector(draggingExited:),sender,0);
   486         _viewDropTarget = target;
   487         if( _viewDropTarget )
   488             _viewDropOp = [_viewDropTarget draggingEntered: sender];
   489         else
   490             _viewDropOp = NSDragOperationNone;
   491     }
   492     return _viewDropOp;
   493 }
   494 
   495 - (BOOL)wantsPeriodicDraggingUpdates
   496 {
   497     return (_viewDropTarget!=nil);
   498 }
   499 
   500 - (void)draggingExited:(id <NSDraggingInfo>)sender
   501 {
   502     tell(_viewDropTarget,@selector(draggingExited:),sender,0);
   503     _viewDropTarget = nil;
   504 }
   505 
   506 - (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
   507 {
   508     return tell(_viewDropTarget,@selector(prepareForDragOperation:),sender,YES);
   509 }
   510 
   511 - (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
   512 {
   513     return [_viewDropTarget performDragOperation: sender];
   514 }
   515 
   516 - (void)concludeDragOperation:(id <NSDraggingInfo>)sender
   517 {
   518     tell(_viewDropTarget,@selector(concludeDragOperation:),sender,0);
   519 }
   520 
   521 - (void)draggingEnded:(id <NSDraggingInfo>)sender
   522 {
   523     tell(_viewDropTarget,@selector(draggingEnded:),sender,0);
   524 }
   525 
   526 @end