We already had a basic event loop just to make the window show up, but now we'll handle all the events you're likely to need for your game/app. The setup code is mostly the same; the delegate and event loop have extra code this time.
Build with:
clang main.m -framework Cocoa
Most events are self-explanatory, but I'll explore a few.
case NSEventTypeScrollWheel: {
if ([e hasPreciseScrollingDeltas]) {
const NSTimeInterval time = [e timestamp];
const float dy = [e scrollingDeltaY];
printf ("Precise scroll %f @ %f\n", dy, time);
}
else {
const float dy = [e deltaY];
printf ("Simple scroll %f\n", dy);
}
} break;
Scrolling on MacOS for games is a disaster. Mac uses "precise scrolling," which is great if you want to smoothly change a continuous value with the scroll wheel/ball (like moving around a map, say), but horrible for switching weapons with the scroll wheel in a shooter. You may just need to ask the user what kind of scrolling device they're using. Some old mice *might* send simple scrolls. Every modern, non-Apple mouse I've tried has sent a bunch of precise scroll events for each wheel notch. I don't own an Apple mouse, but apparently they'll send loads of tiny scrolls. The second case (non-Apple modern mice) can be handled fairly well by filtering out any events with a scrollingDeltaY of 0, then recording the last timestamp and ignoring subsequent events with the same timestamp. As for using Apple mice in the discrete way games tend to prefer: good luck!
case NSEventTypeFlagsChanged: {
typedef union {
struct {
uint8_t alpha_shift:1;
uint8_t shift:1;
uint8_t control:1;
uint8_t alternate:1;
uint8_t command:1;
uint8_t numeric_pad:1;
uint8_t help:1;
uint8_t function:1;
};
uint8_t mask;
} osx_event_modifiers_t;
static osx_event_modifiers_t mods_prev = {};
osx_event_modifiers_t mods = {.mask = ([e modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask) >> 16};
if (mods.alpha_shift ^ mods_prev.alpha_shift) printf ("Key %s Alpha Shift\n", mods.alpha_shift ? "pressed" : "released");
if (mods.shift ^ mods_prev.shift) printf ("Key %s Shift\n", mods.shift ? "pressed" : "released");
if (mods.control ^ mods_prev.control) printf ("Key %s Ctrl\n", mods.control ? "pressed" : "released");
if (mods.alternate ^ mods_prev.alternate) printf ("Key %s Alt\n", mods.alternate ? "pressed" : "released");
if (mods.command ^ mods_prev.command) printf ("Key %s Command\n", mods.command ? "pressed" : "released");
if (mods.numeric_pad ^ mods_prev.numeric_pad) printf ("Key %s NumLock\n", mods.numeric_pad ? "pressed" : "released");
if (mods.help ^ mods_prev.help) printf ("Key %s Help\n", mods.help ? "pressed" : "released");
if (mods.function ^ mods_prev.function) printf ("Key %s Function\n", mods.function ? "pressed" : "released");
mods_prev = mods;
} break;
Cocoa sets aside various keys like Alt, CTRL, etc. and sends them as a group of flags instead of normal key events. Here I record the last collective flag state and check for any changes each time this event comes in. Note that some of these keys represent different things on different types of keyboards.
case NSEventTypeApplicationDefined: {
switch ([e subtype]) {
case OSXUserEvent_WindowClose: {
printf ("Window close\n");
quit = true;
} break;
case OSXUserEvent_WindowResize: printf ("Resize %d, %d\n", (int)[e data1], (int)[e data2]); break;
case OSXUserEvent_LostFocus: puts ("Focus Out"); break;
default: puts ("Unknown user event?"); break;
}
} break;
Ah, the user events! Finally, something that just works exactly as you'd expect.
enum { OSXUserEvent_WindowClose, OSXUserEvent_WindowResize, OSXUserEvent_LostFocus, };
These are user event values. You can set them to anything - they're entirely created and used by your own code. I like to just put them in an enum to give them all a unique value.
NSView *content_view = [window contentView];
NSTrackingArea *tracking_area = [[NSTrackingArea alloc] initWithRect:content_view.bounds options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect | NSTrackingEnabledDuringMouseDrag owner:[window delegate] userInfo:nil];
[content_view addTrackingArea:tracking_area];
Adding a tracking area to your window allows you to detect whenever the mouse enters or exits the content area of your window. This is important for discarding mouse movement events when the mouse is outside of your window, or showing/hiding the cursor as it enters/leaves.
-(void)mouseEntered:(NSEvent *)event {
printf("Mouse entered content view tracking area\n");
}
-(void)mouseExited:(NSEvent *)event {
printf("Mouse exited content view tracking area\n");
}
-(void)mouseMoved:(NSEvent *)event {
NSPoint p = [[window contentView] convertPointToBacking:[event locationInWindow]];
printf("Mouse moved inside content view tracking area: %.1f %.1f\n", p.x, p.y);
}
Since we set the NSTrackingMouseEnteredAndExited and NSTrackingMouseMoved options, we must implement the associated functions to be called.
case NSEventTypeMouseMoved:
case NSEventTypeLeftMouseDragged:
case NSEventTypeRightMouseDragged:
case NSEventTypeOtherMouseDragged: {
if (e.window == nil) {
NSRect frame = [window frame];
NSPoint p = [e locationInWindow];
p.x -= frame.origin.x;
p.y -= frame.origin.y;
p = [[window contentView] convertPointToBacking:p];
printf ("Mouse moved outside window: %.1f %.1f\n", p.x, p.y);
}
else {
NSPoint p = [[window contentView] convertPointToBacking:[e locationInWindow]];
printf("Mouse moved: %.1f %.1f\n", p.x, p.y);
}
} break;
We need to detect mouse movements with all 4 of these event types. You'd think you could just use [e locationInWindow] to get the position. But that coordinate is highly inconsistent. If the mouse is in our window, it's relative to the bottom-left corner of our window. If not, it's relative to the global desktop bottom-left corner. And the coordinates are scaled depending on the system HiDPI resolution scaling setting, so even when it's relative to our window, it's scaled differently than our pixels! So, we use convertPointToBacking to convert it to the coordinate space of the content view. We also manually map it relative to the bottom-left corner of our window when it's outside our window.
-(NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication*)sender {
NSEvent *event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined location:(NSPoint){0,0} modifierFlags:0 timestamp:[[NSProcessInfo processInfo] systemUptime] windowNumber:[window windowNumber] context:nil subtype:OSXUserEvent_WindowClose data1:0 data2:0];
[app postEvent:event atStart:true];
return NSTerminateCancel;
}
We create an event with [NSEvent otherEventWithType:NSEventTypeApplicationDefined ...]. subtype is where we use our custom OSXUserEvent enum. In [app postEvent ...] you have the choice of posting it to the start of the event queue, so that it's the next event to be processed. I only use this for quit events, since there's no point processing remaining events.
-(void)windowDidResignKey:(NSNotification*)notification {
NSEvent *event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined location:(NSPoint){0,0} modifierFlags:0 timestamp:[[NSProcessInfo processInfo] systemUptime] windowNumber:[window windowNumber] context:nil subtype:OSXUserEvent_LostFocus data1:0 data2:0];
[app postEvent:event atStart:false];
}
windowDidResignKey means your window is no longer the "key window" - Apple's term for the window with focus.
@implementation Delegate {
bool live_resizing;
}
-(void)windowDidResize:(NSNotification *)notification {
if (live_resizing) return;
[self SendResizeEvent];
}
-(void)windowWillStartLiveResize:(NSNotification *)notification {
live_resizing = true;
}
-(void)windowDidEndLiveResize:(NSNotification *)notification {
live_resizing = false;
[self SendResizeEvent];
}
-(void)SendResizeEvent {
NSSize size = [[window contentView] convertRectToBacking:[[window contentView] bounds]].size;
NSEvent *event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined location:(NSPoint){0,0} modifierFlags:0 timestamp:[[NSProcessInfo processInfo] systemUptime] windowNumber:[window windowNumber] context:nil subtype:OSXUserEvent_WindowResize data1:size.width data2:size.height];
[app postEvent:event atStart:false];
}
The window can either be instantaneously resized, or "live resized" when the user drags the edges/corners. Whenever the window is finished resizing, we call our own SendResizeEvent function to send a user event with the new size. If we're live resizing, we avoid sending thousands of resize events by using this live_resizing member variable.