Source/BoardView.m
author Jens Alfke <jens@mooseyard.com>
Sun Mar 16 15:06:47 2008 -0700 (2008-03-16)
changeset 7 428a194e3e59
parent 3 40d225cf9c43
child 9 a59acc683080
permissions -rw-r--r--
Game class now tracks board state and moves, as strings, and can step through its history.
Fixed another bug in Go (you could drag your captured stones back to the board!)
jens@0
     1
/*  This code is based on Apple's "GeekGameBoard" sample code, version 1.0.
jens@0
     2
    http://developer.apple.com/samplecode/GeekGameBoard/
jens@0
     3
    Copyright © 2007 Apple Inc. Copyright © 2008 Jens Alfke. All Rights Reserved.
jens@0
     4
jens@0
     5
    Redistribution and use in source and binary forms, with or without modification, are permitted
jens@0
     6
    provided that the following conditions are met:
jens@0
     7
jens@0
     8
    * Redistributions of source code must retain the above copyright notice, this list of conditions
jens@0
     9
      and the following disclaimer.
jens@0
    10
    * Redistributions in binary form must reproduce the above copyright notice, this list of
jens@0
    11
      conditions and the following disclaimer in the documentation and/or other materials provided
jens@0
    12
      with the distribution.
jens@0
    13
jens@0
    14
    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
jens@0
    15
    IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
jens@0
    16
    FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI-
jens@0
    17
    BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
jens@0
    18
    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
jens@0
    19
    PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
jens@0
    20
    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 
jens@0
    21
    THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
jens@0
    22
*/
jens@0
    23
#import "BoardView.h"
jens@0
    24
#import "Bit.h"
jens@0
    25
#import "BitHolder.h"
jens@0
    26
#import "Game.h"
jens@0
    27
#import "QuartzUtils.h"
jens@0
    28
#import "GGBUtils.h"
jens@0
    29
jens@0
    30
jens@3
    31
@interface BoardView ()
jens@3
    32
- (void) _findDropTarget: (NSPoint)pos;
jens@3
    33
@end
jens@3
    34
jens@3
    35
jens@0
    36
@implementation BoardView
jens@0
    37
jens@0
    38
jens@0
    39
@synthesize game=_game, gameboard=_gameboard;
jens@0
    40
jens@0
    41
jens@0
    42
- (void) dealloc
jens@0
    43
{
jens@0
    44
    [_game release];
jens@0
    45
    [super dealloc];
jens@0
    46
}
jens@0
    47
jens@0
    48
jens@0
    49
- (void) startGameNamed: (NSString*)gameClassName
jens@0
    50
{
jens@0
    51
    if( _gameboard ) {
jens@0
    52
        [_gameboard removeFromSuperlayer];
jens@0
    53
        _gameboard = nil;
jens@0
    54
    }
jens@0
    55
    _gameboard = [[CALayer alloc] init];
jens@0
    56
    _gameboard.frame = [self gameBoardFrame];
jens@0
    57
    _gameboard.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable;
jens@0
    58
    [self.layer addSublayer: _gameboard];
jens@0
    59
    [_gameboard release];
jens@0
    60
    
jens@0
    61
    Class gameClass = NSClassFromString(gameClassName);
jens@0
    62
    setObj(&_game, [[gameClass alloc] initWithBoard: _gameboard]);
jens@0
    63
}
jens@0
    64
jens@0
    65
jens@0
    66
- (CGRect) gameBoardFrame
jens@0
    67
{
jens@0
    68
    return self.layer.bounds;
jens@0
    69
}
jens@0
    70
jens@0
    71
jens@0
    72
- (void)resetCursorRects
jens@0
    73
{
jens@0
    74
    [super resetCursorRects];
jens@0
    75
    [self addCursorRect: self.bounds cursor: [NSCursor openHandCursor]];
jens@0
    76
}
jens@0
    77
jens@0
    78
jens@0
    79
