Source/BoardView.m
author Jens Alfke <jens@mooseyard.com>
Tue Mar 11 17:09:50 2008 -0700 (2008-03-11)
changeset 4 d781b00f3ed4
parent 0 e9f7ba4718e1
child 5 3ba1f29595c7
permissions -rw-r--r--
Text, playing cards, and Klondike solitaire all work on iPhone now. (Regression: Klondike UI layout has changed, and is awkward on Mac now. Need to special case that.)
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@0
   107
// Hit-testing callbacks (to identify which layers caller is interested in):
jens@0
   108
typedef BOOL (*LayerMatchCallback)(CALayer*);
jens@0
   109
jens@0
   110
static BOOL layerIsBit( CALayer* layer )        {return [layer isKindOfClass: [Bit class]];}
jens@0
   111
static BOOL layerIsBitHolder( CALayer* layer )  {return [layer conformsToProtocol: @protocol(BitHolder)];}
jens@0
   112
static BOOL layerIsDropTarget( CALayer* layer ) {return [layer respondsToSelector: @selector(draggingEntered:)];}
jens@0
   113
jens@0
   114
jens@0
   115
/** Locates the layer at a given point in window coords.
jens@0
   116
    If the leaf layer doesn't pass the layer-match callback, the nearest ancestor that does is returned.
jens@0
   117
    If outOffset is provided, the point's position relative to the layer is stored into it. */
jens@0
   118
- (CALayer*) hitTestPoint: (NSPoint)locationInWindow
jens@0
   119
         forLayerMatching: (LayerMatchCallback)match
jens@0
   120
                   offset: (CGPoint*)outOffset
jens@0
   121
{
jens@0
   122
    CGPoint where = NSPointToCGPoint([self convertPoint: locationInWindow fromView: nil]);
jens@0
   123
    where = [_gameboard convertPoint: where fromLayer: self.layer];
jens@0
   124
    CALayer *layer = [_gameboard hitTest: where];
jens@0
   125
    while( layer ) {
jens@0
   126
        if( match(layer) ) {
jens@0
   127
            CGPoint bitPos = [self.layer convertPoint: layer.position 
jens@0
   128
                              fromLayer: layer.superlayer];
jens@0
   129
            if( outOffset )
jens@0
   130
                *outOffset = CGPointMake( bitPos.x-where.x, bitPos.y-where.y);
jens@0
   131
            return layer;
jens@0
   132
        } else
jens@0
   133
            layer = layer.superlayer;
jens@0
   134
    }
jens@0
   135
    return nil;
jens@0
   136
}
jens@0
   137
jens@0
   138
jens@0
   139
#pragma mark -
jens@0
   140
#pragma mark MOUSE CLICKS & DRAGS:
jens@0
   141
jens@0
   142
jens@0
   143
