This is CardPileView.m in view mode; [Download] [Up]
/* indent:4 tabsize:8 font:fixed-width */ #import <Solitaire/CardSet.h> #import "CardPileView.h" /*-------------------------------------------------------------------------- | | CardPileView Globals | | CardPilePBoardType is a private pasteboard type used by the | CardPileView class. | \---------------------------------------------------------------------------*/ static NSString* CardPilePBoardType; @implementation CardPileView /*" Instances of this class provide a visual representation of a pile of cards. CardPileView uses CardPile as the underlying representation, but adds a visual aspect and user interaction. Users can click on CardPileViews and drag cards from one view to another. The decisions on when this is allowed and what happens as a result are left to the CardPileView's delegate. Dragging cards between applications is disabled by default. It can be enabled by subclassing and overriding #draggingSourceOperationMaskForLocal:. #{Notifying the CardPileView Object's Delegate} The logic of a card game is primarily implemented through the delegate mechanism. The delegate has the option of implementing the following methods: clickedCard:in: doubleClickedCard:in: draggedPile:from: canAcceptPile:from:in: acceptPile:in: removedPile:from: getOffset:: Knowing the sequence in which these delegate messages are sent is critical in using the CardSet objects to create card games. If there is no delegate (or the delegate does not implement the above methods) the cards on a pile may not be dragged, flipped, or have cards dropped on them. When the user single clicks on a CardPileView, the message #{clickedCard:}%{aCard} #{in:}%{aCardPileView} is sent to the delegate. Note that the parameter %{aCard} will be nil if the pile was empty, or if the user clicked in an area of the pile where there is no Card displayed. This message is sent even if the user keeps holding down the mouse button (ie. dragging) after clicking. If the user holds down the mouse button, and is pointing at a Card, the delegate receives the message #{draggedPile:}%{aCardPile} #{from:}%{aCardPileView}. %{aCardPile} is a temporary pile containing the Card that the user clicked on. The delegate may add additional cards to this pile, if desired. This would typically be done in games where the user can click on a card part-way down a deck, and drag that card and all the cards above it. The delegate is responsible for adding the cards above the clicked card to %{aCardPile}. If #draggedPile:from: returns YES, CardPileView performs the dragging animation with the cards on the temporary CardPile. If the user releases the dragged pile on any CardPileView (other than the source pile), that CardPileView's delegate receives the #{canAcceptPile:}%{dropPile} #{from:}%{sender} #{in:}%{cardPileView} message, where %dropPile is the pile that wants to be dropped, %cardPileView is the pile receiving the cards, and %sender is the CardPileView that the cards were dragged from. If this method returns NO, the dragged cards return to the source pile. If a CardPileView agrees to accept some dragged cards, they are automatically added to that pile. Then the delegate receives the #{acceptPile:}%{dropPile} #{in:}%{self} message, which lets the pile know which cards were added to it. Note that the dropped cards are, at this point, on both the original pile (where the cards were dragged from), and on the pile that they have been dropped on. The cards are now removed from the original CardPileView, and its delegate receives the #{removePile:}%{aCardPile} #{from:}%{aCardPileView} message. The #getOffset:: message allows the delegate to control the amount of stagger that the deck has when it draws. "*/ + (void) initialize /*" Initializes our class object. We set our CardPileBPBoardType. "*/ { if (self == [CardPileView class]) { CardPilePBoardType = @"CardPilePBoardType"; } } - initWithFrame:(NSRect)theFrame /*" Creates an empty CardPile object to be managed by the CardPileView. If theFrame is wider and/or taller than the size required by a single Card object, then the pile will automatically be drawn with an offset in that direction. Returns self. "*/ { /* Default background color is felt green or light gray */ NSColor * aColor = [NSColor colorWithCalibratedRed:0.0 green:51.0/255.0 blue:34.0/255.0 alpha:1.0]; [super initWithFrame:theFrame]; cardPile = [[CardPile allocWithZone:[self zone]] init]; [self setBackgroundColor:aColor]; backgroundGray = NSLightGray; [self setDrawOutline:YES]; [self registerForDraggedTypes:[NSArray arrayWithObject:CardPilePBoardType]]; // Show all the cards we have by default. [self setMaxVisibleCards:CS_SHOWALL]; /*---------------------------------------------------------------------- | | If the view is large enough to allow cards to be | stacked vertically or horizontally, initialize the | offsets appropriately to give an apparent "height" | to the pile | \----------------------------------------------------------------------*/ if ([cardPile cardSize] == CS_SMALL) { if (theFrame.size.width > CS_SMALLCARDWIDTH) { xOffset = 0.2; } if (theFrame.size.height > CS_SMALLCARDHEIGHT) { yOffset = 0.2; } } else { if (theFrame.size.width > CS_CARDWIDTH) { xOffset = 0.5; } if (theFrame.size.height > CS_CARDHEIGHT) { yOffset = 0.5; } } return self; } //Kluge method below added because of problem with prepareForDragOperation:sender on NT - (CardPileView *) currentCardView { return currentCardView; } //Kluge method below added because of problem with prepareForDragOperation:sender on NT - (void) setCurrentCardView: (CardPileView *) theView { if (currentCardView) [currentCardView autorelease]; currentCardView = [theView retain]; } - (void) setMaxVisibleCards:(int)cardsVisible /*" Sets the maximum number of cards visible at any one time. An example of this would be when you are playing 3 card Klondike and you can see the edges of three cards at one time. The default is CS_SHOWALL. "*/ { cardsVisible = (cardsVisible < 0) ? 0 : cardsVisible; maxVisibleCards = cardsVisible; } -(int) maxVisibleCards /*" Returns the maximum number of visible cards at any one time. The default is CS_SHOWALL. "*/ { return maxVisibleCards; } - (NSColor *) backgroundColor /*" Returns the current background color. "*/ { return backgroundColor; } - (void)setBackgroundColor:(NSColor *)aColor /*" Returns the current background color. "*/ { [backgroundColor autorelease]; backgroundColor = [aColor retain]; } - (void) setCardSize:(CardSize)aSize /*" Sets the size of cards displayed in this pile. #{Constant Description} CS_SMALL Small cards CS_LARGE Big cards "*/ { [cardPile setCardSize:aSize]; if ([cardPile cardSize] == CS_SMALL) { if ([self bounds].size.width > CS_SMALLCARDWIDTH) { xOffset = 0.2; } if ([self bounds].size.height > CS_SMALLCARDHEIGHT) { yOffset = 0.2; } } else { if ([self bounds].size.width > CS_CARDWIDTH) { xOffset = 0.5; } if ([self bounds].size.height > CS_CARDHEIGHT) { yOffset = 0.5; } } } - (BOOL) willDrawOutline /*" Returns YES if the CardPileView will draw a border, NO otherwise. "*/ { return drawOutline; } - (void) setDrawOutline:(BOOL)aFlag /*" Sets whether we'll draw a border around ourselves. The default is YES. "*/ { drawOutline = aFlag; } - (void) dealloc /*" Frees our resources. "*/ { [cardPile release]; if (beneath) [beneath release]; [backgroundColor release]; [super dealloc]; } - (void)setDelegate:(id)anObject /*" Sets our delegate. "*/ { delegate = anObject; delegateFlags = 0; if (delegate) { if ([delegate respondsToSelector:@selector(clickedCard:in:)]) { delegateFlags |= CS_CLICKED; } if ([delegate respondsToSelector:@selector(draggedPile:from:)]) { delegateFlags |= CS_DRAGGED; } if ([delegate respondsToSelector:@selector(canAcceptPile:from:in:)]) { delegateFlags |= CS_CANACCEPT; } if ([delegate respondsToSelector:@selector(acceptPile:in:)]) { delegateFlags |= CS_ACCEPT; } if ([delegate respondsToSelector:@selector(removedPile:from:)]) { delegateFlags |= CS_REMOVED; } if ([delegate respondsToSelector:@selector(doubleClickedCard:in:)]) { delegateFlags |= CS_DOUBLECLICKED; } /*------------------------------------------------------------------- | | If the delegate responds to getOffset::call it | immediately to set new offset values for the view | \------------------------------------------------------------------*/ if ([delegate respondsToSelector:@selector(getOffset::forSize:)]) { delegateFlags |= CS_GETOFFSET; [delegate getOffset:&xOffset :&yOffset forSize:[cardPile cardSize]]; } } } - delegate /*" Returns our current delegate. If we don't have one then nil is returned. "*/ { return delegate; } - (CardPile*) cardPile /*" Returns our CardPile instance that holds all the cards we're displaying. "*/ { return cardPile; } - (void) drawRect:(NSRect)theRect /*" Draws a visual representation of our CardPile. Use the #display message, rather than calling this method directly. You must call #display (or preferably #displayIfNeeded ) after any change to a CardPileView. "*/ { NSRect cardRect = {{0, [self bounds].size.height - CS_CARDHEIGHT}, {CS_CARDWIDTH, CS_CARDHEIGHT}}; NSRect bezelRect; int cardCount = [cardPile cardCount]; int cardIndex; NSPoint lastOrigin = {-1000, -1000}; Card* drawLastCard = nil; int depth; if (delegateFlags & CS_GETOFFSET) { [delegate getOffset:&xOffset :&yOffset forSize:[cardPile cardSize]]; } if ([cardPile cardSize] == CS_SMALL) { cardRect.origin.x = 0.0; cardRect.origin.y = [self bounds].size.height - CS_SMALLCARDHEIGHT; cardRect.size.width = CS_SMALLCARDWIDTH; cardRect.size.height = CS_SMALLCARDHEIGHT; } /*----------------------------------------------------------------------- | | Preserve area under the card (first time only) | \-----------------------------------------------------------------------*/ if (coversOthers && !beneath) { beneath = [[NSBitmapImageRep allocWithZone:[self zone]] initWithFocusedViewRect:theRect]; } /*---------------------------------------------------------------------- | | Draw a neutral background with a bezel | \---------------------------------------------------------------------*/ /* Support added for a color background if using a color machine */ if ((depth = [[self window] depthLimit]) == 0) { depth = [NSWindow defaultDepthLimit]; } if(depth == NSBestDepth(NSCalibratedWhiteColorSpace, 2, 2, YES, NULL)) { if (!coversOthers) { PSsetgray(backgroundGray); PSrectfill(theRect.origin.x, theRect.origin.y, theRect.size.width, theRect.size.height); } else if (beneath) { [beneath draw]; } if (drawOutline) { NSDrawGroove(cardRect , theRect); } } else { if (!coversOthers) { [backgroundColor set]; PSrectfill(theRect.origin.x, theRect.origin.y, theRect.size.width, theRect.size.height); } else if (beneath) { [beneath draw]; } if (drawOutline) { PSsetgray(NSBlack); bezelRect = cardRect; bezelRect.size.width -= 1.0; bezelRect.size.height -= 1.0; NSFrameRect(bezelRect); bezelRect.origin.x += 1.0; bezelRect.origin.y += 1.0; PSsetgray(NSWhite); NSFrameRect(bezelRect); } } /*---------------------------------------------------------------------- | | Draw each card in turn | \---------------------------------------------------------------------*/ for (cardIndex = ([self maxVisibleCards] < cardCount) ? cardCount - [self maxVisibleCards] : 0; cardIndex < cardCount; cardIndex++) { Card* theCard = [cardPile cardAtIndex:cardIndex]; /*------------------------------------------------------------------- | | Draw the card unless cards are being dragged | and this is one of them | \-------------------------------------------------------------------*/ if ((!useDragCardPile) || (!dragCardPile) || ([dragCardPile indexOfCard:theCard] == NSNotFound)) { /*-------------------------------------------------------------- | | If the cards are widely spread draw the whole card | \-------------------------------------------------------------*/ if ((yOffset > 2) || (xOffset > 2)) { [theCard drawCardAt:cardRect.origin]; } else /*------------------------------------------------------------- | | Since the cards are closely spaced, don't draw | an outline unless it is at least 2 pixels from the | last card drawn. Regardless, keep track of the | most recent card so we can draw the image after | the stack | \--------------------------------------------------------------*/ { if ((cardRect.origin.x - lastOrigin.x >= 2.0) || (lastOrigin.y - cardRect.origin.y >= 2.0)) { [theCard drawOutlineAt:cardRect.origin]; lastOrigin = cardRect.origin; } drawLastCard = theCard; } /*-------------------------------------------------------------- | | Reset the origin for the next card | \--------------------------------------------------------------*/ cardRect.origin.x += xOffset; cardRect.origin.y -= yOffset; } } /*-------------------------------------------------------------------- | | If a final card needs to be drawn, draw it over the | last outline drawn | \-------------------------------------------------------------------*/ if (drawLastCard) { [drawLastCard drawCardAt:lastOrigin]; } } - (BOOL) pileCovered:sender /*" Returns YES if any of the CardPileViews connected to coverPile1, coverPile2, coverPile3, or coverPile4 contain cards. Returns NO if these id's are nil, or if the covering piles are empty. "*/ { int coveringCards = 0; int count; CardPileView* covers[4] = {coverPile1, coverPile2, coverPile3, coverPile4}; for (count = 0; count < 4; count++) { if (covers[count]) { coveringCards += [[covers[count] cardPile] cardCount]; } } return (coveringCards > 0); } - (BOOL) pileCoveredBy:(CardPileView*)aCardPileView /*" Returns YES if aCarePileView is non-empty (ie. contains cards) and covers this CardPileView (ie. is connected to coverPile1, coverPile2, coverPile3, or coverPile4). "*/ { int coveringCards = 0; int count; id covers[4] = {coverPile1, coverPile2, coverPile3, coverPile4}; for (count = 0; count < 4; count++) { if (covers[count] == aCardPileView) { coveringCards += [[covers[count] cardPile] cardCount]; break; } } return (coveringCards > 0); } - (void) setCoverPile:(int)offset to:(CardPileView*)aPile /*" Set the cover pile indicated by offset (which must be in the range 1 to 4) to aPile. "*/ { switch (offset) { case 1: coverPile1 = aPile; break; case 2: coverPile2 = aPile; break; case 3: coverPile3 = aPile; break; case 4: coverPile4 = aPile; break; default: break; } } - (void) setCoversOthers:(BOOL)doesCover /*" If this flag is set to YES, the pile will cache and restore the area under the pile, rather that just drawing a rectangle in the current background color, when the pile is redrawn. Used when one card pile overlaps another. "*/ { coversOthers = doesCover; } - (void) resetBacking:sender /*" Discards the current backing, and reloads it when the pile is redisplayed. Only relevant if #setCoversOthers: is set to YES. "*/ { if (beneath) { [beneath release]; beneath = nil; } } - (void) mouseDown:(NSEvent *)thisEvent /*" Used internally to handle card dragging and clicking. "*/ { NSPoint thePoint = [thisEvent locationInWindow]; NSEvent* nextEvent; Card* theCard; [[self window] setAcceptsMouseMovedEvents:YES]; /*----------------------------------------------------------------------- | | Determine which card was clicked on | \-----------------------------------------------------------------------*/ thePoint = [self convertPoint:thePoint fromView:nil]; theCard = [self findCardAtPoint:thePoint]; /*----------------------------------------------------------------------- | | Let our delegate know about the double click or single click if | it cares | \----------------------------------------------------------------------*/ if (([thisEvent clickCount] == 2) && (delegateFlags & CS_DOUBLECLICKED)) { [delegate doubleClickedCard:theCard in:self]; return; } else if (delegateFlags & CS_CLICKED) { [delegate clickedCard:theCard in:self]; } /*----------------------------------------------------------------------- | | Find out what card is under the cursor NOW for | dragging purposes and see if the delegate will | allow us to drag it | \----------------------------------------------------------------------*/ if ((nextEvent = [[self window] nextEventMatchingMask:NSLeftMouseDraggedMask untilDate:[NSDate dateWithTimeIntervalSinceNow:0.5] inMode:NSEventTrackingRunLoopMode dequeue:NO])) { theCard = [self findCardAtPoint:thePoint]; if ((theCard) && (delegateFlags & CS_DRAGGED)) { dragCardPile = [[CardPile allocWithZone:[self zone]] initForCardSize:[cardPile cardSize]]; [dragCardPile addCard:theCard]; if ([delegate draggedPile:dragCardPile from:self]) { NSPoint theOffset = {0, 0}; NSPasteboard* thePasteboard; NSRect cardRect; NSImage* cardImage; /*---------------------------------------------------------- | | If it can be dragged, calculate the size of the image | to be dragged | \---------------------------------------------------------*/ cardRect = [self getRectForCard:theCard]; cardRect.size.height += ([dragCardPile cardCount] - 1) * yOffset; cardRect.origin.y -= ([dragCardPile cardCount] - 1) * yOffset; cardRect.size.width += ([dragCardPile cardCount] - 1) * xOffset; /*---------------------------------------------------------- | | Create the image and pasteboard | \---------------------------------------------------------*/ cardImage = [[NSImage allocWithZone:[self zone]] initWithSize:cardRect.size]; [cardImage addRepresentation:[[[NSCustomImageRep alloc] initWithDrawSelector:@selector(drawDragCard:) delegate:self] autorelease]]; // We have to retain the pasteboard until after the dragging. thePasteboard = [[NSPasteboard pasteboardWithName:@"NXDragPBoard"] retain]; [thePasteboard declareTypes: [NSArray arrayWithObject:CardPilePBoardType] owner:self]; [thePasteboard writeType:CardPilePBoardType asObject:dragCardPile]; /*---------------------------------------------------------- | | Drag it | \----------------------------------------------------------*/ [self dragImage:cardImage at:cardRect.origin offset:NSMakeSize((&theOffset)->x,(&theOffset)->y) event:thisEvent pasteboard:thePasteboard source:self slideBack:YES]; /*--------------------------------------------------------- | | Destroy the image and pasteboard. | \---------------------------------------------------------*/ [thePasteboard autorelease]; [cardImage release]; } /*------------------------------------------------------------- | | Get rid of the temporary drag pile | \-------------------------------------------------------------*/ [dragCardPile release]; dragCardPile = nil; } } [[self window] setAcceptsMouseMovedEvents:NO]; } - (unsigned int) draggingSourceOperationMaskForLocal:(BOOL)flag /*" Let the dragging mechanism know that only generic dragging is available, | and then only within the same application. "*/ { if (flag) { return NSDragOperationGeneric; } return NSDragOperationNone; } - (void) draggedImage:(NSImage *)image beganAt:(NSPoint)screenPoint /*" After the dragged image has been displayed, redraw ourselves without the cards being dragged. "*/ { PSWait(); useDragCardPile = YES; [self display]; useDragCardPile = NO; } - (void) draggedImage:(NSImage *)image endedAt:(NSPoint)screenPoint deposited:(BOOL)flag /*" After the cards have been dragged successfully, remove them from the pile and notify our delegate, if appropriate. Redraw ourselves whether cards were removed or not. "*/ { int cardIndex; if (flag && ([self currentCardView] != self)) //Kluge above changed from: if (flag) { for (cardIndex = 0; cardIndex < [dragCardPile cardCount]; cardIndex++) { [cardPile removeCard:[dragCardPile cardAtIndex:cardIndex]]; } if (delegateFlags & CS_REMOVED) { [delegate removedPile:dragCardPile from:self]; } } [self display]; PSWait(); } - (unsigned int) draggingEntered:sender /*" Ask our delegate if the cards dragged in can be dropped. "*/ { CardPile* dropPile; unsigned int theOperation = NSDragOperationNone; [[sender draggingPasteboard] readType:CardPilePBoardType asObject:&dropPile]; if ((dragCardPile) || ((delegateFlags & CS_CANACCEPT) && ([delegate canAcceptPile:dropPile from:sender in:self]))) { theOperation = NSDragOperationGeneric; } return theOperation; } - (BOOL) prepareForDragOperation:sender /*" Add cards dropped on our pile to the top of the pile and notify our delegate, if possible. Redisplay the pile afterwards in any case. "*/ { CardPile* dropPile; int cardIndex; // I'd like to throw this puppy only in the below if, but I'm afraid of a wierd circumstance // like we're dragging to a place that we had a previous "failure" // OK you've gotta love this -- in the destination we're setting the source's destination // which for those who aren't smarter than dirt is self //Kluge line below added [[sender draggingSource] setCurrentCardView: self]; if ([sender draggingSource] == self) { [self display]; PSWait(); return NO; } [[sender draggingPasteboard] readType:CardPilePBoardType asObject:&dropPile]; for (cardIndex = 0; cardIndex < [dropPile cardCount]; cardIndex++) { [cardPile insertCard:[dropPile cardAtIndex:cardIndex] at:CS_TOP]; } if (delegateFlags & CS_ACCEPT) { [delegate acceptPile:dropPile in:self]; } [self display]; PSWait(); return YES; } - (BOOL)performDragOperation:sender /*" Returns YES. "*/ { return YES; } - (Card*) findCardAtPoint:(NSPoint)thePoint /*" Returns the Card object at thePoint, or nil if there is no card at that location. "*/ { Card* theCard = nil; NSRect cardRect = {{0, 0}, {CS_CARDWIDTH, CS_CARDHEIGHT}}; int counter; int stopIndex; int cardCount = [[self cardPile] cardCount]; stopIndex = ([self maxVisibleCards] < cardCount) ? cardCount - [self maxVisibleCards] : 0; if ([cardPile cardSize] == CS_SMALL) { cardRect.origin.x = cardRect.origin.y = 0.0; cardRect.size.width = CS_SMALLCARDWIDTH; cardRect.size.height = CS_SMALLCARDHEIGHT; } /*---------------------------------------------------------------------- | | Search for a card hit from the top of the stack down | \---------------------------------------------------------------------*/ for (counter = [cardPile cardCount] - 1; counter >= stopIndex; counter--) { cardRect.origin.x = (counter - stopIndex) * xOffset; if ([cardPile cardSize] == CS_SMALL) { cardRect.origin.y = ([self bounds].size.height - CS_SMALLCARDHEIGHT) - ((counter - stopIndex) * yOffset); } else { cardRect.origin.y = ([self bounds].size.height - CS_CARDHEIGHT) - ((counter - stopIndex) * yOffset); } if (NSMouseInRect(thePoint , cardRect , NO)) { theCard = [cardPile cardAtIndex:counter]; break; } } return theCard; } - (NSRect) getRectForCard:(Card*)aCard /*" Returns an NSRect for the bounding rectangle of aCard. If aCard isn't in our CardPile then the returned NSRect will be all zeros. "*/ { int cardIndex = [cardPile indexOfCard:aCard]; int cardCount = [[self cardPile] cardCount]; NSRect cardRect; cardRect = NSMakeRect(0.0, 0.0, 0.0, 0.0); /*---------------------------------------------------------------------- | | Return immediately if the card isn't in the cardPile | \---------------------------------------------------------------------*/ if (cardIndex == NSNotFound) { return cardRect; } // Take into the cards that aren't visible cardIndex -= ([self maxVisibleCards] < cardCount) ? (cardCount - [self maxVisibleCards]) : 0; /*---------------------------------------------------------------------- | | Calculate and supply the bounding rectangle | \---------------------------------------------------------------------*/ if ([cardPile cardSize] == CS_SMALL) { cardRect.origin.x = xOffset * cardIndex; cardRect.origin.y = ( [self bounds].size.height - CS_SMALLCARDHEIGHT) - (yOffset * cardIndex); cardRect.size.width = CS_SMALLCARDWIDTH; cardRect.size.height = CS_SMALLCARDHEIGHT; } else { cardRect.origin.x = xOffset * cardIndex; cardRect.origin.y = ([self bounds].size.height - CS_CARDHEIGHT) - (yOffset * cardIndex); cardRect.size.width = CS_CARDWIDTH; cardRect.size.height = CS_CARDHEIGHT; } return cardRect; } - (void) drawDragCard:sender /*" Draw the pile of cards being dragged. Do not call directly. "*/ { NSPoint tempPoint = { 0, 0 }; int counter; tempPoint.y += ([dragCardPile cardCount] - 1) * yOffset; for (counter = 0; counter < [dragCardPile cardCount]; counter++) { [[dragCardPile cardAtIndex:counter] drawCardAt:tempPoint]; tempPoint.x += xOffset; tempPoint.y -= yOffset; } } - (void) setTag:(int)theTag /*" Sets our tag. "*/ { tag = theTag; } - (int) tag /*" Returns our tag. "*/ { return tag; } @end
These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.