- (IBAction) enterFullScreen: (id)sender
jens@0
    80
{
jens@0
    81
    if( self.isInFullScreenMode ) {
jens@0
    82
        [self exitFullScreenModeWithOptions: nil];
jens@0
    83
    } else {
jens@0
    84
        [self enterFullScreenMode: self.window.screen 
jens@0
    85
                      withOptions: nil];
jens@0
    86
    }
jens@0
    87
}
jens@0
    88
jens@0
    89
jens@0
    90
#pragma mark -
jens@0
    91
#pragma mark KEY EVENTS:
jens@0
    92
jens@0
    93
jens@0
    94
- (void) keyDown: (NSEvent*)ev
jens@0
    95
{
jens@0
    96
    if( self.isInFullScreenMode ) {
jens@0
    97
        if( [ev.charactersIgnoringModifiers hasPrefix: @"\033"] )       // Esc key
jens@0
    98
            [self enterFullScreen: self];
jens@0
    99
    }
jens@0
   100
}
jens@0
   101
jens@0
   102
jens@0
   103
#pragma mark -
jens@0
   104
#pragma mark HIT-TESTING:
jens@0
   105
jens@0
   106
jens@5
   107
/** Converts a point from window coords, to this view's root layer's coords. */
jens@5
   108
- (CGPoint) _convertPointFromWindowToLayer: (NSPoint)locationInWindow
jens@5
   109
{
jens@5
   110
    NSPoint where = [self convertPoint: locationInWindow fromView: nil];    // convert to view coords
jens@5
   111
    return NSPointToCGPoint( [self convertPointToBase: where] );            // then to layer coords
jens@5
   112
}
jens@5
   113
jens@5
   114
jens@0
   115
// Hit-testing callbacks (to identify which layers caller is interested in):
jens@0
   116
typedef BOOL (*LayerMatchCallback)(CALayer*);
jens@0
   117
jens@0
   118
static BOOL layerIsBit( CALayer* layer )        {return [layer isKindOfClass: [Bit class]];}
jens@0
   119
static BOOL layerIsBitHolder( CALayer* layer )  {return [layer conformsToProtocol: @protocol(BitHolder)];}
jens@0
   120
static BOOL layerIsDropTarget( CALayer* layer ) {return [layer respondsToSelector: @selector(draggingEntered:)];}
jens@0
   121
jens@0
   122
jens@0
   123
/** Locates the layer at a given point in window coords.
jens@0
   124
    If the leaf layer doesn't pass the layer-match callback, the nearest ancestor that does is returned.
jens@0
   125
    If outOffset is provided, the point's position relative to the layer is stored into it. */
jens@0
   126
- (CALayer*) hitTestPoint: (NSPoint)locationInWindow
jens@0
   127
         forLayerMatching: (LayerMatchCallback)match
jens@0
   128
                   offset: (CGPoint*)outOffset
jens@0
   129
{
jens@5
   130
    CGPoint where = [self _convertPointFromWindowToLayer: locationInWindow ];
jens@0
   131
    CALayer *layer = [_gameboard hitTest: where];
jens@0
   132
    while( layer ) {
jens@0
   133
        if( match(layer) ) {
jens@0
   134
            CGPoint bitPos = [self.layer convertPoint: layer.position 
jens@0
   135
                              fromLayer: layer.superlayer];
jens@0
   136
            if( outOffset )
jens@0
   137
                *outOffset = CGPointMake( bitPos.x-where.x, bitPos.y-where.y);
jens@0
   138
            return layer;
jens@0
   139
        } else
jens@0
   140
            layer = layer.superlayer;
jens@0
   141
    }
jens@0
   142
    return nil;
jens@0
   143
}
jens@0
   144
jens@0
   145
jens@0
   146
#pragma mark -
jens@0
   147
#pragma mark MOUSE CLICKS & DRAGS:
jens@0
   148
jens@0
   149
jens@0
   150