- (void) mouseDown: (NSEvent*)ev
jens@0
   144
{
jens@3
   145
    BOOL placing = NO;
jens@0
   146
    _dragStartPos = ev.locationInWindow;
jens@0
   147
    _dragBit = (Bit*) [self hitTestPoint: _dragStartPos
jens@0
   148
                        forLayerMatching: layerIsBit 
jens@0
   149
                                  offset: &_dragOffset];
jens@3
   150
    
jens@3
   151
    if( ! _dragBit ) {
jens@3
   152
        // If no bit was clicked, see if it's a BitHolder the game will let the user add a Bit to:
jens@3
   153
        id<BitHolder> holder = (id<BitHolder>) [self hitTestPoint: _dragStartPos
jens@3
   154
                                                 forLayerMatching: layerIsBitHolder
jens@3
   155
                                                           offset: NULL];
jens@3
   156
        if( holder ) {
jens@3
   157
            _dragBit = [_game bitToPlaceInHolder: holder];
jens@3
   158
            if( _dragBit ) {
jens@3
   159
                _dragOffset.x = _dragOffset.y = 0;
jens@3
   160
                if( _dragBit.superlayer==nil )
jens@3
   161
                    _dragBit.position = NSPointToCGPoint([self convertPoint: _dragStartPos fromView: nil]);
jens@3
   162
                placing = YES;
jens@3
   163
            }
jens@3
   164
        }
jens@3
   165
    }
jens@3
   166
    
jens@3
   167
    if( ! _dragBit ) {
jens@3
   168
        Beep();
jens@3
   169
        return;
jens@3
   170
    }
jens@3
   171
    
jens@3
   172
    // Clicked on a Bit:
jens@3
   173
    _dragMoved = NO;
jens@3
   174
    _dropTarget = nil;
jens@3
   175
    _oldHolder = _dragBit.holder;
jens@3
   176
    // Ask holder's and game's permission before dragging:
jens@3
   177
    if( _oldHolder ) {
jens@3
   178
        _dragBit = [_oldHolder canDragBit: _dragBit];
jens@0
   179
        if( _dragBit && ! [_game canBit: _dragBit moveFrom: _oldHolder] ) {
jens@0
   180
            [_oldHolder cancelDragBit: _dragBit];
jens@0
   181
            _dragBit = nil;
jens@0
   182
        }
jens@0
   183
        if( ! _dragBit ) {
jens@0
   184
            _oldHolder = nil;
jens@0
   185
            NSBeep();
jens@0
   186
            return;
jens@0
   187
        }
jens@3
   188
    }
jens@3
   189
    
jens@3
   190
    // Start dragging:
jens@3
   191
    _oldSuperlayer = _dragBit.superlayer;
jens@3
   192
    _oldLayerIndex = [_oldSuperlayer.sublayers indexOfObjectIdenticalTo: _dragBit];
jens@3
   193
    _oldPos = _dragBit.position;
jens@3
   194
    ChangeSuperlayer(_dragBit, self.layer, self.layer.sublayers.count);
jens@3
   195
    _dragBit.pickedUp = YES;
jens@3
   196
    [[NSCursor closedHandCursor] push];
jens@3
   197
    
jens@3
   198
    if( placing ) {
jens@3
   199
        if( _oldSuperlayer )
jens@3
   200
            _dragBit.position = NSPointToCGPoint([self convertPoint: _dragStartPos fromView: nil]);
jens@3
   201
        _dragMoved = YES;
jens@3
   202
        [self _findDropTarget: _dragStartPos];
jens@3
   203
    }
jens@0
   204
}
jens@0
   205
jens@3
   206
jens@0
   207
- (void) mouseDragged: (NSEvent*)ev
jens@0
   208
{
jens@0
   209
    if( _dragBit ) {
jens@0
   210
        // Get the mouse position, and see if we've moved 3 pixels since the mouseDown:
jens@0
   211
        NSPoint pos = ev.locationInWindow;
jens@0
   212
        if( fabs(pos.x-_dragStartPos.x)>=3 || fabs(pos.y-_dragStartPos.y)>=3 )
jens@0
   213
            _dragMoved = YES;
jens@0
   214
        
jens@0
   215
        // Move the _dragBit (without animation -- it's unnecessary and slows down responsiveness):
jens@0
   216
        NSPoint where = [self convertPoint: pos fromView: nil];
jens@0
   217
        where.x += _dragOffset.x;
jens@0
   218
        where.y += _dragOffset.y;
jens@0
   219
        
jens@0
   220
        CGPoint newPos = [_dragBit.superlayer convertPoint: NSPointToCGPoint(where) 
jens@0
   221
                                                 fromLayer: self.layer];
jens@0
   222
jens@0
   223
        [CATransaction flush];
jens@0
   224
        [CATransaction begin];
jens@0
   225
        [CATransaction setValue:(id)kCFBooleanTrue
jens@0
   226
                         forKey:kCATransactionDisableActions];
jens@0
   227
        _dragBit.position = newPos;
jens@0
   228
        [CATransaction commit];
jens@0
   229
jens@0
   230
        // Find what it's over:
jens@3
   231
        [self _findDropTarget: pos];
jens@3
   232
    }
jens@3
   233
}
jens@3
   234
