Chapter 18. Images and Mouse Events
NSResponder
NSView inherits from NSResponder. All the event-handling methods are declared in NSResponder. We discuss keyboard events in the next chapter. For now, we are interested only in mouse events. NSResponder declares these methods:
- (void)mouseDown:(NSEvent *)theEvent;
- (void)rightMouseDown:(NSEvent *)theEvent;
- (void)otherMouseDown:(NSEvent *)theEvent;
- (void)mouseUp:(NSEvent *)theEvent;
- (void)rightMouseUp:(NSEvent *)theEvent;
- (void)otherMouseUp:(NSEvent *)theEvent;
- (void)mouseDragged:(NSEvent *)theEvent;
- (void)rightMouseDragged:(NSEvent *)theEvent;
- (void)otherMouseDragged:(NSEvent *)theEvent;
- (void)scrollWheel:(NSEvent *)theEvent;
Note that the argument is always an NSEvent object.
NSEvent
An event object has all the information about what the user did to trigger the event. When you are dealing with mouse events, you might be interested in the following methods:
- (NSPoint)locationInWindow
This method returns the location of the mouse event.
- (unsigned int)modifierFlags
The integer tells you which modifier keys the user is holding down on the keyboard. This enables the programmer to tell a Control-click from a Shift-click, for example. The code would look like this:
- (void)mouseDown:(NSEvent *)e { unsigned int flags; flags = [e modifierFlags]; if (flags & NSControlKeyMask) { ...handle control click... } if (flags & NSShiftKeyMask) { ...handle shift click... } }
Here are the constants that you commonly AND (&) against the modifier flags:
NSShiftKeyMaskNSControlKeyMaskNSAlternateKeyMaskNSCommandKeyMask- (NSTimeInterval)timestamp
This method gives the time interval in seconds between the time the machine booted and the time of the event. NSTimeInterval is a double.
- (NSWindow *)window
This method returns the window associated with the event.
- (int)clickCount
Was it a single-, double-, or triple-click?
- (float)pressure
If the user is using an input device that gives pressure (a tablet, for example), this method returns the pressure. It is between 0 and 1.
- (float)deltaX; - (float)deltaY; - (float)deltaZ;These methods give the change in the position of the mouse or scroll wheel.
Getting Mouse Events
To get mouse events, you need to override the mouse event methods in StretchView.m:
#pragma mark Events - (void)mouseDown:(NSEvent *)event{
NSLog(@"mouseDown: %d", [event clickCount]);
} - (void)mouseDragged:(NSEvent *)event{
NSPoint p = [event locationInWindow];
NSLog(@"mouseDragged:%@", NSStringFromPoint(p));
} - (void)mouseUp:(NSEvent *)event{
NSLog(@"mouseUp:");
}
Build and run your application. Try double-clicking, and check the click count. Note that the first click is sent and then the second click. The first click has a click count of 1; the second click has a click count of 2.
Note the use of #pragma mark. At the top of any Xcode editing window there is a pop-up that enables you to jump to any of the declarations and definitions in the file. #pragma mark puts a label into that pop-up. Stylish programmers (like you, dear reader) use it to group their methods.
Composite an Image onto Your View
You will also need to change StretchView so that it uses the opacity and image. First, declare variables and methods in your StretchView.h file:
#import <Cocoa/Cocoa.h> @interface StretchView : NSView{
NSBezierPath *path;
NSImage *image;
float opacity;
}
@property (readwrite) float opacity;
- (void)setImage:(NSImage *)newImage;
- (NSPoint)randomPoint;
@end
Now implement these methods in your StretchView.m file:
#pragma mark Accessors - (void)setImage:(NSImage *)newImage{
[newImage retain];
[image release];
image = newImage;
[self setNeedsDisplay:YES];
} - (float)opacity{
return opacity;
}
- (void)setOpacity:(float)x{
opacity = x;
[self setNeedsDisplay:YES];
}
At the end of each of the methods, you inform the view that it needs to redraw itself. Near the end of the initWithFramemethod, set opacity to be 1.0:
[path closePath];
opacity = 1.0;
return self;
}
Also in StretchView.m, you need to add compositing of the image to the drawRect: method:
- (void)drawRect:(NSRect)rect{
NSRect bounds = [self bounds];
[[NSColor greenColor] set];
[NSBezierPath fillRect:bounds];
[[NSColor whiteColor] set];
[path fill];
if (image) {
NSRect imageRect;
imageRect.origin = NSZeroPoint;
imageRect.size = [image size];
NSRect drawingRect = imageRect;
[image drawInRect:drawingRect
fromRect:imageRect
operation:NSCompositeSourceOver
fraction:opacity];
}
}
Note that the drawInRect:fromRect:operation:fraction: method composites the image onto the view. The fraction determines the image's opacity.
You should release the image in your dealloc method:
- (void)dealloc
{
[path release];
[image release];
[super dealloc];
}
Build and run your application. You will find a few images in /Developer/Examples/AppKit/Sketch . When you open an image, it will appear in the lower-left corner of your StretchView object.
The View's Coordinate System
The final bit of fun comes from being able to choose the location and dimensions of the image, based on the user's dragging. The mouseDown will indicate one corner of the rectangle where the image will appear, and the mouseUp will indicate the opposite corner. The final application will look something like Figure 18.8.
Figure 18.8. Completed Application
Each view has its own coordinate system. By default, (0, 0) is in the lower-left corner. This is consistent with PDF and PostScript. If you wish, you can change the coordinate system of the view. You can move the origin, change the scale, or rotate the coordinates. The window also has a coordinate system.
If you have two views, a and b, and you need to translate an NSPoint p from b's coordinate system to a's coordinate system, it would look like this:
NSPoint q = [a convertPoint:p fromView:b];
If b is nil, the point is converted from the window's coordinate system.
Mouse events have their locations in the window's coordinate system, so you will nearly always have to convert the point to the local coordinate system. You are going to create variables to hold onto the corners of the rectangle where the image will be drawn.
Add these instance variables to StretchView.h:
NSPoint downPoint; NSPoint currentPoint;
The location of the mouseDown: will be downPoint and currentPoint will be updated by mouseDragged: and mouseUp:.
Edit the mouse event-handling methods to update downPoint and currentPoint:
- (void)mouseDown:(NSEvent *)event { NSPoint p = [event locationInWindow]; downPoint = [self convertPoint:p fromView:nil]; currentPoint = downPoint; [self setNeedsDisplay:YES]; } - (void)mouseDragged:(NSEvent *)event { NSPoint p = [event locationInWindow]; currentPoint = [self convertPoint:p fromView:nil]; [self setNeedsDisplay:YES]; } - (void)mouseUp:(NSEvent *)event { NSPoint p = [event locationInWindow]; currentPoint = [self convertPoint:p fromView:nil]; [self setNeedsDisplay:YES]; }
Add a method to calculate the rectangle based on the two points:
- (NSRect)currentRect { float minX = MIN(downPoint.x, currentPoint.x); float maxX = MAX(downPoint.x, currentPoint.x); float minY = MIN(downPoint.y, currentPoint.y); float maxY = MAX(downPoint.y, currentPoint.y); return NSMakeRect(minX, minY, maxX-minX, maxY-minY); }
(I don't know why, but many people mistype that last method. Look at yours once more before going on. If you get it wrong, the results are disappointing.)
Declare the currentRect method in StretchView.h.
So that the user will see something even if he or she has not dragged, initialize downPoint and currentPoint in thesetImage: method:
- (void)setImage:(NSImage *)newImage { [newImage retain]; [image release]; image = newImage; NSSize imageSize = [newImage size]; downPoint = NSZeroPoint; currentPoint.x = downPoint.x + imageSize.width; currentPoint.y = downPoint.y + imageSize.height; [self setNeedsDisplay:YES]; }
In the drawRect: method, composite the image inside the rectangle:
- (void)drawRect:(NSRect)rect
{
NSRect bounds = [self bounds];
[[NSColor greenColor] set];
[NSBezierPath fillRect:bounds];
[[NSColor whiteColor] set];
[path stroke];
if (image) {
NSRect imageRect;
imageRect.origin = NSZeroPoint;
imageRect.size = [image size];
NSRect drawingRect = [self currentRect];
[image drawInRect:drawingRect
fromRect:imageRect
operation:NSCompositeSourceOver
fraction:opacity];
}
}
Build and run your application. Note that the view doesn't scroll when you drag past the edge. It would be nice if the scroll view would move to allow users to see where they have dragged to, a technique known as autoscrolling.
Autoscrolling
To add autoscrolling to your application, you will send the message autoscroll: to the clip view when the user drags. You will include the event as an argument. Open StretchView.m and add the following line to the mouseDragged: method:
- (void)mouseDragged:(NSEvent *)event
{
NSPoint p = [event locationInWindow];
currentPoint = [self convertPoint:p fromView:nil];
[self autoscroll:event];
[self setNeedsDisplay:YES];
}
Build and run your application.
Note that autoscrolling happens only as you drag. For smoother autoscrolling, most developers will create a timer that sends the view the autoscroll: method periodically while the user is dragging. Timers are discussed in Chapter 24.