- (void) mouseDown: (NSEvent*)ev
jens@0
   151
{
jens@3
   152
    BOOL placing = NO;
jens@0
   153
    _dragStartPos = ev.locationInWindow;
jens@0
   154
    _dragBit = (Bit*) [self hitTestPoint: _dragStartPos
jens@0
   155
                        forLayerMatching: layerIsBit 
jens@0
   156
                                  offset: &_dragOffset];
jens@3
   157
    
jens@3
   158
    if( ! _dragBit ) {
jens@3
   159
        // If no bit was clicked, see if it's a BitHolder the game will let the user add a Bit to:
jens@3
   160
        id<BitHolder> holder = (id<BitHolder>) [self hitTestPoint: _dragStartPos
jens@3
   161
                                                 forLayerMatching: layerIsBitHolder
jens@3
   162
                                                           offset: NULL];
jens@3
   163
        if( holder ) {
jens@3
   164
            _dragBit = [_game bitToPlaceInHolder: holder];
jens@3
   165
            if( _dragBit ) {
jens@3
   166
                _dragOffset.x = _dragOffset.y = 0;
jens@3
   167
                if( _dragBit.superlayer==nil )
jens@5
   168
                    _dragBit.position = [self _convertPointFromWindowToLayer: _dragStartPos];
jens@3
   169
                placing = YES;
jens@3
   170
            }
jens@3
   171
        }
jens@3
   172
    }
jens@3
   173
    
jens@3
   174
    if( ! _dragBit ) {
jens@3
   175
        Beep();
jens@3
   176
        return;
jens@3
   177
    }
jens@3
   178
    
jens@3
   179
    // Clicked on a Bit:
jens@3
   180
    _dragMoved = NO;
jens@3
   181
    _dropTarget = nil;
jens@3
   182
    _oldHolder = _dragBit.holder;
jens@3
   183
    // Ask holder's and game's permission before dragging:
jens@3
   184
    if( _oldHolder ) {
jens@3
   185
        _dragBit = [_oldHolder canDragBit: _dragBit];
jens@0
   186
        if( _dragBit && ! [_game canBit: _dragBit moveFrom: _oldHolder] ) {
jens@0
   187
            [_oldHolder cancelDragBit: _dragBit];
jens@0
   188
            _dragBit = nil;
jens@0
   189
        }
jens@0
   190
        if( ! _dragBit ) {
jens@0
   191
            _oldHolder = nil;
jens@0
   192
            NSBeep();
jens@0
   193
            return;
jens@0
   194
        }
jens@3
   195
    }
jens@3
   196
    
jens@3
   197
    // Start dragging:
jens@3
   198
    _oldSuperlayer = _dragBit.superlayer;
jens@3
   199
    _oldLayerIndex = [_oldSuperlayer.sublayers indexOfObjectIdenticalTo: _dragBit];
jens@3
   200
    _oldPos = _dragBit.position;
jens@3
   201
    ChangeSuperlayer(_dragBit, self.layer, self.layer.sublayers.count);
jens@3
   202
    _dragBit.pickedUp = YES;
jens@3
   203
    [[NSCursor closedHandCursor] push];
jens@3
   204
    
jens@3
   205
    if( placing ) {
jens@3
   206
        if( _oldSuperlayer )
jens@5
   207
            _dragBit.position = [self _convertPointFromWindowToLayer: _dragStartPos];
jens@3
   208
        _dragMoved = YES;
jens@3
   209
        [self _findDropTarget: _dragStartPos];
jens@3
   210
    }
jens@0
   211
}
jens@0
   212
jens@3
   213
jens@0
   214
- (void) mouseDragged: (NSEvent*)ev
jens@0
   215
{
jens@0
   216
    if( _dragBit ) {
jens@0
   217
        // Get the mouse position, and see if we've moved 3 pixels since the mouseDown:
jens@0
   218
        NSPoint pos = ev.locationInWindow;
jens@0
   219
        if( fabs(pos.x-_dragStartPos.x)>=3 || fabs(pos.y-_dragStartPos.y)>=3 )
jens@0
   220
            _dragMoved = YES;
jens@0
   221
        
jens@0
   222
        // Move the _dragBit (without animation -- it's unnecessary and slows down responsiveness):
jens@5
   223
        CGPoint where = [self _convertPointFromWindowToLayer: pos];
jens@0
   224
        where.x += _dragOffset.x;
jens@0
   225
        where.y += _dragOffset.y;
jens@0
   226
        
jens@5
   227
        CGPoint newPos = [_dragBit.superlayer convertPoint: where fromLayer: self.layer];
jens@0
   228
jens@0
   229
        [CATransaction flush];
jens@0
   230
        [CATransaction begin];
jens@0
   231
        [CATransaction setValue:(id)kCFBooleanTrue
jens@0
   232
                         forKey:kCATransactionDisableActions];
jens@0
   233
        _dragBit.position = newPos;
jens@0
   234
        [CATransaction commit];
jens@0
   235
jens@0
   236
        // Find what it's over:
jens@3
   237
        [self _findDropTarget: pos];
jens@3
   238
    }
jens@3
   239
}
jens@3
   240