jens@3
   235
jens@3
   236
- (void) _findDropTarget: (NSPoint)locationInWindow
jens@3
   237
{
jens@3
   238
    locationInWindow.x += _dragOffset.x;
jens@3
   239
    locationInWindow.y += _dragOffset.y;
jens@3
   240
    id<BitHolder> target = (id<BitHolder>) [self hitTestPoint: locationInWindow
jens@3
   241
                                             forLayerMatching: layerIsBitHolder
jens@3
   242
                                                       offset: NULL];
jens@3
   243
    if( target == _oldHolder )
jens@3
   244
        target = nil;
jens@3
   245
    if( target != _dropTarget ) {
jens@3
   246
        [_dropTarget willNotDropBit: _dragBit];
jens@3
   247
        _dropTarget.highlighted = NO;
jens@3
   248
        _dropTarget = nil;
jens@3
   249
    }
jens@3
   250
    if( target ) {
jens@3
   251
        CGPoint targetPos = [(CALayer*)target convertPoint: _dragBit.position
jens@3
   252
                                                 fromLayer: _dragBit.superlayer];
jens@3
   253
        if( [target canDropBit: _dragBit atPoint: targetPos]
jens@3
   254
           && [_game canBit: _dragBit moveFrom: _oldHolder to: target] ) {
jens@3
   255
            _dropTarget = target;
jens@3
   256
            _dropTarget.highlighted = YES;
jens@0
   257
        }
jens@0
   258
    }
jens@0
   259
}
jens@0
   260
jens@3
   261
jens@0
   262
- (void) mouseUp: (NSEvent*)ev
jens@0
   263
{
jens@0
   264
    if( _dragBit ) {
jens@0
   265
        if( _dragMoved ) {
jens@0
   266
            // Update the drag tracking to the final mouse position:
jens@0
   267
            [self mouseDragged: ev];
jens@0
   268
            _dropTarget.highlighted = NO;
jens@0
   269
            _dragBit.pickedUp = NO;
jens@0
   270
jens@0
   271
            // Is the move legal?
jens@0
   272
            if( _dropTarget && [_dropTarget dropBit: _dragBit
jens@0
   273
                                            atPoint: [(CALayer*)_dropTarget convertPoint: _dragBit.position 
jens@0
   274
                                                                            fromLayer: _dragBit.superlayer]] ) {
jens@0
   275
                // Yes, notify the interested parties:
jens@0
   276
                [_oldHolder draggedBit: _dragBit to: _dropTarget];
jens@0
   277
                [_game bit: _dragBit movedFrom: _oldHolder to: _dropTarget];
jens@0
   278
            } else {
jens@0
   279
                // Nope, cancel:
jens@0
   280
                [_dropTarget willNotDropBit: _dragBit];
jens@3
   281
                if( _oldSuperlayer ) {
jens@3
   282
                    ChangeSuperlayer(_dragBit, _oldSuperlayer, _oldLayerIndex);
jens@3
   283
                    _dragBit.position = _oldPos;
jens@3
   284
                    [_oldHolder cancelDragBit: _dragBit];
jens@3
   285
                } else {
jens@3
   286
                    [_dragBit removeFromSuperlayer];
jens@3
   287
                }
jens@0
   288
            }
jens@0
   289
        } else {
jens@0
   290
            // Just a click, without a drag:
jens@0
   291
            _dropTarget.highlighted = NO;
jens@0
   292
            _dragBit.pickedUp = NO;
jens@0
   293
            ChangeSuperlayer(_dragBit, _oldSuperlayer, _oldLayerIndex);
jens@0
   294
            [_oldHolder cancelDragBit: _dragBit];
jens@0
   295
            if( ! [_game clickedBit: _dragBit] )
jens@0
   296
                NSBeep();
jens@0
   297
        }
jens@0
   298
        _dropTarget = nil;
jens@0
   299
        _dragBit = nil;
jens@0
   300
        [NSCursor pop];
jens@0
   301
    }
jens@0
   302
}
jens@0
   303
