Programming Mac OS X with Cocoa for Beginners/More shapes

From Wikibooks, open books for an open world
< Programming Mac OS X with Cocoa for Beginners
Jump to: navigation, search

Previous Page: Wikidraw's view class | Next Page: An Inspector calls

Before we can act upon the different tools, we'll need the classes that implement the different shapes. We have four tool buttons at the moment, so it would be nice to have one being a selection tool, and three being shape drawing tools. We already have the rectangle shape so we need two more. We'll create an oval shape and a slightly more complex polygonal shape. You can go on to create any number of additional shapes if you want, and just extend the tool palette to access them.

Select WKDShape.m and choose File->New File... Choose Objective-C class, and set its name to "WKDOvalShape.m". By the way the only reason to select the WKDShape file before you do this is to make sure the new files go in the same group - unfortunately Xcode doesn't automatically subclass existing classes, so we'll have to do that by hand.

Open the WKDOvalShape.h file and add #import "WKDShape.h" to the top of the file. Then change NSObject to WKDShape. Your class definition looks like this:

#import <Cocoa/Cocoa.h>
#import "WKDShape.h"

@interface WKDOvalShape : WKDShape
{
}

@end

The only method to be implemented is 'path', and we don't need to declare it. Just add it to the implementation, like this:

@implementation WKDOvalShape

- (NSBezierPath*)       path
{
        return [NSBezierPath bezierPathWithOvalInRect:[self bounds]];
}

@end

Now repeat the whole exercise again, this time making files for a class called 'WKDPolyShape'. Here's the implementation we need:

@implementation WKDPolyShape

- (NSBezierPath*)       path
{
        NSBezierPath* path = [NSBezierPath bezierPath];
        NSRect  br = [self bounds];
        NSPoint p;
        
        [path moveToPoint:br.origin];
        
        p.x = NSMidX( br );
        p.y = NSMidY( br );
        [path lineToPoint:p];
        
        p.x = NSMaxX( br );
        p.y = NSMinY( br );
        [path lineToPoint:p];
        
        p.x = NSMaxX( br );
        p.y = NSMaxY( br );
        [path lineToPoint:p];

        p.x = NSMidX( br );
        p.y = NSMidY( br );
        [path lineToPoint:p];
        
        p.x = NSMinX( br );
        p.y = NSMaxY( br );
        [path lineToPoint:p];
        [path closePath];
        
        return path;
}
@end

Now return to WKDDrawView.m. Add imports for both of these new files to the top of the file. Add the following to the method 'shapeForCurrentTool'.

- (WKDShape*)   shapeForCurrentTool
{
        switch([self currentTool])
        {
                default:
                        return nil;
        
                case 1:
                        return [[[WKDShape alloc] init] autorelease];
                        
                case 2:
                        return [[[WKDOvalShape alloc] init] autorelease];
                        
                case 3:
                        return [[[WKDPolyShape alloc] init] autorelease];
        }
}


- (int)                 currentTool
{
        return sCurrentTool;
}

It's simple - depending on the current tool, we make one of the three different kinds of shapes. We return 'nil' as the default case, which we'll use to mean the selection tool. Build and Go and check that three kinds of shapes can be drawn. The tool with ID 0 currently will log an error because we haven't adjusted our mouseDown: method to handle the nil case as a selection yet. Let's fix that next.

Selection marquee[edit]

The idea of a selection marquee is very simple - the user clicks in the view with the selection tool, and drags a rectangular box over a set of objects. Any objects that the box touches are selected, and any others outside the box are deselected.

To make this work, we'll need a way to tell which objects are touched by the marquee and which are not. This is straightforward, as the utility function NSIntersectsRect, which we've already seen, will figure it out. We just need to iterate over our objects, retrieve their bounds (or more accurately, the drawBounds) and test for an intersection with the marquee. If it intersects, it's selected, if not, it isn't.

To draw the marquee itself, we'll need to track its original anchor point, and use this to form a rectangle. All of the required code goes in the view class. We also add a pair of NSPoint data members, _anchor and _marquee, to track the current size of the marquee.

- (void)                updateMarquee:(NSPoint) newPoint
{
        _marquee = newPoint;
        
        NSRect mr = NSRectFromTwoPoints( _anchor, _marquee );
        NSEnumerator* iter = [[[self delegate] objects] objectEnumerator];
        WKDShape* obj;
        NSRect          br;
        
        [[self delegate] deselectAll];
        
        while ( obj = [iter nextObject])
        {
                br = [obj drawBounds];
                
                if ( NSIntersectsRect( mr, br ))
                        [[self delegate] selectObject:obj];
        }
        [self setNeedsDisplayInRect:mr];
}


- (void)                drawMarquee
{
        NSRect mr = NSInsetRect( NSRectFromTwoPoints( _anchor, _marquee ), 1, 1 );
        
        [[NSColor grayColor] set];
        NSFrameRect( mr );
}