jens@3
   241
jens@3
   242
- (void) _findDropTarget: (NSPoint)locationInWindow
jens@3
   243
{
jens@3
   244
    locationInWindow.x += _dragOffset.x;
jens@3
   245
    locationInWindow.y += _dragOffset.y;
jens@3
   246
    id<BitHolder> target = (id<BitHolder>) [self hitTestPoint: locationInWindow
jens@3
   247
                                             forLayerMatching: layerIsBitHolder
jens@3
   248
                                                       offset: NULL];
jens@3
   249
    if( target == _oldHolder )
jens@3
   250
        target = nil;
jens@3
   251
    if( target != _dropTarget ) {
jens@3
   252
        [_dropTarget willNotDropBit: _dragBit];
jens@3
   253
        _dropTarget.highlighted = NO;
jens@3
   254
        _dropTarget = nil;
jens@3
   255
    }
jens@3
   256
    if( target ) {
jens@3
   257
        CGPoint targetPos = [(CALayer*)target convertPoint: _dragBit.position
jens@3
   258
                                                 fromLayer: _dragBit.superlayer];
jens@3
   259
        if( [target canDropBit: _dragBit atPoint: targetPos]
jens@3
   260
           && [_game canBit: _dragBit moveFrom: _oldHolder to: target] ) {
jens@3
   261
            _dropTarget = target;
jens@3
   262
            _dropTarget.highlighted = YES;
jens@0
   263
        }
jens@0
   264
    }
jens@0
   265
}
jens@0
   266
jens@3
   267
jens@0
   268
- (void) mouseUp: (NSEvent*)ev
jens@0
   269
{
jens@0
   270
    if( _dragBit ) {
jens@0
   271
        if( _dragMoved ) {
jens@0
   272
            // Update the drag tracking to the final mouse position:
jens@0
   273
            [self mouseDragged: ev];
jens@0
   274
            _dropTarget.highlighted = NO;
jens@0
   275
            _dragBit.pickedUp = NO;
jens@0
   276
jens@0
   277
            // Is the move legal?
jens@0
   278
            if( _dropTarget && [_dropTarget dropBit: _dragBit
jens@0
   279
                                            atPoint: [(CALayer*)_dropTarget convertPoint: _dragBit.position 
jens@0
   280
                                                                            fromLayer: _dragBit.superlayer]] ) {
jens@0
   281
                // Yes, notify the interested parties:
jens@0
   282
                [_oldHolder draggedBit: _dragBit to: _dropTarget];
jens@0
   283
                [_game bit: _dragBit movedFrom: _oldHolder to: _dropTarget];
jens@0
   284
            } else {
jens@0
   285
                // Nope, cancel:
jens@0
   286
                [_dropTarget willNotDropBit: _dragBit];
jens@3
   287
                if( _oldSuperlayer ) {
jens@3
   288
                    ChangeSuperlayer(_dragBit, _oldSuperlayer, _oldLayerIndex);
jens@3
   289
                    _dragBit.position = _oldPos;
jens@3
   290
                    [_oldHolder cancelDragBit: _dragBit];
jens@3
   291
                } else {
jens@3
   292
                    [_dragBit removeFromSuperlayer];
jens@3
   293
                }
jens@0
   294
            }
jens@0
   295
        } else {
jens@0
   296
            // Just a click, without a drag:
jens@0
   297
            _dropTarget.highlighted = NO;
jens@0
   298
            _dragBit.pickedUp = NO;
jens@0
   299
            ChangeSuperlayer(_dragBit, _oldSuperlayer, _oldLayerIndex);
jens@0
   300
            [_oldHolder cancelDragBit: _dragBit];
jens@0
   301
            if( ! [_game clickedBit: _dragBit] )
jens@0
   302
                NSBeep();
jens@0
   303
        }
jens@0
   304
        _dropTarget = nil;
jens@0
   305
        _dragBit = nil;
jens@0
   306
        [NSCursor pop];
jens@0
   307
    }
jens@0
   308
}
jens@0
   309
