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