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