jens@0
   310
jens@0
   311
#pragma mark -
jens@0
   312
#pragma mark INCOMING DRAGS:
jens@0
   313
jens@0
   314
jens@0
   315
// subroutine to call the target
jens@0
   316
static int tell( id target, SEL selector, id arg, int defaultValue )
jens@0
   317
{
jens@0
   318
    if( target && [target respondsToSelector: selector] )
jens@0
   319
        return (ssize_t) [target performSelector: selector withObject: arg];
jens@0
   320
    else
jens@0
   321
        return defaultValue;
jens@0
   322
}
jens@0
   323
jens@0
   324
jens@0
   325
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
jens@0
   326
{
jens@0
   327
    _viewDropTarget = [self hitTestPoint: [sender draggingLocation]
jens@0
   328
                        forLayerMatching: layerIsDropTarget
jens@0
   329
                                  offset: NULL];
jens@0
   330
    _viewDropOp = _viewDropTarget ?[_viewDropTarget draggingEntered: sender] :NSDragOperationNone;
jens@0
   331
    return _viewDropOp;
jens@0
   332
}
jens@0
   333
jens@0
   334
- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
jens@0
   335
{
jens@0
   336
    CALayer *target = [self hitTestPoint: [sender draggingLocation]
jens@0
   337
                        forLayerMatching: layerIsDropTarget 
jens@0
   338
                                  offset: NULL];
jens@0
   339
    if( target == _viewDropTarget ) {
jens@0
   340
        if( _viewDropTarget )
jens@0
   341
            _viewDropOp = tell(_viewDropTarget,@selector(draggingUpdated:),sender,_viewDropOp);
jens@0
   342
    } else {
jens@0
   343
        tell(_viewDropTarget,@selector(draggingExited:),sender,0);
jens@0
   344
        _viewDropTarget = target;
jens@0
   345
        if( _viewDropTarget )
jens@0
   346
            _viewDropOp = [_viewDropTarget draggingEntered: sender];
jens@0
   347
        else
jens@0
   348
            _viewDropOp = NSDragOperationNone;
jens@0
   349
    }
jens@0
   350
    return _viewDropOp;
jens@0
   351
}
jens@0
   352
jens@0
   353
- (BOOL)wantsPeriodicDraggingUpdates
jens@0
   354
{
jens@0
   355
    return (_viewDropTarget!=nil);
jens@0
   356
}
jens@0
   357
jens@0
   358
- (void)draggingExited:(id <NSDraggingInfo>)sender
jens@0
   359
{
jens@0
   360
    tell(_viewDropTarget,@selector(draggingExited:),sender,0);
jens@0
   361
    _viewDropTarget = nil;
jens@0
   362
}
jens@0
   363
jens@0
   364
- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
jens@0
   365
{
jens@0
   366
    return tell(_viewDropTarget,@selector(prepareForDragOperation:),sender,YES);
jens@0
   367
}
jens@0
   368
jens@0
   369
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
jens@0
   370
{
jens@0
   371
    return [_viewDropTarget performDragOperation: sender];
jens@0
   372
}
jens@0
   373
jens@0
   374
- (void)concludeDragOperation:(id <NSDraggingInfo>)sender
jens@0
   375
{
jens@0
   376
    tell(_viewDropTarget,@selector(concludeDragOperation:),sender,0);
jens@0
   377
}
jens@0
   378
jens@0
   379
- (void)draggingEnded:(id <NSDraggingInfo>)sender
jens@0
   380
{
jens@0
   381
    tell(_viewDropTarget,@selector(draggingEnded:),sender,0);
jens@0
   382
}
jens@0
   383
jens@0
   384
@end