updateMarquee: does most of the work. It sets the _marquee data member, and then uses this and _anchor to calculate the rectangle of the marquee. Then it deselects everything, and iterates over the objects, testing each one's drawBounds against the marquee rect. If they intersect, the object is selected.

drawMarquee also calculates the marquee rectangle, and frames it in grey. Both of these methods call the utility NSRectFromTwoPoints. In fact Cocoa doesn't provide this function, so instead we define it ourselves. Note that this is a plain C function, outside of the class implementation. It should be placed above the @implementation declaration in WKDDrawView.m.

NSRect  NSRectFromTwoPoints( NSPoint a, NSPoint b )
{
        NSRect r;
        
        r.origin.x = MIN( a.x, b.x );
        r.origin.y = MIN( a.y, b.y );
        
        r.size.width = ABS( a.x - b.x );
        r.size.height = ABS( a.y - b.y );
        
        return r;
}

Now we need to modify our mousing methods to handle the marquee. All three methods are affected, but mouseDown: is the most complex. The logic of the method needs to be changed so that a more consistent behaviour is implemented, now we have a working tool palette. Rather than immediately locate the object under the mouse, we need to determine the current tool first - we only need to look for an object under the mouse if the selection tool is active. Otherwise, we are creating a new object. Thus the order of the logic at the top of the method needs to be altered. Here's the modified method:

- (void)        mouseDown:(NSEvent*) evt
{
        NSPoint pt = [self convertPoint:[evt locationInWindow] fromView:nil];
        _dragShape = [self shapeForCurrentTool];
        
        if (_dragShape == nil)
        {
                _dragShape = [[self delegate] objectUnderMousePoint:pt];
                
                if ( _dragShape == nil )
                {
                        // marquee drag
                        
                        _anchor = pt;
                        [self updateMarquee:pt];
                        return;
                }
        }
        else
        {
                [[self delegate] addObject:_dragShape];
                [_dragShape setLocation:pt];
        }
        
        if (([evt modifierFlags] & NSShiftKeyMask) == 0)
        {
                [[self delegate] deselectAll];
                [[self delegate] selectObject:_dragShape];
        }
        else
        {
                if ([[[self delegate] selection] containsObject:_dragShape])
                {
                        [[self delegate] deselectObject:_dragShape];
                        _dragShape = nil;
                }
                else
                        [[self delegate] selectObject:_dragShape];
        }
        
        [self setNeedsDisplay:YES];
        [_dragShape mouseDown:pt];
}


- (void)        mouseDragged:(NSEvent*) evt
{
        NSPoint pt = [self convertPoint:[evt locationInWindow] fromView:nil];
        NSRect update = [_dragShape drawBounds];
        
        if ( _dragShape )
                [_dragShape mouseDragged:pt];
        else
                [self updateMarquee:pt];
                
        [self setNeedsDisplayInRect:NSUnionRect([_dragShape drawBounds], update)];
}


- (void)        mouseUp:(NSEvent*) evt
{
        NSPoint pt = [self convertPoint:[evt locationInWindow] fromView:nil];
        
        if ( _dragShape )
                [_dragShape mouseUp:pt];
        else
        {
                [self setNeedsDisplayInRect:NSRectFromTwoPoints( _anchor, _marquee )];
                _anchor = _marquee = NSZeroPoint;
        }       
        [self setNeedsDisplayInRect:[_dragShape drawBounds]];
        _dragShape = nil;
}

If the selection tool is current, we will have a nil dragShape, so we use that to subsequently detect the marquee case by testing if the mouse hit empty space or an existing object. For mouseDown:, we set the value of _anchor, then call for an update of the marquee. On mouseDragged:, we detect the marquee case and update it again, changing the selection as needed. On mouseUp:, we refresh the marquee area and set the marquee points to zero. This will erase the marquee, leaving anything it selected still selected. The only remaining task is to make the marquee visible. To do this, we simply add a call to drawMarquee to our drawRect: method.

- (void)        drawRect:(NSRect) rect
{
        [[NSColor whiteColor] set];
        NSRectFill( rect );
        
        NSArray*                drawList = [[self delegate] objects];
        NSArray*                selection = [[self delegate] selection];
        NSEnumerator*   iter = [drawList objectEnumerator];
        WKDShape*               shape;
        
        while( shape = [iter nextObject])
        {
                if ( NSIntersectsRect( rect, [shape drawBounds]))
                        [shape drawWithSelection:[selection containsObject:shape]];
        }
        
        [self drawMarquee];
}

Now we'll find that when we compile and run the application, we have a much more standard behaviour for the selection tool.

There are some fairly serious inefficiencies here - every time the marquee size changes, the selection is recalculated entirely from scratch. For a few objects this isn't a problem, but if our drawing became complex it could well be. Once we add the inspector which will respond to selection changes, performance could suffer even more. We won't be worrying about this now, but perhaps you could think of a few simple ways to improve the efficiency?

Previous Page: Wikidraw's view class | Next Page: An Inspector calls