The fourth version of PegSolitaire adds the dragging of pegs around. There is no rule logic, so the user can move pegs from spot to spot.
The dragging is done via an NSImage. A peg is drawn in the image, along
with a shadow, and then the image is moved around the view. When the mouse
pointer is over a valid drop spot (currently any open slot), the spot
will highlight. Releasing the mouse button will move the peg to that slot.
(in reality, just set that slot's state to YES
.)
Details on creating the image and doing the dragging are down in the code
snippets.
CHPegBoard.h
:
The class has grown four new instance variables:
@interface CHPegBoard : NSView { BOOL pegStates[PEG_COUNT]; // YES - occupied, NO - empty NSBezierPath *pegPaths[PEG_COUNT]; // where the peg lives BOOL recalcPegPaths; int selectedPeg; NSImage *dragImage; // non-nil means we're in a drag int dragPeg; // index of where the drag came from // selectedPeg is what the mouse is currently over NSPoint dragPoint; // where to draw the drag image NSPoint lastMouse; // previous mouse coordinates } @end // CHPegBoard
dragImage
is the NSImage
that gets moved
around while the user drags. No actual 'object' is directly moved by
the user, just the illusion. The dragPoint
is the key to
that illusion, which is where the image gets drawn. The
lastMouse
position is kept, so that if the mouse gets
dragged, we know the delta of the drag. The image then gets shifted
by that amount. dragPeg
is the origin of the drag.
CHPegBoard.m
:
The default zero value set during object creation is fine, except for
the dragPeg. It won't change the logic of the program, but it's good
to have a peg index not be a legal peg value if it's not being used.
- (id) initWithFrame: (NSRect) frame
{
if (self = [super initWithFrame: frame]) {
int i;
// alternate them on and off, for initial development
for (i = 0; i < PEG_COUNT; i++) {
pegStates[i] = ((i % 2) == 0) ? YES : NO;
}
recalcPegPaths = YES;
selectedPeg = CHPegBoard_NO_PEG;
dragPeg = CHPegBoard_NO_PEG;
}
return (self);
} // initWIthFrame
drawPeg:
. Rather than have it make the
determiniation whether to draw the peg as selected by looking at an
instance variable, it's now passed as a parameter. This way the peg
can be rendered into a different context (say the drag image) without
having the selectedPeg
instance variable confusing
things. The drawing has been tweaked slightly, with the black pegs
stroked with a white ring. This makes pegs a little easier to see
when being dragged around.
The code uses a new Panther feature, using NSColor
to set the
stroke and fill color independently. This simplifies the code; otherwise
the fill and stroke calls would need to be intermixed with the color
setting, or local variables used to hold the fill and stroke colors, which
are then set before the drawing operation.
- (void) drawPeg: (int) pegIndex selected: (BOOL) isSelected { assert (pegIndex>= 0 && pegIndex< PEG_COUNT); // right now, just make 'on' pegs black, and 'off' pegs white // eventually we can do much snazzier pegs if (isSelected) { [[NSColor yellowColor] setFill]; [[NSColor redColor] setStroke]; } else { if (pegStates[pegIndex]) { [[NSColor blackColor] setFill]; [[NSColor whiteColor] setStroke]; } else { [[NSColor whiteColor] setFill]; [[NSColor blackColor] setStroke]; } } [pegPaths[pegIndex] fill]; [pegPaths[pegIndex] stroke]; } // drawPeg
- (int) pegIndexAtPoint: (NSPoint) point { // see if it is over a peg int pegIndex = CHPegBoard_NO_PEG; int i; for (i = 0; i < PEG_COUNT; i++) { if ([pegPaths[i] containsPoint: point]) { pegIndex = i; break; } } return (pegIndex); } // pegIndexAtPoint
mouseMoved:
was updated to track this change
- (void) mouseMoved: (NSEvent *) event { // get the mouse position in view coordinates NSPoint mouse; mouse = [self convertPoint: [event locationInWindow] fromView: nil];int candidatePeg = CHPegBoard_NO_PEG;// only redraw if the currently selected peg (if any) has changed int candidatePeg = [self pegIndexAtPoint: mouse];int i;for (i = 0; i < PEG_COUNT; i++) {if (candidatePeg != CHPegBoard_NO_PEG) { // only allow occupied spots if (!pegStates[candidatePeg]) { candidatePeg = CHPegBoard_NO_PEG; } } if (selectedPeg != candidatePeg) { selectedPeg = candidatePeg; [self setNeedsDisplay: YES]; } } // mouseMoved
mouseDown
:, for when the button gets clicked.
mouseDragged:
is called when the user moves the mouse
around (while the button is down), and then mouseUp:
when
the button is released.
So first the mouseDown:
First see if there's a peg at
the mouse down location. Convert from window to view coordinates,
and use the pegIndexAtPoint:
method to see what peg
is under the mouse.
- (void) mouseDown: (NSEvent *) event { // get the mouse position in view coordinates NSPoint mouse; mouse = [self convertPoint: [event locationInWindow] fromView: nil]; dragPeg = [self pegIndexAtPoint: mouse];If we're over a location (drag peg is a valid number) and it is unoccupied (the state is YES), the start the drag. Make the image that we're going to drag around
if (dragPeg != CHPegBoard_NO_PEG && pegStates[dragPeg]) { // make the image of a peg with a drop shadow dragImage = [self makeDragImageForPeg: dragPeg dragPoint: &dragPoint]; [dragImage retain];The
makeDragImageForPeg:dragPoint:
method modifies
the drag point so that it is at the bottom-left of the image. That is,
where the image should be composited so that the peg will be drawn in
the right place:
selectedPeg
instance variable. We'll be using that to
keep track of the current drop destination (which will highlight
when moused over). Also clear out the source slot, since we've pulled
the peg out of there. Finally schedule a redraw.
lastMouse = mouse; // clear out the selected peg. This will be the current drop // destination selectedPeg = CHPegBoard_NO_PEG; // clear out the source slot, since the peg that lives there is // currently moving pegStates[dragPeg] = NO; [self setNeedsDisplay: YES]; } } // mouseDown
- (void) mouseDragged: (NSEvent *) event { // get the mouse position in view coordinates NSPoint mouse; mouse = [self convertPoint: [event locationInWindow] fromView: nil]; if (dragImage != nil) {but if we are dragging a peg, calculate the delta from the last known mouse position, and offset the dragPoint. This will have the side effect of moving the image:
// update the drag image float deltaX, deltaY; deltaX = mouse.x - lastMouse.x; deltaY = mouse.y - lastMouse.y; dragPoint.x += deltaX; dragPoint.y += deltaY;Hang on to the current mouse position so we can calculate this delta on the next mouse drag. Also highlight the peg location if we're over one. And finally schedule a redraw.
// remember the mouse position so we can calculate the delta // next time we get a drag lastMouse = mouse; // highlight the destination if we're over an empty cell selectedPeg = [self pegIndexAtPoint: mouse]; // make sure the destination isn't already occupied if (selectedPeg != CHPegBoard_NO_PEG && pegStates[selectedPeg]) { selectedPeg = CHPegBoard_NO_PEG; } [self setNeedsDisplay: YES]; } } // mouseDragged
mouseUp:
, if we're dragging and we have a
selectedPeg
(set in mouseDragged:
),
'move' the peg we're dragging to that spot (really just setting its
state to YES). Otherwise 'move' the peg back to where it came from,
which is just setting the state for dragPeg
to YES.
Then release the drag image (since we don't need it anymore) and
then schedule a redraw.
- (void) mouseUp: (NSEvent *) event { if (dragImage != nil) { // if we're over a peg, return it from whence it came if (selectedPeg == CHPegBoard_NO_PEG) { pegStates[dragPeg] = YES; } else { // otherwise 'move' the peg to the new stop pegStates[selectedPeg] = YES; } // clear the selection selectedPeg = CHPegBoard_NO_PEG; // get rid of the drag image [dragImage release]; dragImage = nil; // and redraw to get rid of the drag indicator [self setNeedsDisplay: YES]; } } // mouseUp
- (void) drawRect: (NSRect) rect { // get the bounds, since the rect might be a smaller area to redraw NSRect bounds = [self bounds]; // make a nice white background to draw on [[NSColor whiteColor] set]; [NSBezierPath fillRect: bounds]; // this area is where the triangular pegboard will go NSRect squareRect = [self makeCenterSquareRect]; if (recalcPegPaths) { [self recalcPegPathsForRect: squareRect]; recalcPegPaths = NO; } // draw the board [self drawBoardInRect: squareRect]; // draw the pegs on top of the board int i; for (i = 0; i < PEG_COUNT; i++) { [self drawPeg: i selected: (i == selectedPeg)]; } // draw any dragged peg on top of everything if (dragImage != nil) { [dragImage dissolveToPoint: dragPoint fraction: 1.0]; } [[NSColor blackColor] set]; [NSBezierPath strokeRect: bounds]; } // drawRect
drawPeg:selected:
method to draw at an
arbitrary point. Instead, we'll make a transparent image, draw the
peg and use NSShadow
to make a drop shadow. An
NSAffineTransform
is used to shift the coordinate system
when drawing the peg.
First off, the method comments, signature, and some sanity checking.
// creates an image, with a clear background, of a peg with a drop-shadow // underneath it. // The dragLocation is adjusted so that if the image is drawn with its // bottom-left at that point, the peg will appear to be under the mouse // where the user clicked. - (NSImage *) makeDragImageForPeg: (int) pegIndex dragPoint: (NSPoint *) dragLocation { assert (pegIndex != CHPegBoard_NO_PEG); assert (pegIndex >= 0 && pegIndex < PEG_COUNT);Now get the rectangle that surrounds the peg
NSRect pathBounds = [pegPaths[pegIndex] bounds];The actual image will need to be bigger than this to allow room for the shadow. By experiments, 15 was a good amount.
// the size of the dragged image is the size of the peg, // plus a skootch so that the stroked line around the peg // stays round. Plus some space for the drop shadow #define IMAGE_EXPANSION 15 // expand it to give us room for the drop image NSRect bounds; bounds = NSInsetRect (pathBounds, -IMAGE_EXPANSION, -IMAGE_EXPANSION);
NSShadow
is a new class in Panther that will draw a shadow
on any drawing that happens. Configure the shadow to look good.
// make a drop shadow (this is new in Panther) NSShadow *shadow = [[NSShadow alloc] init]; [shadow setShadowBlurRadius: 15.0]; [shadow setShadowOffset: NSMakeSize (10, -10)];Lock focus on the image. Any drawing calls will now be accumulated into the image. Tell the shadow to set itself. When
unlockFocus
is
called the shadow will be added to the image.
// draw into the image [image lockFocus]; { // all drawing will get a shadow [shadow set];Now to play with the coordinate system.
drawPeg:selected:
draws the peg at the location in the window where the peg lives. The
image wants stuff draw with an origin of (zero, zero). Rather than change
the way drawPeg:
works, we'll just shift the coordinate
system. The peg will draw itself where it wants (say at 100, 100), but
we'll shift the corrdinate system by -100, -100, so that the peg will
draw at 0, 0.
// since the peg wants to draw at the point it was defined at, // and the image wants us to draw at (0, 0), shift the // coordinate system over so the drawing does happen at (0, 0) NSAffineTransform *transform = [NSAffineTransform transform]; [transform translateXBy: -bounds.origin.x yBy: -bounds.origin.y]; [transform concat];Fill the image with the clear color, so that parts that don't get drawn will be transparent.
// make it transparent where drawing doesn't touch [[NSColor clearColor] set]; [NSBezierPath fillRect: bounds];And draw the peg
[self drawPeg: pegIndex selected: NO];Unlock the focus, indicating the image drawing is done. This also will tell the shadow to draw itself.
} [image unlockFocus];When we draw the image, the bottom-left of the image will be located at the point we tell it to draw at. Offset the
dragLocation
such that it is the bottom-left of the image.
// offset the given drag location to account for the extra padding // put on the image dragLocation->x = pathBounds.origin.x - IMAGE_EXPANSION; dragLocation->y = pathBounds.origin.y - IMAGE_EXPANSION;Finally clean up after the shadow and return the image.
[shadow release]; return ([image autorelease]); } // makeDragImageForPeg