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