jens@0: /* This code is based on Apple's "GeekGameBoard" sample code, version 1.0. jens@0: http://developer.apple.com/samplecode/GeekGameBoard/ jens@0: Copyright © 2007 Apple Inc. Copyright © 2008 Jens Alfke. All Rights Reserved. jens@0: jens@0: Redistribution and use in source and binary forms, with or without modification, are permitted jens@0: provided that the following conditions are met: jens@0: jens@0: * Redistributions of source code must retain the above copyright notice, this list of conditions jens@0: and the following disclaimer. jens@0: * Redistributions in binary form must reproduce the above copyright notice, this list of jens@0: conditions and the following disclaimer in the documentation and/or other materials provided jens@0: with the distribution. jens@0: jens@0: THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR jens@0: IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND jens@0: FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI- jens@0: BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES jens@0: (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR jens@0: PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN jens@0: CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF jens@0: THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. jens@0: */ jens@0: #import "BoardView.h" jens@0: #import "Bit.h" jens@0: #import "BitHolder.h" jens@22: #import "Game+Protected.h" jens@12: #import "Turn.h" jens@10: #import "Player.h" jens@0: #import "QuartzUtils.h" jens@0: #import "GGBUtils.h" jens@0: jens@0: jens@22: #define kMaxPerspective 0.965 // 55 degrees jens@22: jens@22: jens@3: @interface BoardView () jens@3: - (void) _findDropTarget: (NSPoint)pos; jens@3: @end jens@3: jens@3: jens@0: @implementation BoardView jens@0: jens@0: jens@22: @synthesize table=_table, gameBoardInset=_gameBoardInset; jens@0: jens@0: jens@0: - (void) dealloc jens@0: { jens@0: [_game release]; jens@0: [super dealloc]; jens@0: } jens@0: jens@0: jens@22: - (void) _applyPerspective jens@22: { jens@22: CATransform3D t; jens@22: if( fabs(_perspective) >= M_PI/180 ) { jens@22: CGSize size = self.layer.bounds.size; jens@22: t = CATransform3DMakeTranslation(-size.width/2, -size.height/4, 0); jens@22: t = CATransform3DConcat(t, CATransform3DMakeRotation(-_perspective, 1,0,0)); jens@22: jens@22: CATransform3D pers = CATransform3DIdentity; jens@22: pers.m34 = 1.0/-800; jens@22: t = CATransform3DConcat(t, pers); jens@22: t = CATransform3DConcat(t, CATransform3DMakeTranslation(size.width/2, jens@22: size.height*(0.25 + 0.05*sin(2*_perspective)), jens@22: 0)); jens@22: self.layer.borderWidth = 3; jens@22: } else { jens@22: t = CATransform3DIdentity; jens@22: self.layer.borderWidth = 0; jens@22: } jens@22: self.layer.transform = t; jens@22: } jens@22: jens@22: - (CGFloat) perspective {return _perspective;} jens@22: jens@22: - (void) setPerspective: (CGFloat)p jens@22: { jens@22: p = MAX(0.0, MIN(kMaxPerspective, p)); jens@22: if( p != _perspective ) { jens@22: _perspective = p; jens@22: [self _applyPerspective]; jens@22: _game.tablePerspectiveAngle = p; jens@22: } jens@22: } jens@22: jens@22: jens@10: - (void) _removeGameBoard jens@10: { jens@16: if( _table ) { jens@16: RemoveImmediately(_table); jens@16: _table = nil; jens@10: } jens@10: } jens@10: jens@10: - (void) createGameBoard jens@10: { jens@10: [self _removeGameBoard]; jens@16: _table = [[CALayer alloc] init]; jens@16: _table.frame = [self gameBoardFrame]; jens@16: _table.autoresizingMask = kCALayerMinXMargin | kCALayerMaxXMargin | kCALayerMinYMargin | kCALayerMaxYMargin; jens@22: jens@10: // Tell the game to set up the board: jens@16: _game.table = _table; jens@10: jens@16: [self.layer addSublayer: _table]; jens@16: [_table release]; jens@10: } jens@10: jens@10: jens@10: - (Game*) game jens@10: { jens@10: return _game; jens@10: } jens@10: jens@10: - (void) setGame: (Game*)game jens@10: { jens@10: if( game!=_game ) { jens@16: _game.table = nil; jens@10: setObj(&_game,game); jens@10: [self createGameBoard]; jens@10: } jens@10: } jens@10: jens@0: - (void) startGameNamed: (NSString*)gameClassName jens@0: { jens@10: Class gameClass = NSClassFromString(gameClassName); jens@10: Game *game = [[gameClass alloc] init]; jens@10: if( game ) { jens@10: self.game = game; jens@10: [game release]; jens@0: } jens@10: } jens@10: jens@10: jens@0: - (CGRect) gameBoardFrame jens@0: { jens@22: return CGRectInset(self.layer.bounds, _gameBoardInset.width,_gameBoardInset.height); jens@0: } jens@0: jens@0: jens@0: - (void)resetCursorRects jens@0: { jens@0: [super resetCursorRects]; jens@15: if( _game.okToMove ) jens@10: [self addCursorRect: self.bounds cursor: [NSCursor openHandCursor]]; jens@0: } jens@0: jens@0: jens@18: - (NSView*) fullScreenView jens@18: { jens@18: return _fullScreenView ?: self; jens@18: } jens@18: jens@0: - (IBAction) enterFullScreen: (id)sender jens@0: { jens@18: //[self _removeGameBoard]; jens@18: if( self.fullScreenView.isInFullScreenMode ) { jens@18: [self.fullScreenView exitFullScreenModeWithOptions: nil]; jens@0: } else { jens@18: [self.fullScreenView enterFullScreenMode: self.window.screen jens@18: withOptions: nil]; jens@0: } jens@18: //[self createGameBoard]; jens@10: } jens@10: jens@10: jens@10: - (void)viewWillStartLiveResize jens@10: { jens@10: [super viewWillStartLiveResize]; jens@10: _oldSize = self.frame.size; jens@10: } jens@10: jens@10: - (void)setFrameSize:(NSSize)newSize jens@10: { jens@10: [super setFrameSize: newSize]; jens@10: if( _oldSize.width > 0.0f ) { jens@16: CGAffineTransform xform = _table.affineTransform; jens@10: xform.a = xform.d = MIN(newSize.width,newSize.height)/MIN(_oldSize.width,_oldSize.height); jens@18: BeginDisableAnimations(); jens@22: [self _applyPerspective]; jens@16: _table.affineTransform = xform; jens@18: EndDisableAnimations(); jens@10: } else jens@10: [self createGameBoard]; jens@10: } jens@10: jens@10: - (void)viewDidEndLiveResize jens@10: { jens@10: [super viewDidEndLiveResize]; jens@10: _oldSize.width = _oldSize.height = 0.0f; jens@10: [self createGameBoard]; jens@0: } jens@0: jens@0: jens@0: #pragma mark - jens@0: #pragma mark KEY EVENTS: jens@0: jens@0: jens@18: - (BOOL) performKeyEquivalent: (NSEvent*)ev jens@0: { jens@18: if( [ev.charactersIgnoringModifiers hasPrefix: @"\033"] ) { // Esc key jens@18: if( self.fullScreenView.isInFullScreenMode ) { jens@18: [self performSelector: @selector(enterFullScreen:) withObject: nil afterDelay: 0.0]; jens@18: // without the delayed-perform, NSWindow crashes right after this method returns! jens@18: return YES; jens@18: } jens@0: } jens@18: return NO; jens@0: } jens@0: jens@0: jens@0: #pragma mark - jens@0: #pragma mark HIT-TESTING: jens@0: jens@0: jens@5: /** Converts a point from window coords, to this view's root layer's coords. */ jens@5: - (CGPoint) _convertPointFromWindowToLayer: (NSPoint)locationInWindow jens@5: { jens@5: NSPoint where = [self convertPoint: locationInWindow fromView: nil]; // convert to view coords jens@22: where = [self convertPointToBase: where]; // then to layer base coords jens@22: return [self.layer convertPoint: NSPointToCGPoint(where) // then to transformed layer coords jens@22: fromLayer: self.layer.superlayer]; jens@5: } jens@5: jens@5: jens@0: // Hit-testing callbacks (to identify which layers caller is interested in): jens@0: typedef BOOL (*LayerMatchCallback)(CALayer*); jens@0: jens@0: static BOOL layerIsBit( CALayer* layer ) {return [layer isKindOfClass: [Bit class]];} jens@0: static BOOL layerIsBitHolder( CALayer* layer ) {return [layer conformsToProtocol: @protocol(BitHolder)];} jens@0: static BOOL layerIsDropTarget( CALayer* layer ) {return [layer respondsToSelector: @selector(draggingEntered:)];} jens@0: jens@0: jens@0: /** Locates the layer at a given point in window coords. jens@0: If the leaf layer doesn't pass the layer-match callback, the nearest ancestor that does is returned. jens@0: If outOffset is provided, the point's position relative to the layer is stored into it. */ jens@0: - (CALayer*) hitTestPoint: (NSPoint)locationInWindow jens@0: forLayerMatching: (LayerMatchCallback)match jens@0: offset: (CGPoint*)outOffset jens@0: { jens@5: CGPoint where = [self _convertPointFromWindowToLayer: locationInWindow ]; jens@16: CALayer *layer = [_table hitTest: where]; jens@0: while( layer ) { jens@0: if( match(layer) ) { jens@0: CGPoint bitPos = [self.layer convertPoint: layer.position jens@0: fromLayer: layer.superlayer]; jens@0: if( outOffset ) jens@0: *outOffset = CGPointMake( bitPos.x-where.x, bitPos.y-where.y); jens@0: return layer; jens@0: } else jens@0: layer = layer.superlayer; jens@0: } jens@0: return nil; jens@0: } jens@0: jens@0: jens@0: #pragma mark - jens@0: #pragma mark MOUSE CLICKS & DRAGS: jens@0: jens@0: jens@0: - (void) mouseDown: (NSEvent*)ev jens@0: { jens@15: if( ! _game.okToMove ) { jens@10: NSBeep(); jens@10: return; jens@10: } jens@10: jens@3: BOOL placing = NO; jens@0: _dragStartPos = ev.locationInWindow; jens@0: _dragBit = (Bit*) [self hitTestPoint: _dragStartPos jens@0: forLayerMatching: layerIsBit jens@0: offset: &_dragOffset]; jens@3: jens@3: if( ! _dragBit ) { jens@3: // If no bit was clicked, see if it's a BitHolder the game will let the user add a Bit to: jens@3: id holder = (id) [self hitTestPoint: _dragStartPos jens@3: forLayerMatching: layerIsBitHolder jens@3: offset: NULL]; jens@3: if( holder ) { jens@3: _dragBit = [_game bitToPlaceInHolder: holder]; jens@3: if( _dragBit ) { jens@3: _dragOffset.x = _dragOffset.y = 0; jens@3: if( _dragBit.superlayer==nil ) jens@5: _dragBit.position = [self _convertPointFromWindowToLayer: _dragStartPos]; jens@3: placing = YES; jens@3: } jens@3: } jens@3: } jens@3: jens@3: if( ! _dragBit ) { jens@3: Beep(); jens@3: return; jens@3: } jens@3: jens@3: // Clicked on a Bit: jens@3: _dragMoved = NO; jens@3: _dropTarget = nil; jens@3: _oldHolder = _dragBit.holder; jens@3: // Ask holder's and game's permission before dragging: jens@3: if( _oldHolder ) { jens@3: _dragBit = [_oldHolder canDragBit: _dragBit]; jens@0: if( _dragBit && ! [_game canBit: _dragBit moveFrom: _oldHolder] ) { jens@0: [_oldHolder cancelDragBit: _dragBit]; jens@0: _dragBit = nil; jens@0: } jens@0: if( ! _dragBit ) { jens@0: _oldHolder = nil; jens@0: NSBeep(); jens@0: return; jens@0: } jens@3: } jens@3: jens@3: // Start dragging: jens@3: _oldSuperlayer = _dragBit.superlayer; jens@3: _oldLayerIndex = [_oldSuperlayer.sublayers indexOfObjectIdenticalTo: _dragBit]; jens@3: _oldPos = _dragBit.position; jens@3: ChangeSuperlayer(_dragBit, self.layer, self.layer.sublayers.count); jens@3: _dragBit.pickedUp = YES; jens@3: [[NSCursor closedHandCursor] push]; jens@3: jens@3: if( placing ) { jens@3: if( _oldSuperlayer ) jens@5: _dragBit.position = [self _convertPointFromWindowToLayer: _dragStartPos]; jens@3: _dragMoved = YES; jens@3: [self _findDropTarget: _dragStartPos]; jens@3: } jens@0: } jens@0: jens@3: jens@0: - (void) mouseDragged: (NSEvent*)ev jens@0: { jens@0: if( _dragBit ) { jens@0: // Get the mouse position, and see if we've moved 3 pixels since the mouseDown: jens@0: NSPoint pos = ev.locationInWindow; jens@0: if( fabs(pos.x-_dragStartPos.x)>=3 || fabs(pos.y-_dragStartPos.y)>=3 ) jens@0: _dragMoved = YES; jens@0: jens@0: // Move the _dragBit (without animation -- it's unnecessary and slows down responsiveness): jens@5: CGPoint where = [self _convertPointFromWindowToLayer: pos]; jens@0: where.x += _dragOffset.x; jens@0: where.y += _dragOffset.y; jens@0: jens@5: CGPoint newPos = [_dragBit.superlayer convertPoint: where fromLayer: self.layer]; jens@0: jens@0: [CATransaction flush]; jens@0: [CATransaction begin]; jens@0: [CATransaction setValue:(id)kCFBooleanTrue jens@0: forKey:kCATransactionDisableActions]; jens@0: _dragBit.position = newPos; jens@0: [CATransaction commit]; jens@0: jens@0: // Find what it's over: jens@3: [self _findDropTarget: pos]; jens@3: } jens@3: } jens@3: jens@3: jens@3: - (void) _findDropTarget: (NSPoint)locationInWindow jens@3: { jens@3: locationInWindow.x += _dragOffset.x; jens@3: locationInWindow.y += _dragOffset.y; jens@3: id target = (id) [self hitTestPoint: locationInWindow jens@3: forLayerMatching: layerIsBitHolder jens@3: offset: NULL]; jens@3: if( target == _oldHolder ) jens@3: target = nil; jens@3: if( target != _dropTarget ) { jens@3: [_dropTarget willNotDropBit: _dragBit]; jens@3: _dropTarget.highlighted = NO; jens@3: _dropTarget = nil; jens@3: } jens@3: if( target ) { jens@3: CGPoint targetPos = [(CALayer*)target convertPoint: _dragBit.position jens@3: fromLayer: _dragBit.superlayer]; jens@3: if( [target canDropBit: _dragBit atPoint: targetPos] jens@3: && [_game canBit: _dragBit moveFrom: _oldHolder to: target] ) { jens@3: _dropTarget = target; jens@3: _dropTarget.highlighted = YES; jens@0: } jens@0: } jens@0: } jens@0: jens@3: jens@0: - (void) mouseUp: (NSEvent*)ev jens@0: { jens@0: if( _dragBit ) { jens@0: if( _dragMoved ) { jens@0: // Update the drag tracking to the final mouse position: jens@0: [self mouseDragged: ev]; jens@0: _dropTarget.highlighted = NO; jens@0: _dragBit.pickedUp = NO; jens@0: jens@0: // Is the move legal? jens@0: if( _dropTarget && [_dropTarget dropBit: _dragBit jens@0: atPoint: [(CALayer*)_dropTarget convertPoint: _dragBit.position jens@0: fromLayer: _dragBit.superlayer]] ) { jens@0: // Yes, notify the interested parties: jens@0: [_oldHolder draggedBit: _dragBit to: _dropTarget]; jens@0: [_game bit: _dragBit movedFrom: _oldHolder to: _dropTarget]; jens@0: } else { jens@0: // Nope, cancel: jens@0: [_dropTarget willNotDropBit: _dragBit]; jens@3: if( _oldSuperlayer ) { jens@3: ChangeSuperlayer(_dragBit, _oldSuperlayer, _oldLayerIndex); jens@3: _dragBit.position = _oldPos; jens@3: [_oldHolder cancelDragBit: _dragBit]; jens@3: } else { jens@3: [_dragBit removeFromSuperlayer]; jens@3: } jens@0: } jens@0: } else { jens@0: // Just a click, without a drag: jens@0: _dropTarget.highlighted = NO; jens@0: _dragBit.pickedUp = NO; jens@0: ChangeSuperlayer(_dragBit, _oldSuperlayer, _oldLayerIndex); jens@0: [_oldHolder cancelDragBit: _dragBit]; jens@0: if( ! [_game clickedBit: _dragBit] ) jens@0: NSBeep(); jens@0: } jens@9: jens@0: _dropTarget = nil; jens@0: _dragBit = nil; jens@0: [NSCursor pop]; jens@0: } jens@0: } jens@0: jens@0: jens@22: - (void)scrollWheel:(NSEvent *)e jens@22: { jens@22: self.perspective += e.deltaY * M_PI/180; jens@22: //Log(@"Perspective = %2.0f degrees (%5.3f radians)", self.perspective*180/M_PI, self.perspective); jens@22: } jens@22: jens@22: jens@0: #pragma mark - jens@0: #pragma mark INCOMING DRAGS: jens@0: jens@0: jens@0: // subroutine to call the target jens@0: static int tell( id target, SEL selector, id arg, int defaultValue ) jens@0: { jens@0: if( target && [target respondsToSelector: selector] ) jens@0: return (ssize_t) [target performSelector: selector withObject: arg]; jens@0: else jens@0: return defaultValue; jens@0: } jens@0: jens@0: jens@0: - (NSDragOperation)draggingEntered:(id )sender jens@0: { jens@0: _viewDropTarget = [self hitTestPoint: [sender draggingLocation] jens@0: forLayerMatching: layerIsDropTarget jens@0: offset: NULL]; jens@0: _viewDropOp = _viewDropTarget ?[_viewDropTarget draggingEntered: sender] :NSDragOperationNone; jens@0: return _viewDropOp; jens@0: } jens@0: jens@0: - (NSDragOperation)draggingUpdated:(id )sender jens@0: { jens@0: CALayer *target = [self hitTestPoint: [sender draggingLocation] jens@0: forLayerMatching: layerIsDropTarget jens@0: offset: NULL]; jens@0: if( target == _viewDropTarget ) { jens@0: if( _viewDropTarget ) jens@0: _viewDropOp = tell(_viewDropTarget,@selector(draggingUpdated:),sender,_viewDropOp); jens@0: } else { jens@0: tell(_viewDropTarget,@selector(draggingExited:),sender,0); jens@0: _viewDropTarget = target; jens@0: if( _viewDropTarget ) jens@0: _viewDropOp = [_viewDropTarget draggingEntered: sender]; jens@0: else jens@0: _viewDropOp = NSDragOperationNone; jens@0: } jens@0: return _viewDropOp; jens@0: } jens@0: jens@0: - (BOOL)wantsPeriodicDraggingUpdates jens@0: { jens@0: return (_viewDropTarget!=nil); jens@0: } jens@0: jens@0: - (void)draggingExited:(id )sender jens@0: { jens@0: tell(_viewDropTarget,@selector(draggingExited:),sender,0); jens@0: _viewDropTarget = nil; jens@0: } jens@0: jens@0: - (BOOL)prepareForDragOperation:(id )sender jens@0: { jens@0: return tell(_viewDropTarget,@selector(prepareForDragOperation:),sender,YES); jens@0: } jens@0: jens@0: - (BOOL)performDragOperation:(id )sender jens@0: { jens@0: return [_viewDropTarget performDragOperation: sender]; jens@0: } jens@0: jens@0: - (void)concludeDragOperation:(id )sender jens@0: { jens@0: tell(_viewDropTarget,@selector(concludeDragOperation:),sender,0); jens@0: } jens@0: jens@0: - (void)draggingEnded:(id )sender jens@0: { jens@0: tell(_viewDropTarget,@selector(draggingEnded:),sender,0); jens@0: } jens@0: jens@0: @end