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