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