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