views:

509

answers:

2

If you create a status bar app with no windows, how do you respond to events?

My first guess was creating an subclass of NSResponder and override the appropriate methods. However they never get called.

This lead explicitly calling:

[self becomeFirstResponder];

Which also didn't work (and I don't believe is recommended by the Apple Docs)

Is there some way to get my NSResponder subclass in the responder chain?

+1  A: 

What sort of key events are you expecting to receive if you don't have a window?

If you need to intercept key events globally then you will need to use a Quartz Event Tap. You must be very careful with these as throwing an exception in an event tap handler can freeze the window server so you should have exception handling in place.

#import <ApplicationServices/ApplicationServices.h>

//assume CGEventTap eventTap is an ivar or other global

void createEventTap(void)
{
 CFRunLoopSourceRef runLoopSource;

 ///we only want keydown events
 CGEventMask eventMask = (1 << kCGEventKeyDown);

 // Keyboard event taps need Universal Access enabled, 
 // check whether we're allowed to attach an event tap
 if (!AXAPIEnabled()&&!AXIsProcessTrusted()) { 
  // error dialog here 
  NSAlert *alert = [[[NSAlert alloc] init] autorelease];
  [alert addButtonWithTitle:@"OK"];
  [alert setMessageText:@"Could not start event monitoring."];
  [alert setInformativeText:@"Please enable \"access for assistive devices\" in the Universal Access pane of System Preferences."];
  [alert runModal];
  return;
 } 


 //create the event tap
 eventTap = CGEventTapCreate(kCGHIDEventTap, //this intercepts events at the lowest level, where they enter the window server
        kCGHeadInsertEventTap, 
        kCGEventTapOptionDefault, 
        eventMask,
        myCGEventCallback, //this is the callback that we receive when the event fires
        nil); 

 // Create a run loop source.
 runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);

 // Add to the current run loop.
 CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);

 // Enable the event tap.
 CGEventTapEnable(eventTap, true);
}


//the CGEvent callback that does the heavy lifting
CGEventRef myCGEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef theEvent, void *refcon)
{
 //handle the event here
 //if you want to capture the event and prevent it propagating as normal, return NULL.

 //if you want to let the event process as normal, return theEvent.
 return theEvent;
}
Rob Keniger
I'm intercepting option and cmd presses, similar to the apple menu to change the function of menu items. It looks like I might need to go this way.
Corey Floyd
If you need to change the menu title while the menu is already open, then you will definitely need to do it this way. However, you could just implement -menuNeedsUpdate: in your menu delegate and adjust the menu titles based on the active modifiers at that point instead. This won't give you "live" changes to the menu when you press/release modifier keys but it is much simpler than using a CGEventTap.
Rob Keniger
I'm not so concerned about the state of the MSMenuItems as I am performing the correct function when clicked. I technically don't need updates, but I need the correct keyboard state when the user clicks on a menu item. I'd rather not put the user through the extra step by requiring "assistive devices". I found Dave Dribin's HidLib, which after a while I can get to work, but it seems like overkill. Is there no Cocoa abstraction that you can use to just type: [keyboard activeKeys]; Seems like it should be trivial.
Corey Floyd
Yes, you can use [[NSApp currentEvent] modifierFlags] to get the current modifier keys.
Rob Keniger
+1  A: 

Of course the answer is simple:

[NSApp currentEvent]; 

returns the current event in any cocoa app.

Corey Floyd