The third version of Peg Solitaire adds mouse-over highlighting of occupied spacs. Changes to the code include storing the actual bezier path for the peg rather than just the peg rectangle, plus the code necessary for supporting mouse moved events.
Taking a look at CHPegBoard.h
, the changes were:
recalcPegPaths
flag gets set when the paths need to be regenerated.
CHPegBoard.h
:
#import <Cocoa/Cocoa.h> #define PEG_COUNT 15 enum { CHPegBoard_NO_PEG = -1 }; @interface CHPegBoard : NSView { BOOL pegStates[PEG_COUNT]; // YES - occupied, NO - emptyNSRect pegRects[PEG_COUNT]; // where the peg livesNSBezierPath *pegPaths[PEG_COUNT]; // where the peg lives BOOL recalcPegPaths; int selectedPeg; } @end // CHPegBoard
Now for the code in the peg view. First, initWithFrame:
is extended to initialze the recalcPegPaths
and
selectedPeg
values. This is necessary because their
useful default values are non-zero. Had they been zero, no explicit
initialization would be necessary.
CHPegBoard.m
:
- (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; } return (self); } // initWIthFrame
awakeFromNib
is a good place to set the mouse motion
attribute for the view since it gets called after all the Interface Builder
connections have been made:
// nib file has been loaded and all connections made in interface builder // have been made - (void) awakeFromNib { // we want mouse motion events so we can highlight pegs as the user // mouses over them [[self window] setAcceptsMouseMovedEvents: YES]; } // awakeFromNib
// we accept first responder status. This is necessary to receive // mouse motion events - (BOOL) acceptsFirstResponder { return (YES); } // acceptsFirstResponder
CHPegView
is the window's initial first responder by control-dragging from the window
to the pegboard, and setting the initialFirstResponder
outlet.
mouseMoved:
method when an event happens. First the mouse
location is converted from window coordinates to view coordinates, then
the pegs are looped over looking for a match (the actual construction of
the bezier path for the pegs will come a little later in this narrative)
Finally, the view is told to redraw itself (eventually) if the peg
changes. The extra variable in the code (candidatePeg
) is
there so that we don't trigger a redraw on every mouse moved event, since
the selection will only change in a fraction of the events. The pegboard
drawing code will look at the selectedPeg
variable to decide
how it wants to render the peg.
- (void) mouseMoved: (NSEvent *) event { // get the mouse position in view coordinates NSPoint mouse; mouse = [self convertPoint: [event locationInWindow] fromView: nil]; // see if it is over a peg int candidatePeg = CHPegBoard_NO_PEG; int i; for (i = 0; i < PEG_COUNT; i++) { // see if a peg at an occupied spot if (pegStates[i]) { // encloses the mouse point if ([pegPaths[i] containsPoint: mouse]) { candidatePeg = i; break; } } } // only redraw if the currently selected peg (if any) has changed if (selectedPeg != candidatePeg) { selectedPeg = candidatePeg; [self setNeedsDisplay: YES]; } } // mouseMoved
setFrame:
. Calculating the peg locations on every draw
is pretty wasteful, although with the insanely fast computers these days,
it's not noticable. But on general principles, I want the peg location
calculation code to only be called when necessary. Our setFrame:
will set the recalcPegPaths
flag, and then at the next redraw
we'll recalculate the paths for the pegs. Why not calculate the peg
paths in this method? You could do that, and my first iteration of this
actually recalculated the peg locations, but I had ended up copying and
pasting some lines of code to do it, so I figured just keep all that
stuff in one place.
// this gets called by Cocoa when the view gets resized. // when that happens, we need to recalculate the paths for the pegs - (void) setFrame: (NSRect) frame { [super setFrame: frame]; recalcPegPaths = YES; } // setFrame
NSBezierPath
objects, one for each peg location.
recalcPegRectsForRect
is largely untouched. An array of
rectangles is declared as a local variable rather than an instance
variable, and a loop has been added for the creation of circular paths.
And the method has been renamed to better reflect what it is doing now
- (void) recalcPegRectsForRect: (NSRect) squareRect- (void) recalcPegPathsForRect: (NSRect) squareRect { // first make all the rects flush-left // these magic constants were determined via tweaking and fiddling float pegRadius = squareRect.size.width / 8; float pad = (squareRect.size.width - (pegRadius * 5)) / 7.0; float rowHeight = squareRect.size.height / 6.0; float x, y; // union rectangle for each row NSRect pegRects[PEG_COUNT]; NSRect unions[5]; int i = 0; // which peg we're looking at int row; for (row = 0; row < 5; row++) { y = pad + rowHeight * row; x = 0; // do the pegs of the row. int peg; for (peg = 0; peg < 5 - row; peg++) { pegRects[i] = NSMakeRect (x, y, pegRadius, pegRadius); x += pegRadius + pad; if (peg == 0) { unions[row] = pegRects[i]; } else { unions[row] = NSUnionRect (unions[row], pegRects[i]); } i++; } } // now center each rect in our useful area NSRect centerSquare = [self makeCenterSquareRect]; i = 0; for (row = 0; row < 5; row++) { NSRect rect = [self centerRect: unions[row] inRect: centerSquare]; // now update the x values for the row, and update the y // for the start of the center rect int peg; for (peg = 0; peg < 5 - row; peg++) { pegRects[i].origin.x += rect.origin.x + centerSquare.origin.x; pegRects[i].origin.y += centerSquare.origin.y; i++; } } // make the bezier path for each peg for (i = 0; i < PEG_COUNT; i++) { [pegPaths[i] release]; pegPaths[i] = [NSBezierPath bezierPathWithOvalInRect: pegRects[i]]; [pegPaths[i] retain]; } } // recalcPegPathsForRect
bezierPathWithOvalInRect:
is not an alloc
or
copy
method, so assume the object is auto-released. Since
we want to hang on to these paths, retain them. Since we're retaining
them, we need a dealloc
method to release them when our
pegboard goes away.
- (void) dealloc { int i; for (i = 0; i < PEG_COUNT; i++) { [pegPaths[i] release]; } [super dealloc]; } // dealloc
// given a peg index, draw it. - (void) drawPeg: (int) pegIndex { 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 (pegStates[pegIndex]) { if (pegIndex== selectedPeg) { [[NSColor yellowColor] set]; } else { [[NSColor blackColor] set]; } } else { [[NSColor whiteColor] set]; } [pegPaths[pegIndex] fill]; if (pegIndex== selectedPeg) { [NSBezierPath setDefaultLineWidth: 5]; [[NSColor redColor] set]; [pegPaths[pegIndex] stroke]; [NSBezierPath setDefaultLineWidth: 1]; } else { [[NSColor blackColor] set]; [pegPaths[pegIndex] stroke]; } } // drawPeg
drawRect:
needs to recalculate
the paths when the recalcPegPaths
flag is set.
- (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]; } [[NSColor blackColor] set]; [NSBezierPath strokeRect: bounds]; } // drawRect