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