jens@0
   304
jens@0
   305
#pragma mark -
jens@0
   306
#pragma mark INCOMING DRAGS:
jens@0
   307
jens@0
   308
jens@0
   309
// subroutine to call the target
jens@0
   310
static int tell( id target, SEL selector, id arg, int defaultValue )
jens@0
   311
{
jens@0
   312
    if( target && [target respondsToSelector: selector] )
jens@0
   313
        return (ssize_t) [target performSelector: selector withObject: arg];
jens@0
   314
    else
jens@0
   315
        return defaultValue;
jens@0
   316
}
jens@0
   317
jens@0
   318
jens@0
   319
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
jens@0
   320
{
jens@0
   321
    _viewDropTarget = [self hitTestPoint: [sender draggingLocation]
jens@0
   322
                        forLayerMatching: layerIsDropTarget
jens@0
   323
                                  offset: NULL];
jens@0
   324
    _viewDropOp = _viewDropTarget ?[_viewDropTarget draggingEntered: sender] :NSDragOperationNone;
jens@0
   325
    return _viewDropOp;
jens@0
   326
}
jens@0
   327
jens@0
   328
- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
jens@0
   329
{
jens@0
   330
    CALayer *target = [self hitTestPoint: [sender draggingLocation]
jens@0
   331
                        forLayerMatching: layerIsDropTarget 
jens@0
   332
                                  offset: NULL];
jens@0
   333
    if( target == _viewDropTarget ) {
jens@0
   334
        if( _viewDropTarget )
jens@0
   335
            _viewDropOp = tell(_viewDropTarget,@selector(draggingUpdated:),sender,_viewDropOp);
jens@0
   336
    } else {
jens@0
   337
        tell(_viewDropTarget,@selector(draggingExited:),sender,0);
jens@0
   338
        _viewDropTarget = target;
jens@0
   339
        if( _viewDropTarget )
jens@0
   340
            _viewDropOp = [_viewDropTarget draggingEntered: sender];
jens@0
   341
        else
jens@0
   342
            _viewDropOp = NSDragOperationNone;
jens@0
   343
    }
jens@0
   344
    return _viewDropOp;
jens@0
   345
}
jens@0
   346
jens@0
   347
- (BOOL)wantsPeriodicDraggingUpdates
jens@0
   348
{
jens@0
   349
    return (_viewDropTarget!=nil);
jens@0
   350
}
jens@0
   351
jens@0
   352
- (void)draggingExited:(id <NSDraggingInfo>)sender
jens@0
   353
{
jens@0
   354
    tell(_viewDropTarget,@selector(draggingExited:),sender,0);
jens@0
   355
    _viewDropTarget = nil;
jens@0
   356
}
jens@0
   357
jens@0
   358
- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
jens@0
   359
{
jens@0
   360
    return tell(_viewDropTarget,@selector(prepareForDragOperation:),sender,YES);
jens@0
   361
}
jens@0
   362
jens@0
   363
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
jens@0
   364
{
jens@0
   365
    return [_viewDropTarget performDragOperation: sender];
jens@0
   366
}
jens@0
   367
jens@0
   368
- (void)concludeDragOperation:(id <NSDraggingInfo>)sender
jens@0
   369
{
jens@0
   370
    tell(_viewDropTarget,@selector(concludeDragOperation:),sender,0);
jens@0
   371
}
jens@0
   372
jens@0
   373
- (void)draggingEnded:(id <NSDraggingInfo>)sender
jens@0
   374
{
jens@0
   375
    tell(_viewDropTarget,@selector(draggingEnded:),sender,0);
jens@0
   376
}
jens@0
   377
jens@0
   378
@end