aboutsummaryrefslogtreecommitdiff
path: root/AppKit
diff options
context:
space:
mode:
authorGravatar thomasvl@gmail.com <thomasvl@gmail.com@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3>2008-12-12 15:24:34 +0000
committerGravatar thomasvl@gmail.com <thomasvl@gmail.com@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3>2008-12-12 15:24:34 +0000
commit2e8516354aacef064d01425808da06d2cdcb4791 (patch)
tree9da4758828930280d32f18d54ece7a249df742c7 /AppKit
parent9f64d056dd70f2f938ac6f5adb8e75b650dc2e1a (diff)
- GTMStackTrace works on 10.5+ (and iPhone) using NSThread to build the call stack.
- Added GTM_EXTERN that makes it easier to mix and match objc and objc++ code. - Added GTMHotKeysTextField for display and editing of hot key settings. - Added GTMCarbonEvent for dealing with Carbon Events and HotKeys in a ObjC like way. - Backported the Atomic Barrier Swap functions for Objective C back to Tiger. - Added a variety of new functions to GTMUnitTestingUtilities for checking if the screensaver is in the way, waiting on user events, and generating keystrokes. - If you are using any Carbon routines that log (DebugStr, AssertMacros.h) and use GTMUnitTestDevLog, the log routines now go through _GTMDevLog so that they can be caught in GTMUnitTestDevLog and verified like any _GTMDevLog calls you may make. For an example of this in action see GTMCarbonEventTest.m. - Added GTMFileSystemKQueue. It provides a simple wrapper for kqueuing something in the file system and tracking changes to it. - RunIPhoneUnitTest.sh now cleans up the user home directory and creates a documents directory within it, used when requesting a NSDocumentDirectory. - Added GTMNSFileManager+Carbon which contains routines for path <-> Alias conversion and path <-> FSRef conversion. - Added GTMNSArray+Merge for merging one array into another with or without a custom merging function, returning a new array with the merged contents.
Diffstat (limited to 'AppKit')
-rw-r--r--AppKit/GTMCarbonEvent.h380
-rw-r--r--AppKit/GTMCarbonEvent.m709
-rw-r--r--AppKit/GTMCarbonEventTest.m360
-rw-r--r--AppKit/GTMGetURLHandler.m82
-rw-r--r--AppKit/GTMGetURLHandlerTest.m85
-rw-r--r--AppKit/GTMHotKeyTextField.h127
-rw-r--r--AppKit/GTMHotKeyTextField.m1009
-rw-r--r--AppKit/GTMHotKeyTextFieldTest.m204
-rw-r--r--AppKit/GTMLoginItems.h7
9 files changed, 2931 insertions, 32 deletions
diff --git a/AppKit/GTMCarbonEvent.h b/AppKit/GTMCarbonEvent.h
new file mode 100644
index 0000000..83482e9
--- /dev/null
+++ b/AppKit/GTMCarbonEvent.h
@@ -0,0 +1,380 @@
+//
+// GTMCarbonEvent.h
+//
+// Copyright 2006-2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy
+// of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+//
+
+#import <Foundation/Foundation.h>
+#import <Carbon/Carbon.h>
+
+#import "GTMDefines.h"
+
+@class GTMCarbonEventHandler;
+
+// Objective C wrapper for a Carbon Event
+@interface GTMCarbonEvent : NSObject <NSCopying> {
+ @private
+ EventRef event_; //Event we are wrapping. STRONG
+}
+
+
+// Create an event of class |inClass| and kind |inKind|
+//
+// Returns:
+// Autoreleased GTMCarbonEvent
+//
++ (id)eventWithClass:(UInt32)inClass kind:(UInt32)kind;
+
+// Create an event based on |event|. Retains |event|.
+//
+// Returns:
+// Autoreleased GTMCarbonEvent
+//
++ (id)eventWithEvent:(EventRef)event;
+
+// Create an event based on the event currently being handled.
+//
+// Returns:
+// Autoreleased GTMCarbonEvent
+//
++ (id)currentEvent;
+
+// Create an event of class |inClass| and kind |inKind|
+//
+// Returns:
+// GTMCarbonEvent
+//
+- (id)initWithClass:(UInt32)inClass kind:(UInt32)kind;
+
+// Create an event based on |event|. Retains |event|.
+//
+// Returns:
+// GTMCarbonEvent
+//
+- (id)initWithEvent:(EventRef)event;
+
+// Get the event's class.
+//
+// Returns:
+// event class
+//
+- (UInt32)eventClass;
+
+// Get the event's kind.
+//
+// Returns:
+// event kind
+//
+- (UInt32)eventKind;
+
+// Set the event's time.
+//
+// Arguments:
+// time - the time you want associated with the event
+//
+- (void)setTime:(EventTime)eventTime;
+
+// Get the event's time.
+//
+// Returns:
+// the time associated with the event
+//
+- (EventTime)time;
+
+// Get the event's eventref for passing to other carbon functions.
+//
+// Returns:
+// the event ref associated with the event
+//
+- (EventRef)event;
+
+// Sets (or adds) a parameter to an event. Try not to use this function
+// directly. Look at the PARAM_TEMPLATE_DECL/DEFN macros below.
+//
+// Arguments:
+// name - the parameter name.
+// type - the parameter type.
+// size - the size of the data that |data| points to.
+// data - pointer to the data you want to set the parameter to.
+//
+- (void)setParameterNamed:(EventParamName)name
+ type:(EventParamType)type
+ size:(ByteCount)size
+ data:(const void *)data;
+
+
+// Gets a parameter from an event. Try not to use this function
+// directly. Look at the PARAM_TEMPLATE_DECL/DEFN macros below.
+//
+// Arguments:
+// name - the parameter name.
+// type - the parameter type.
+// size - the size of the data that |data| points to.
+// data - pointer to the buffer that you want to fill with your data.
+//
+// Returns:
+// YES is parameter is retrieved successfully. NO if parameter doesn't exist.
+//
+- (BOOL)getParameterNamed:(EventParamName)name
+ type:(EventParamType)type
+ size:(ByteCount)size
+ data:(void *)data;
+
+// Gets a the size of a parameter from an event.
+//
+// Arguments:
+// name - the parameter name.
+// type - the parameter type.
+//
+// Returns:
+// The size of the buffer required to hold the parameter. 0 if parameter
+// doesn't exist.
+//
+- (ByteCount)sizeOfParameterNamed:(EventParamName)name
+ type:(EventParamType)type;
+
+// Sends event to an event target with options
+//
+// Arguments:
+// target - target to send event to.
+// options - options to send event. See SendEventToEventTargetWithOptions
+// for details.
+//
+// Returns:
+// OSStatus value.
+//
+- (OSStatus)sendToTarget:(GTMCarbonEventHandler *)target
+ options:(OptionBits)options;
+
+// Post event to an event queue.
+//
+// Arguments:
+// queue - queue to post it to.
+// priority - priority to post it with
+//
+// Returns:
+// OSStatus value.
+//
+- (OSStatus)postToQueue:(EventQueueRef)queue priority:(EventPriority)priority;
+
+// Post event to current queue with standard priority.
+//
+- (void)postToCurrentQueue;
+
+// Post event to main queue with standard priority.
+//
+- (void)postToMainQueue;
+
+@end
+
+// Macros for defining simple set/get parameter methods for GTMCarbonEvent. See
+// the category GTMCarbonEvent (GTMCarbonEventGettersAndSetters) for an example
+// of their use. GTM_PARAM_TEMPLATE_DECL2/DEFN2 is for the case where the
+// parameter name is different than the parameter type (rare, but it does
+// occur...e.g. for a Rect, the name is typeQDRectangle, and the type is Rect,
+// so it would be GTM_PARAM_TEMPLATE_DECL2(QDRectangle, Rect) ). In most cases
+// you will just use GTM_PARAM_TEMPLATE_DECL/DEFN.
+#define GTM_PARAM_TEMPLATE_DECL2(paramName, paramType) \
+- (void)set##paramName##ParameterNamed:(EventParamName)name data:(paramType *)data; \
+- (BOOL)get##paramName##ParameterNamed:(EventParamName)name data:(paramType *)data;
+
+#define GTM_PARAM_TEMPLATE_DEFN2(paramName, paramType) \
+- (void)set##paramName##ParameterNamed:(EventParamName)name data:(paramType *)data { \
+[self setParameterNamed:name type:type##paramName size:sizeof(paramType) data:data]; \
+} \
+- (BOOL)get##paramName##ParameterNamed:(EventParamName)name data:(paramType *)data { \
+return [self getParameterNamed:name type:type##paramName size:sizeof(paramType) data:data]; \
+}
+
+#define GTM_PARAM_TEMPLATE_DECL(paramType) GTM_PARAM_TEMPLATE_DECL2(paramType, paramType)
+#define GTM_PARAM_TEMPLATE_DEFN(paramType) GTM_PARAM_TEMPLATE_DEFN2(paramType, paramType)
+
+
+// Category defining some basic types that we want to be able to easily set and
+// get from GTMCarbonEvents
+@interface GTMCarbonEvent (GTMCarbonEventGettersAndSetters)
+GTM_PARAM_TEMPLATE_DECL(UInt32)
+GTM_PARAM_TEMPLATE_DECL(EventHotKeyID)
+@end
+
+// Utility function for converting between modifier types
+// Arguments:
+// inCocoaModifiers - keyboard modifiers in carbon form
+// (NSCommandKeyMask etc)
+// Returns:
+// Carbon modifiers equivalent to |inCocoaModifiers| (cmdKey etc)
+GTM_EXTERN UInt32 GTMCocoaToCarbonKeyModifiers(NSUInteger inCocoaModifiers);
+
+// Utility function for converting between modifier types
+// Arguments:
+// inCarbonModifiers - keyboard modifiers in carbon form (cmdKey etc)
+// Returns:
+// cocoa modifiers equivalent to |inCocoaModifiers| (NSCommandKeyMask etc)
+GTM_EXTERN NSUInteger GTMCarbonToCocoaKeyModifiers(UInt32 inCarbonModifiers);
+
+// An "abstract" superclass for objects that handle events such as
+// menus, HIObjects, etc.
+//
+// Subclasses are expected to override the eventTarget and
+// handleEvent:handler: methods to customize them.
+@interface GTMCarbonEventHandler : NSObject {
+ @private
+ // handler we are wrapping
+ // lazily created in the eventHandler method
+ EventHandlerRef eventHandler_;
+ __weak id delegate_; // Our delegate
+ // Does our delegate respond to the gtm_eventHandler:receivedEvent:handler:
+ // selector? Cached for performance reasons.
+ BOOL delegateRespondsToHandleEvent_;
+}
+
+// Registers the event handler to listen for |events|.
+//
+// Arguments:
+// events - an array of EventTypeSpec. The events to register for.
+// count - the number of EventTypeSpecs in events.
+//
+- (void)registerForEvents:(const EventTypeSpec *)events count:(size_t)count;
+
+// Causes the event handler to stop listening for |events|.
+//
+// Arguments:
+// events - an array of EventTypeSpec. The events to register for.
+// count - the number of EventTypeSpecs in events.
+//
+- (void)unregisterForEvents:(const EventTypeSpec *)events count:(size_t)count;
+
+// To be overridden by subclasses to respond to events.
+//
+// All subclasses should call [super handleEvent:handler:] if they
+// don't handle the event themselves.
+//
+// Arguments:
+// event - the event to be handled
+// handler - the call ref in case you want to call CallNextEventHandler
+// in your method
+// Returns:
+// OSStatus - usually either noErr or eventNotHandledErr
+//
+- (OSStatus)handleEvent:(GTMCarbonEvent *)event
+ handler:(EventHandlerCallRef)handler;
+
+// To be overridden by subclasses to return the event target for the class.
+// GTMCarbonEventHandler's implementation returns NULL.
+//
+// Returns:
+// The event target ref.
+//
+- (EventTargetRef)eventTarget;
+
+// Gets the underlying EventHandlerRef for that this class wraps.
+//
+// Returns:
+// The EventHandlerRef this class wraps.
+//
+- (EventHandlerRef)eventHandler;
+
+// Gets the delegate for the handler
+//
+// Returns:
+// the delegate
+- (id)delegate;
+
+// Sets the delegate for the handler
+//
+// Arguments:
+// delegate - the delegate to set to
+- (void)setDelegate:(id)delegate;
+
+@end
+
+// Category for methods that a delegate of GTMCarbonEventHandlerDelegate may
+// want to implement.
+@interface NSObject (GTMCarbonEventHandlerDelegate)
+
+// If a delegate implements this method it gets called before every event
+// that the handler gets sent. If it returns anything but eventNotHandledErr,
+// the handlers handlerEvent:handler: method will not be called, and
+// the return value returned by the delegate will be returned back to the
+// carbon event dispatch system. This allows you to override any method
+// that a handler may implement.
+//
+// Arguments:
+// delegate - the delegate to set to
+//
+- (OSStatus)gtm_eventHandler:(GTMCarbonEventHandler *)sender
+ receivedEvent:(GTMCarbonEvent *)event
+ handler:(EventHandlerCallRef)handler;
+
+@end
+
+// A general OSType for use when setting properties on GTMCarbonEvent objects.
+// This is the "signature" as part of commandIDs, controlsIDs, and properties.
+// 'GooG'
+GTM_EXTERN const OSType kGTMCarbonFrameworkSignature;
+
+// An event handler class representing the event monitor event handler
+//
+// there is only one of these per application. This way you can put
+// event handlers directly on the dispatcher if necessary.
+@interface GTMCarbonEventMonitorHandler : GTMCarbonEventHandler
+// Accessor to get the GTMCarbonEventMonitorHandler singleton.
+//
+// Returns:
+// pointer to the GTMCarbonEventMonitorHandler singleton.
++ (GTMCarbonEventMonitorHandler *)sharedEventMonitorHandler;
+@end
+
+// An event handler class representing the toolbox dispatcher event handler
+//
+// there is only one of these per application. This way you can put
+// event handlers directly on the dispatcher if necessary.
+@interface GTMCarbonEventDispatcherHandler : GTMCarbonEventHandler {
+ @private
+ NSMutableDictionary *hotkeys_; // Collection of registered hotkeys
+}
+
+// Accessor to get the GTMCarbonEventDispatcherHandler singleton.
+//
+// Returns:
+// pointer to the GTMCarbonEventDispatcherHandler singleton.
++ (GTMCarbonEventDispatcherHandler *)sharedEventDispatcherHandler;
+
+// Registers a hotkey. When the hotkey is executed by the user, target will be
+// called with selector.
+// Arguments:
+// keyCode - the virtual keycode of the hotkey
+// cocoaModifiers - the modifiers that need to be used with |keyCode|. NB
+// that these are cocoa modifiers, so NSCommandKeyMask etc.
+// target - instance that will get |action| called when the hotkey fires
+// action - the method to call on |target| when the hotkey fires
+// onPress - is YES, the hotkey fires on the keydown (usual) otherwise
+// it fires on the key up.
+// Returns:
+// a EventHotKeyRef that you can use with other Carbon functions, or for
+// unregistering the hotkey. Note that all hotkeys are unregistered
+// automatically when an app quits. Will be NULL on failure.
+- (EventHotKeyRef)registerHotKey:(NSUInteger)keyCode
+ modifiers:(NSUInteger)cocoaModifiers
+ target:(id)target
+ action:(SEL)action
+ whenPressed:(BOOL)onPress;
+
+// Unregisters a hotkey previously registered with registerHotKey.
+// Arguments:
+// keyRef - the EventHotKeyRef to unregister
+- (void)unregisterHotKey:(EventHotKeyRef)keyRef;
+
+@end
diff --git a/AppKit/GTMCarbonEvent.m b/AppKit/GTMCarbonEvent.m
new file mode 100644
index 0000000..270a078
--- /dev/null
+++ b/AppKit/GTMCarbonEvent.m
@@ -0,0 +1,709 @@
+//
+// GTMCarbonEvent.m
+//
+// Copyright 2006-2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy
+// of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+//
+
+#import "GTMCarbonEvent.h"
+#import "GTMObjectSingleton.h"
+#import "GTMDebugSelectorValidation.h"
+
+// Wrapper for all the info we need about a hotkey that we can store in a
+// Foundation storage class. We expecct selector to have this signature:
+// - (void)hitHotKey:sender;
+@interface GTMCarbonHotKey : NSObject {
+ @private
+ EventHotKeyID id_; // EventHotKeyID for this hotkey.
+ id target_; // Object we are going to call when the hotkey is hit
+ SEL selector_; // Selector we are going to call on target_
+ BOOL onKeyDown_; // Do we do it on key down or on key up?
+}
+
+// Create a HotKey record
+// Arguments:
+// keyID - id of the hotkey
+// target - object we are going to call when the hotkey is hit
+// action - selector we are going to call on target
+// whenPressed - do we do it on key down or key up?
+// Returns:
+// a hotkey record, or nil on failure
+- (id)initWithHotKey:(EventHotKeyID)keyID
+ target:(id)target
+ action:(SEL)selector
+ whenPressed:(BOOL)onKeyDown;
+
+// Does this record match key |keyID|
+// Arguments:
+// keyID - the id to match against
+// Returns:
+// Yes if we match this key id
+- (BOOL)matchesHotKeyID:(EventHotKeyID)keyID;
+
+// Make target perform selector
+// Returns:
+// Yes if handled
+- (BOOL)sendAction:(id)sender;
+
+// Do we do it on key down or key up?
+// Returns:
+// Yes if on keydown
+- (BOOL)onKeyDown;
+
+@end
+
+@implementation GTMCarbonEvent
+
+// Create an event of class |inClass| and kind |inKind|
+//
+// Returns:
+// Autoreleased GTMCarbonEvent
+//
++ (id)eventWithClass:(UInt32)inClass kind:(UInt32)kind {
+ return [[[[self class] alloc] initWithClass:inClass kind:kind] autorelease];
+}
+
+
+// Create an event based on |event|. Retains |event|.
+//
+// Returns:
+// Autoreleased GTMCarbonEvent
+//
++ (id)eventWithEvent:(EventRef)event {
+ return [[[[self class] alloc] initWithEvent:event] autorelease];
+}
+
+
+// Create an event based on the event currently being handled.
+//
+// Returns:
+// Autoreleased GTMCarbonEvent
+//
++ (id)currentEvent {
+ return [[self class] eventWithEvent:GetCurrentEvent()];
+}
+
+
+// Create an event of class |inClass| and kind |inKind|
+//
+// Returns:
+// GTMCarbonEvent
+//
+- (id)initWithClass:(UInt32)inClass kind:(UInt32)kind {
+ if ((self = [super init])) {
+ verify_noerr(CreateEvent(kCFAllocatorDefault, inClass, kind,
+ 0, kEventAttributeNone, &event_));
+ }
+ return self;
+}
+
+
+// Create an event based on |event|. Retains |event|.
+//
+// Returns:
+// GTMCarbonEvent
+//
+- (id)initWithEvent:(EventRef)event {
+ if ((self = [super init])) {
+ if (event) {
+ event_ = RetainEvent(event);
+ }
+ }
+ return self;
+}
+
+
+// This does a proper event copy, but ignores the |zone|. No way to do a copy
+// of an event into a specific zone.
+//
+// Arguments:
+// zone - the zone to copy to
+// Returns:
+// the copied event. nil on failure
+- (id)copyWithZone:(NSZone *)zone {
+ GTMCarbonEvent *carbonEvent = nil;
+ EventRef newEvent = CopyEvent([self event]);
+ if (newEvent) {
+ carbonEvent = [[[self class] allocWithZone:zone] initWithEvent:newEvent];
+ ReleaseEvent(newEvent);
+ }
+ return carbonEvent;
+}
+
+- (void)finalize {
+ if (event_) {
+ ReleaseEvent(event_);
+ event_ = NULL;
+ }
+ [super finalize];
+}
+
+// releases our retained event
+//
+- (void)dealloc {
+ if (event_) {
+ ReleaseEvent(event_);
+ event_ = NULL;
+ }
+ [super dealloc];
+}
+
+// description utliity for debugging
+//
+- (NSString *)description {
+ char cls[5];
+ UInt32 kind;
+
+ // Need everything bigendian if we are printing out the class as a "string"
+ *((UInt32 *)cls) = CFSwapInt32HostToBig([self eventClass]);
+ kind = [self eventKind];
+ cls[4] = 0;
+ return [NSString stringWithFormat:@"GTMCarbonEvent '%s' %d", cls, kind];
+}
+
+
+// Get the event's class.
+//
+// Returns:
+// event class
+//
+- (UInt32)eventClass {
+ return GetEventClass(event_);
+}
+
+
+// Get the event's kind.
+//
+// Returns:
+// event kind
+//
+- (UInt32)eventKind {
+ return GetEventKind(event_);
+}
+
+
+// Set the event's time.
+//
+// Arguments:
+// time - the time you want associated with the event
+//
+- (void)setTime:(EventTime)eventTime {
+ verify_noerr(SetEventTime(event_, eventTime));
+}
+
+
+// Get the event's time.
+//
+// Returns:
+// the time associated with the event
+//
+- (EventTime)time {
+ return GetEventTime(event_);
+}
+
+
+// Get the event's eventref for passing to other carbon functions.
+//
+// Returns:
+// the event ref associated with the event
+//
+- (EventRef)event {
+ return event_;
+}
+
+
+// Sends event to an event target with options
+//
+// Arguments:
+// target - target to send event to.
+// options - options to send event. See SendEventToEventTargetWithOptions
+// for details.
+//
+// Returns:
+// OSStatus value.
+//
+- (OSStatus)sendToTarget:(GTMCarbonEventHandler *)target
+ options:(OptionBits)options {
+ return SendEventToEventTargetWithOptions(event_,
+ [target eventTarget], options);
+}
+
+// Post event to an event queue.
+//
+// Arguments:
+// queue - queue to post it to.
+// priority - priority to post it with
+//
+// Returns:
+// OSStatus value.
+//
+- (OSStatus)postToQueue:(EventQueueRef)queue priority:(EventPriority)priority {
+ return PostEventToQueue(queue, event_, priority);
+}
+
+
+// Post event to current queue with standard priority.
+//
+- (void)postToCurrentQueue {
+ verify_noerr([self postToQueue:GetCurrentEventQueue()
+ priority:kEventPriorityStandard]);
+}
+
+
+// Post event to main queue with standard priority.
+//
+- (void)postToMainQueue {
+ verify_noerr([self postToQueue:GetMainEventQueue()
+ priority:kEventPriorityStandard]);
+}
+
+
+// Sets (or adds) a parameter to an event. Try not to use this function
+// directly. Look at the PARAM_TEMPLATE_DECL/DEFN macros below.
+//
+// Arguments:
+// name - the parameter name.
+// type - the parameter type.
+// size - the size of the data that |data| points to.
+// data - pointer to the data you want to set the parameter to.
+//
+- (void)setParameterNamed:(EventParamName)name
+ type:(EventParamType)type
+ size:(ByteCount)size
+ data:(const void *)data {
+ verify_noerr(SetEventParameter(event_, name, type, size, data));
+}
+
+
+// Gets a parameter from an event. Try not to use this function
+// directly. Look at the PARAM_TEMPLATE_DECL/DEFN macros below.
+//
+// Arguments:
+// name - the parameter name.
+// type - the parameter type.
+// size - the size of the data that |data| points to.
+// data - pointer to the buffer that you want to fill with your data.
+//
+// Returns:
+// YES is parameter is retrieved successfully. NO if parameter doesn't exist.
+//
+- (BOOL)getParameterNamed:(EventParamName)name
+ type:(EventParamType)type
+ size:(ByteCount)size
+ data:(void *)data {
+ OSStatus status = GetEventParameter(event_, name, type,
+ NULL, size, NULL, data);
+ return status == noErr;
+}
+
+
+// Gets a the size of a parameter from an event.
+//
+// Arguments:
+// name - the parameter name.
+// type - the parameter type.
+//
+// Returns:
+// The size of the buffer required to hold the parameter. 0 if parameter
+// doesn't exist.
+//
+- (ByteCount)sizeOfParameterNamed:(EventParamName)name
+ type:(EventParamType)type {
+ ByteCount size = 0;
+ verify_noerr(GetEventParameter(event_, name, type, NULL, 0, &size, NULL));
+ return size;
+}
+
+@end
+
+@implementation GTMCarbonEvent (GTMCarbonEventGettersAndSetters)
+GTM_PARAM_TEMPLATE_DEFN(UInt32)
+GTM_PARAM_TEMPLATE_DEFN(EventHotKeyID)
+@end
+
+UInt32 GTMCocoaToCarbonKeyModifiers(NSUInteger inCocoaModifiers) {
+ UInt32 carbModifiers = 0;
+ if (inCocoaModifiers & NSAlphaShiftKeyMask) carbModifiers |= alphaLock;
+ if (inCocoaModifiers & NSShiftKeyMask) carbModifiers |= shiftKey;
+ if (inCocoaModifiers & NSControlKeyMask) carbModifiers |= controlKey;
+ if (inCocoaModifiers & NSAlternateKeyMask) carbModifiers |= optionKey;
+ if (inCocoaModifiers & NSCommandKeyMask) carbModifiers |= cmdKey;
+ return carbModifiers;
+}
+
+NSUInteger GTMCarbonToCocoaKeyModifiers(UInt32 inCarbonModifiers) {
+ NSUInteger nsModifiers = 0;
+ if (inCarbonModifiers & alphaLock) nsModifiers |= NSAlphaShiftKeyMask;
+ if (inCarbonModifiers & shiftKey) nsModifiers |= NSShiftKeyMask;
+ if (inCarbonModifiers & controlKey) nsModifiers |= NSControlKeyMask;
+ if (inCarbonModifiers & optionKey) nsModifiers |= NSAlternateKeyMask;
+ if (inCarbonModifiers & cmdKey) nsModifiers |= NSCommandKeyMask;
+ return nsModifiers;
+}
+
+const OSType kGTMCarbonFrameworkSignature = 'GTM ';
+
+@implementation GTMCarbonEventHandler
+
+// Does our delegate respond to eventHandler:receivedEvent:handler:
+//
+// Returns:
+// YES if delegate responds to eventHandler:receivedEvent:handler:
+- (BOOL) delegateRespondsToHandleEvent {
+ return delegateRespondsToHandleEvent_;
+}
+
+// Registers the event handler to listen for |events|.
+//
+// Arguments:
+// events - an array of EventTypeSpec. The events to register for.
+// count - the number of EventTypeSpecs in events.
+//
+- (void)registerForEvents:(const EventTypeSpec *)events count:(size_t)count {
+ verify_noerr(AddEventTypesToHandler([self eventHandler], count, events));
+}
+
+// Causes the event handler to stop listening for |events|.
+//
+// Arguments:
+// events - an array of EventTypeSpec. The events to register for.
+// count - the number of EventTypeSpecs in events.
+//
+- (void)unregisterForEvents:(const EventTypeSpec *)events count:(size_t)count {
+ verify_noerr(RemoveEventTypesFromHandler([self eventHandler], count, events));
+}
+
+// To be overridden by subclasses to respond to events. All subclasses should
+// call [super handleEvent:handler:] if they don't handle the event themselves.
+//
+// Arguments:
+// event - the event to be handled
+// handler - the call ref in case you want to call CallNextEventHandler
+// in your method
+// Returns:
+// OSStatus - usually either noErr or eventNotHandledErr
+//
+- (OSStatus)handleEvent:(GTMCarbonEvent *)event
+ handler:(EventHandlerCallRef)handler {
+ OSStatus status = eventNotHandledErr;
+ require(event, CantUseParams);
+ require(handler, CantUseParams);
+ require([event event], CantUseParams);
+ status = CallNextEventHandler(handler, [event event]);
+CantUseParams:
+ return status;
+}
+
+// To be overridden by subclasses to return the event target for the class.
+// GTMCarbonEventHandler's implementation returns NULL.
+//
+// Returns:
+// The event target ref.
+//
+- (EventTargetRef)eventTarget {
+ // Defaults implementation needs to be overridden
+ return NULL;
+}
+
+// C callback for our registered EventHandlerUPP.
+//
+// Arguments:
+// inHandler - handler given to us from Carbon Event system
+// inEvent - the event we are handling
+// inUserData - refcon that we gave to the carbon event system. Is a
+// GTMCarbonEventHandler in disguise.
+// Returns:
+// status of event handler
+//
+static OSStatus EventHandler(EventHandlerCallRef inHandler,
+ EventRef inEvent,
+ void *inUserData) {
+ GTMCarbonEvent *event = [GTMCarbonEvent eventWithEvent:inEvent];
+ GTMCarbonEventHandler *handler= (GTMCarbonEventHandler *)inUserData;
+ check([handler isKindOfClass:[GTMCarbonEventHandler class]]);
+
+ // First check to see if our delegate cares about this event. If the delegate
+ // handles it (i.e responds to it and does not return eventNotHandledErr) we
+ // do not pass it on to default handling.
+ OSStatus status = eventNotHandledErr;
+ if ([handler delegateRespondsToHandleEvent]) {
+ status = [[handler delegate] gtm_eventHandler:handler
+ receivedEvent:event
+ handler:inHandler];
+ }
+ if (status == eventNotHandledErr) {
+ status = [handler handleEvent:event handler:inHandler];
+ }
+ return status;
+}
+
+// Gets the underlying EventHandlerRef for that this class wraps.
+//
+// Returns:
+// The EventHandlerRef this class wraps.
+//
+- (EventHandlerRef)eventHandler {
+ if (!eventHandler_) {
+ static EventHandlerUPP sHandlerProc = NULL;
+ if ( sHandlerProc == NULL ) {
+ sHandlerProc = NewEventHandlerUPP(EventHandler);
+ }
+ verify_noerr(InstallEventHandler([self eventTarget],
+ sHandlerProc, 0,
+ NULL, self, &eventHandler_));
+ }
+ return eventHandler_;
+}
+
+// Gets the delegate for the handler
+//
+// Returns:
+// the delegate
+- (id)delegate {
+ return delegate_;
+}
+
+// Sets the delegate for the handler and caches whether it responds to
+// the eventHandler:receivedEvent:handler: selector for performance purposes.
+//
+// Arguments:
+// delegate - the delegate for the handler
+- (void)setDelegate:(id)delegate {
+ delegate_ = delegate;
+ SEL selector = @selector(gtm_eventHandler:receivedEvent:handler:);
+ delegateRespondsToHandleEvent_ = [delegate respondsToSelector:selector];
+}
+
+@end
+
+@implementation GTMCarbonEventMonitorHandler
+
+GTMOBJECT_SINGLETON_BOILERPLATE(GTMCarbonEventMonitorHandler,
+ sharedEventMonitorHandler);
+
+- (EventTargetRef)eventTarget {
+ return GetEventMonitorTarget();
+}
+
+@end
+
+@implementation GTMCarbonEventDispatcherHandler
+
+GTMOBJECT_SINGLETON_BOILERPLATE(GTMCarbonEventDispatcherHandler,
+ sharedEventDispatcherHandler);
+
+// Register for the events we handle, and set up the dictionaries we need
+// to keep track of the hotkeys and commands that we handle.
+// Returns:
+// GTMCarbonApplication or nil on failure
+- (id)init {
+ if ((self = [super init])) {
+ static EventTypeSpec events[] = {
+ { kEventClassKeyboard, kEventHotKeyPressed },
+ { kEventClassKeyboard, kEventHotKeyReleased },
+ };
+ [self registerForEvents:events count:GetEventTypeCount(events)];
+ hotkeys_ = [[NSMutableDictionary alloc] initWithCapacity:0];
+ }
+ return self;
+}
+
+// COV_NF_START
+// Singleton, we never get released. Just here for completeness.
+- (void)dealloc {
+ [hotkeys_ release];
+ [super dealloc];
+}
+// COV_NF_END
+
+- (EventTargetRef)eventTarget {
+ return GetEventDispatcherTarget();
+}
+
+// Registers a hotkey. When the hotkey is executed by the user, target will be
+// called with selector.
+// Arguments:
+// keyCode - the virtual keycode of the hotkey
+// cocoaModifiers - the modifiers that need to be used with |keyCode|. NB
+// that these are cocoa modifiers, so NSCommandKeyMask etc.
+// target - instance that will get |action| called when the hotkey fires
+// action - the method to call on |target| when the hotkey fires
+// onKeyDown - is YES, the hotkey fires on the keydown (usual) otherwise
+// it fires on the key up.
+// Returns:
+// a EventHotKeyRef that you can use with other Carbon functions, or for
+// unregistering the hotkey. Note that all hotkeys are unregistered
+// automatically when an app quits. Will be NULL on failure.
+- (EventHotKeyRef)registerHotKey:(NSUInteger)keyCode
+ modifiers:(NSUInteger)cocoaModifiers
+ target:(id)target
+ action:(SEL)selector
+ whenPressed:(BOOL)onKeyDown {
+ static UInt32 sCurrentID = 0;
+
+ EventHotKeyRef theRef = NULL;
+ EventHotKeyID keyID;
+ keyID.signature = kGTMCarbonFrameworkSignature;
+ keyID.id = ++sCurrentID;
+ GTMCarbonHotKey *newKey = [[[GTMCarbonHotKey alloc] initWithHotKey:keyID
+ target:target
+ action:selector
+ whenPressed:onKeyDown]
+ autorelease];
+ require(newKey, CantCreateKey);
+ require_noerr(RegisterEventHotKey((UInt32)keyCode,
+ GTMCocoaToCarbonKeyModifiers(cocoaModifiers),
+ keyID,
+ [self eventTarget],
+ 0,
+ &theRef), CantRegisterHotkey);
+
+
+ [hotkeys_ setObject:newKey forKey:[NSValue valueWithPointer:theRef]];
+CantCreateKey:
+CantRegisterHotkey:
+ return theRef;
+}
+
+// Unregisters a hotkey previously registered with registerHotKey.
+// Arguments:
+// keyRef - the EventHotKeyRef to unregister
+- (void)unregisterHotKey:(EventHotKeyRef)keyRef {
+ NSValue *key = [NSValue valueWithPointer:keyRef];
+ check([hotkeys_ objectForKey:key] != nil);
+ [hotkeys_ removeObjectForKey:key];
+ verify_noerr(UnregisterEventHotKey(keyRef));
+}
+
+// A hotkey has been hit. See if it is one of ours, and if so fire it.
+// Arguments:
+// event - the hotkey even that was received
+// Returns:
+// Yes if handled.
+- (BOOL)handleHotKeyEvent:(GTMCarbonEvent *)event {
+ EventHotKeyID keyID;
+ BOOL handled = [event getEventHotKeyIDParameterNamed:kEventParamDirectObject
+ data:&keyID];
+ if (handled) {
+ NSEnumerator *dictEnumerator = [hotkeys_ objectEnumerator];
+ GTMCarbonHotKey *hotkey;
+ while ((hotkey = [dictEnumerator nextObject])) {
+ if ([hotkey matchesHotKeyID:keyID]) {
+ EventKind kind = [event eventKind];
+ BOOL onKeyDown = [hotkey onKeyDown];
+ if ((kind == kEventHotKeyPressed && onKeyDown) ||
+ (kind == kEventHotKeyReleased && !onKeyDown)) {
+ handled = [hotkey sendAction:self];
+ }
+ break;
+ }
+ }
+ }
+ return handled;
+}
+
+// Currently we handle hotkey and command events here. If we get one of them
+// we dispatch them off to the handlers above. Otherwise we just call up to
+// super.
+// Arguments:
+// event - the event to check
+// handler - the handler call ref
+// Returns:
+// OSStatus
+- (OSStatus)handleEvent:(GTMCarbonEvent *)event
+ handler:(EventHandlerCallRef)handler {
+ OSStatus theStatus = eventNotHandledErr;
+ if ([event eventClass] == kEventClassKeyboard) {
+ EventKind kind = [event eventKind];
+ if (kind == kEventHotKeyPressed || kind == kEventHotKeyReleased) {
+ theStatus = [self handleHotKeyEvent:event] ? noErr : eventNotHandledErr;
+ }
+ }
+ // We didn't handle it, maybe somebody upstairs will.
+ if (theStatus == eventNotHandledErr) {
+ theStatus = [super handleEvent:event handler:handler];
+ }
+ return theStatus;
+}
+
+@end
+
+@implementation GTMCarbonHotKey
+
+// Init a HotKey record. In debug version make sure that the selector we are
+// passed matches what we expect. (
+// Arguments:
+// keyID - id of the hotkey
+// target - object we are going to call when the hotkey is hit
+// action - selector we are going to call on target
+// whenPressed - do we do it on key down or key up?
+// Returns:
+// a hotkey record, or nil on failure
+- (id)initWithHotKey:(EventHotKeyID)keyID
+ target:(id)target
+ action:(SEL)selector
+ whenPressed:(BOOL)onKeyDown {
+ if ((self = [super init])) {
+ if(!target || !selector) {
+ [self release];
+ return nil;
+ }
+ id_ = keyID;
+ target_ = [target retain];
+ selector_ = selector;
+ onKeyDown_ = onKeyDown;
+ GTMAssertSelectorNilOrImplementedWithReturnTypeAndArguments(target,
+ selector,
+ @encode(void),
+ @encode(id),
+ NULL);
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [target_ release];
+ [super dealloc];
+}
+
+// Does this record match key |keyID|
+// Arguments:
+// keyID - the id to match against
+// Returns:
+// Yes if we match this key id
+- (BOOL)matchesHotKeyID:(EventHotKeyID)keyID {
+ return (id_.signature == keyID.signature) && (id_.id == keyID.id);
+}
+
+- (BOOL)sendAction:(id)sender {
+ BOOL handled = NO;
+ @try {
+ [target_ performSelector:selector_ withObject:sender];
+ handled = YES;
+ }
+ @catch (NSException * e) {
+ handled = NO;
+ _GTMDevLog(@"Exception fired in hotkey: %@ (%@)", [e name], [e reason]);
+ } // COV_NF_LINE
+ return handled;
+}
+
+- (BOOL)onKeyDown {
+ return onKeyDown_;
+}
+
+@end
+
+
+
diff --git a/AppKit/GTMCarbonEventTest.m b/AppKit/GTMCarbonEventTest.m
new file mode 100644
index 0000000..0615271
--- /dev/null
+++ b/AppKit/GTMCarbonEventTest.m
@@ -0,0 +1,360 @@
+//
+// GTMCarbonEventTest.m
+//
+// Copyright 2006-2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy
+// of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+//
+
+#import "GTMSenTestCase.h"
+#import "GTMCarbonEvent.h"
+#import "GTMUnitTestingUtilities.h"
+#import "GTMUnitTestDevLog.h"
+
+@interface GTMCarbonEventTest : GTMTestCase {
+ @private
+ GTMCarbonEvent *event_;
+}
+@end
+
+@interface GTMCarbonEventHandlerTest : GTMTestCase {
+ @private
+ GTMCarbonEventHandler *handler_;
+}
+@end
+
+@interface GTMCarbonEventMonitorHandlerTest : GTMTestCase
+@end
+
+@interface GTMCarbonEventDispatcherHandlerTest : GTMTestCase {
+ @private
+ BOOL hotKeyHit_;
+}
+@end
+
+static const UInt32 kTestClass = 'foo ';
+static const UInt32 kTestKind = 'bar ';
+static const UInt32 kTestParameterName = 'baz ';
+static const UInt32 kTestBadParameterName = 'bom ';
+static const UInt32 kTestParameterValue = 'bam ';
+
+@implementation GTMCarbonEventTest
+- (void)setUp {
+ event_ = [[GTMCarbonEvent eventWithClass:kTestClass kind:kTestKind] retain];
+}
+
+- (void)tearDown {
+ [event_ release];
+}
+
+- (void)testCopy {
+ GTMCarbonEvent *event2 = [[event_ copy] autorelease];
+ STAssertNotNil(event2, nil);
+}
+
+- (void)testEventWithClassAndKind {
+ STAssertEquals([event_ eventClass], kTestClass, nil);
+ STAssertEquals([event_ eventKind], kTestKind, nil);
+}
+
+- (void)testEventWithEvent {
+ GTMCarbonEvent *event2 = [GTMCarbonEvent eventWithEvent:[event_ event]];
+ STAssertEquals([event2 event], [event_ event], nil);
+}
+
+- (void)testCurrentEvent {
+ EventRef eventRef = GetCurrentEvent();
+ GTMCarbonEvent *event = [GTMCarbonEvent currentEvent];
+ STAssertEquals([event event], eventRef, nil);
+}
+
+- (void)testEventClass {
+ [self testEventWithClassAndKind];
+}
+
+- (void)testEventKind {
+ [self testEventWithClassAndKind];
+}
+
+- (void)testSetTime {
+ EventTime eventTime = [event_ time];
+ STAssertNotEquals(eventTime, kEventDurationNoWait, nil);
+ STAssertNotEquals(eventTime, kEventDurationForever, nil);
+ [event_ setTime:kEventDurationForever];
+ EventTime testTime = [event_ time];
+ STAssertEquals(testTime, kEventDurationForever, nil);
+ [event_ setTime:eventTime];
+ STAssertEquals([event_ time], eventTime, nil);
+}
+
+- (void)testTime {
+ [self testSetTime];
+}
+
+- (void)testEvent {
+ [self testEventWithEvent];
+}
+
+- (void)testSetParameterNamed {
+ UInt32 theData = kTestParameterValue;
+ [event_ setUInt32ParameterNamed:kTestParameterName data:&theData];
+ theData = 0;
+ STAssertEquals([event_ sizeOfParameterNamed:kTestParameterName
+ type:typeUInt32],
+ sizeof(UInt32), nil);
+ STAssertTrue([event_ getUInt32ParameterNamed:kTestParameterName
+ data:&theData], nil);
+ STAssertEquals(theData, kTestParameterValue, nil);
+}
+
+- (void)testGetParameterNamed {
+ [self testSetParameterNamed];
+ UInt32 theData = kTestParameterValue;
+ STAssertFalse([event_ getUInt32ParameterNamed:kTestBadParameterName
+ data:&theData], nil);
+ STAssertFalse([event_ getUInt32ParameterNamed:kTestBadParameterName
+ data:NULL], nil);
+
+}
+
+- (void)testSizeOfParameterNamed {
+ [self testSetParameterNamed];
+}
+
+- (void)testHasParameterNamed {
+ [self testSetParameterNamed];
+}
+
+- (OSStatus)gtm_eventHandler:(GTMCarbonEventHandler *)sender
+ receivedEvent:(GTMCarbonEvent *)event
+ handler:(EventHandlerCallRef)handler {
+ OSStatus status = eventNotHandledErr;
+ if ([event eventClass] == kTestClass && [event eventKind] == kTestKind) {
+ status = noErr;
+ }
+ return status;
+}
+
+- (void)testSendToTarget {
+ EventTypeSpec types = { kTestClass, kTestKind };
+ GTMCarbonEventDispatcherHandler *handler
+ = [[GTMCarbonEventDispatcherHandler sharedEventDispatcherHandler]
+ autorelease];
+ [handler registerForEvents:&types count:1];
+ OSStatus status = [event_ sendToTarget:handler options:0];
+ STAssertErr(status, eventNotHandledErr, @"status: %ld", status);
+ [handler setDelegate:self];
+ status = [event_ sendToTarget:handler options:0];
+ STAssertNoErr(status, @"status: %ld", status);
+ [handler unregisterForEvents:&types count:1];
+}
+
+- (void)testPostToQueue {
+ EventQueueRef eventQueue = GetMainEventQueue();
+ [event_ postToMainQueue];
+ OSStatus status = [event_ postToQueue:eventQueue
+ priority:kEventPriorityStandard];
+ STAssertErr(status, eventAlreadyPostedErr, @"status: %ld", status);
+ EventTypeSpec types = { kTestClass, kTestKind };
+ status = FlushEventsMatchingListFromQueue(eventQueue, 1, &types);
+ STAssertNoErr(status, @"status: %ld", status);
+
+ eventQueue = GetCurrentEventQueue();
+ [event_ postToCurrentQueue];
+ status = [event_ postToQueue:eventQueue priority:kEventPriorityStandard];
+ STAssertErr(status, eventAlreadyPostedErr, @"status: %ld", status);
+ status = FlushEventsMatchingListFromQueue(eventQueue, 1, &types);
+ STAssertNoErr(status, @"status: %ld", status);
+}
+
+- (void)testPostToMainQueue {
+ [self testPostToQueue];
+}
+
+- (void)testPostToCurrentQueue {
+ STAssertEquals(GetCurrentEventQueue(), GetMainEventQueue(), nil);
+ [self testPostToMainQueue];
+}
+
+- (void)testDescription {
+ NSString *descString
+ = [NSString stringWithFormat:@"GTMCarbonEvent 'foo ' %d", kTestKind];
+ STAssertEqualObjects([event_ description], descString, nil);
+}
+@end
+
+@implementation GTMCarbonEventHandlerTest
+
+- (void)setUp {
+ handler_ = [[GTMCarbonEventHandler alloc] init];
+}
+
+- (void)tearDown {
+ [handler_ release];
+}
+
+- (void)testEventTarget {
+ STAssertNULL([handler_ eventTarget], nil);
+}
+
+- (void)testEventHandler {
+ [GTMUnitTestDevLog expectPattern:
+ @"DebugAssert: GoogleToolboxForMac: event CantUseParams .*"];
+ STAssertErr([handler_ handleEvent:nil handler:nil], eventNotHandledErr, nil);
+}
+
+- (void)testDelegate {
+ [handler_ setDelegate:self];
+ STAssertEqualObjects([handler_ delegate], self, nil);
+ [handler_ setDelegate:nil];
+ STAssertNil([handler_ delegate], nil);
+}
+
+
+- (void)testSetDelegate {
+ [self testDelegate];
+}
+
+@end
+
+@implementation GTMCarbonEventMonitorHandlerTest
+
+- (void)testEventHandler {
+ GTMCarbonEventMonitorHandler *monitor
+ = [GTMCarbonEventMonitorHandler sharedEventMonitorHandler];
+ STAssertEquals([monitor eventTarget], GetEventMonitorTarget(), nil);
+}
+
+@end
+
+@implementation GTMCarbonEventDispatcherHandlerTest
+
+- (void)testEventHandler {
+ GTMCarbonEventDispatcherHandler *dispatcher
+ = [GTMCarbonEventDispatcherHandler sharedEventDispatcherHandler];
+ STAssertEquals([dispatcher eventTarget], GetEventDispatcherTarget(), nil);
+}
+
+- (void)hitHotKey:(id)sender {
+ hotKeyHit_ = YES;
+ [NSApp stop:self];
+}
+
+- (void)hitExceptionalHotKey:(id)sender {
+ [NSException raise:@"foo" format:@"bar"];
+}
+
+- (void)testRegisterHotKeyModifiersTargetActionWhenPressed {
+
+ // This test can't be run if the screen saver is active because the security
+ // agent blocks us from sending events via remote operations
+ if (![GTMUnitTestingUtilities isScreenSaverActive]) {
+ GTMCarbonEventDispatcherHandler *dispatcher
+ = [GTMCarbonEventDispatcherHandler sharedEventDispatcherHandler];
+ STAssertNotNil(dispatcher, @"Unable to acquire singleton");
+ UInt32 keyMods = (NSShiftKeyMask | NSControlKeyMask
+ | NSAlternateKeyMask | NSCommandKeyMask);
+ EventHotKeyRef hotKey;
+ [GTMUnitTestDevLog expectPattern:@"DebugAssert: GoogleToolboxForMac: "
+ @"newKey CantCreateKey .*"];
+ STAssertNULL([dispatcher registerHotKey:0x5
+ modifiers:keyMods
+ target:nil
+ action:nil
+ whenPressed:YES],
+ @"Shouldn't have created hotkey");
+ STAssertThrowsSpecificNamed([dispatcher registerHotKey:0x5
+ modifiers:keyMods
+ target:self
+ action:@selector(badSelector:)
+ whenPressed:YES],
+ NSException, NSInternalInconsistencyException,
+ hotKey, @"Shouldn't have created hotkey");
+ hotKey = [dispatcher registerHotKey:0x5
+ modifiers:keyMods
+ target:self
+ action:@selector(hitHotKey:)
+ whenPressed:YES];
+ STAssertNotNULL(hotKey, @"Unable to create hotkey");
+
+ hotKeyHit_ = NO;
+
+ // Post the hotkey combo to the event queue. If everything is working
+ // correctly hitHotKey: should get called, and hotKeyHit_ will be set for
+ // us. We run the event loop for a set amount of time waiting for this to
+ // happen.
+ [GTMUnitTestingUtilities postTypeCharacterEvent:'g' modifiers:keyMods];
+ NSDate* future = [NSDate dateWithTimeIntervalSinceNow:1.0f];
+ [GTMUnitTestingUtilities runUntilDate:future];
+ STAssertTrue(hotKeyHit_, @"Hot key never got fired.");
+ [dispatcher unregisterHotKey:hotKey];
+ }
+}
+
+- (void)testRegisterHotKeyModifiersTargetActionWhenPressedException {
+
+ // This test can't be run if the screen saver is active because the security
+ // agent blocks us from sending events via remote operations
+ if (![GTMUnitTestingUtilities isScreenSaverActive]) {
+ GTMCarbonEventDispatcherHandler *dispatcher
+ = [GTMCarbonEventDispatcherHandler sharedEventDispatcherHandler];
+ STAssertNotNil(dispatcher, @"Unable to acquire singleton");
+ UInt32 keyMods = (NSShiftKeyMask | NSControlKeyMask
+ | NSAlternateKeyMask | NSCommandKeyMask);
+ EventHotKeyRef hotKey = [dispatcher registerHotKey:0x5
+ modifiers:keyMods
+ target:self
+ action:@selector(hitExceptionalHotKey:)
+ whenPressed:YES];
+ STAssertTrue(hotKey != nil, @"Unable to create hotkey");
+
+ // Post the hotkey combo to the event queue. If everything is working correctly
+ // hitHotKey: should get called, and hotKeyHit_ will be set for us.
+ // We run the event loop for a set amount of time waiting for this to happen.
+ [GTMUnitTestingUtilities postTypeCharacterEvent:'g' modifiers:keyMods];
+ NSDate* future = [NSDate dateWithTimeIntervalSinceNow:1.0f];
+ [GTMUnitTestDevLog expectString:@"Exception fired in hotkey: foo (bar)"];
+ [GTMUnitTestingUtilities runUntilDate:future];
+ [dispatcher unregisterHotKey:hotKey];
+ }
+}
+
+- (void)testKeyModifiers {
+ struct {
+ NSUInteger cocoaKey_;
+ UInt32 carbonKey_;
+ } keyMap[] = {
+ { NSAlphaShiftKeyMask, alphaLock},
+ { NSShiftKeyMask, shiftKey},
+ { NSControlKeyMask, controlKey},
+ { NSAlternateKeyMask, optionKey},
+ { NSCommandKeyMask, cmdKey},
+ };
+ size_t combos = pow(2, sizeof(keyMap) / sizeof(keyMap[0]));
+ for (size_t i = 0; i < combos; i++) {
+ NSUInteger cocoaMods = 0;
+ UInt32 carbonMods = 0;
+ for (size_t j = 0; j < 32 && j < sizeof(keyMap) / sizeof(keyMap[0]); j++) {
+ if (i & 1 << j) {
+ cocoaMods |= keyMap[j].cocoaKey_;
+ carbonMods |= keyMap[j].carbonKey_;
+ }
+ }
+ STAssertEquals(GTMCocoaToCarbonKeyModifiers(cocoaMods), carbonMods, nil);
+ STAssertEquals(GTMCarbonToCocoaKeyModifiers(carbonMods), cocoaMods, nil);
+ }
+}
+
+
+@end
+
diff --git a/AppKit/GTMGetURLHandler.m b/AppKit/GTMGetURLHandler.m
index a35dd95..66c02eb 100644
--- a/AppKit/GTMGetURLHandler.m
+++ b/AppKit/GTMGetURLHandler.m
@@ -93,7 +93,8 @@ withReplyEvent:(NSAppleEventDescriptor *)replyEvent;
withDescription:(NSString*)string
toDescriptor:(NSAppleEventDescriptor *)desc;
+ (id)handlerForBundle:(NSBundle *)bundle;
-+ (void)appFinishedLaunchingHandler:(NSNotification*)notification;
++ (void)getUrl:(NSAppleEventDescriptor *)event
+withReplyEvent:(NSAppleEventDescriptor *)replyEvent;
@end
@implementation GTMGetURLHandler
@@ -102,32 +103,28 @@ GTM_METHOD_CHECK(NSString, gtm_appleEventDescriptor);
+ (void)load {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
- NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
- [nc addObserver:self
- selector:@selector(appFinishedLaunchingHandler:)
- name:NSApplicationDidFinishLaunchingNotification
- object:nil];
+ NSAppleEventManager *man = [NSAppleEventManager sharedAppleEventManager];
+ [man setEventHandler:self
+ andSelector:@selector(getUrl:withReplyEvent:)
+ forEventClass:kInternetEventClass
+ andEventID:kAEGetURL];
[pool release];
}
-+ (void)appFinishedLaunchingHandler:(NSNotification*)notification {
- NSBundle *bundle = [NSBundle mainBundle];
- GTMGetURLHandler *handler = [GTMGetURLHandler handlerForBundle:bundle];
- if (handler) {
- [handler retain];
- GTMNSMakeUncollectable(handler);
- NSAppleEventManager *man = [NSAppleEventManager sharedAppleEventManager];
- [man setEventHandler:handler
- andSelector:@selector(getUrl:withReplyEvent:)
- forEventClass:kInternetEventClass
- andEventID:kAEGetURL];
- }
- NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
- [nc removeObserver:self
- name:NSApplicationDidFinishLaunchingNotification
- object:nil];
++ (void)getUrl:(NSAppleEventDescriptor *)event
+withReplyEvent:(NSAppleEventDescriptor *)replyEvent {
+ static GTMGetURLHandler *sHandler = nil;
+ if (!sHandler) {
+ NSBundle *bundle = [NSBundle mainBundle];
+ sHandler = [GTMGetURLHandler handlerForBundle:bundle];
+ if (sHandler) {
+ [sHandler retain];
+ GTMNSMakeUncollectable(sHandler);
+ }
+ }
+ [sHandler getUrl:event withReplyEvent:replyEvent];
}
-
+
+ (id)handlerForBundle:(NSBundle *)bundle {
GTMGetURLHandler *handler = nil;
NSArray *urlTypes
@@ -135,8 +132,11 @@ GTM_METHOD_CHECK(NSString, gtm_appleEventDescriptor);
if (urlTypes) {
handler = [[[GTMGetURLHandler alloc] initWithTypes:urlTypes] autorelease];
} else {
+ // COV_NF_START
+ // Hard to test it if we don't have it.
_GTMDevLog(@"If you don't have CFBundleURLTypes in your plist, you may want"
@" to remove GTMGetURLHandler.m from your project");
+ // COV_NF_END
}
return handler;
}
@@ -146,7 +146,9 @@ GTM_METHOD_CHECK(NSString, gtm_appleEventDescriptor);
urlTypes_ = [urlTypes retain];
#if GTM_CHECK_BUNDLE_URL_CLASSES
// Some debug handling to check to make sure we can handle the
- // classes properly.
+ // classes properly. We check here instead of at init in case some of the
+ // handlers are being handled by plugins or other imported code that are
+ // loaded after we have been initialized.
NSEnumerator *enumerator = [urlTypes_ objectEnumerator];
NSDictionary *urlType;
while ((urlType = [enumerator nextObject])) {
@@ -156,14 +158,14 @@ GTM_METHOD_CHECK(NSString, gtm_appleEventDescriptor);
if (cls) {
if (![cls respondsToSelector:@selector(gtm_openURL:)]) {
_GTMDevLog(@"Class %@ for URL handler %@ "
- "(URL schemes: %@) doesn't respond to openURL:",
+ @"(URL schemes: %@) doesn't respond to openURL:",
className,
[urlType objectForKey:kGTMCFBundleURLNameKey],
[urlType objectForKey:kGTMCFBundleURLSchemesKey]);
}
} else {
_GTMDevLog(@"Unable to get class %@ for URL handler %@ "
- "(URL schemes: %@)",
+ @"(URL schemes: %@)",
className,
[urlType objectForKey:kGTMCFBundleURLNameKey],
[urlType objectForKey:kGTMCFBundleURLSchemesKey]);
@@ -173,7 +175,7 @@ GTM_METHOD_CHECK(NSString, gtm_appleEventDescriptor);
if ([role caseInsensitiveCompare:kGTMCFBundleViewerRole] == NSOrderedSame ||
[role caseInsensitiveCompare:kGTMCFBundleEditorRole] == NSOrderedSame) {
_GTMDevLog(@"Missing %@ for URL handler %@ "
- "(URL schemes: %@)",
+ @"(URL schemes: %@)",
kGTMBundleURLClassKey,
[urlType objectForKey:kGTMCFBundleURLNameKey],
[urlType objectForKey:kGTMCFBundleURLSchemesKey]);
@@ -185,10 +187,14 @@ GTM_METHOD_CHECK(NSString, gtm_appleEventDescriptor);
return self;
}
+// COV_NF_START
+// Singleton is never dealloc'd
- (void)dealloc {
[urlTypes_ release];
[super dealloc];
}
+// COV_NF_END
+
- (NSURL*)extractURLFromEvent:(NSAppleEventDescriptor*)event
withReplyEvent:(NSAppleEventDescriptor *)replyEvent {
@@ -197,9 +203,12 @@ GTM_METHOD_CHECK(NSString, gtm_appleEventDescriptor);
NSString *urlstring = [desc stringValue];
NSURL *url = [NSURL URLWithString:urlstring];
if (!url) {
+ // COV_NF_START
+ // Can't convince the OS to give me a bad URL
[self addError:errAECoercionFail
withDescription:@"Unable to extract url from key direct object."
toDescriptor:replyEvent];
+ // COV_NF_END
}
return url;
}
@@ -227,25 +236,40 @@ GTM_METHOD_CHECK(NSString, gtm_appleEventDescriptor);
if (!cls) {
NSString *errorString
= [NSString stringWithFormat:@"Unable to instantiate class for "
- "%@:%@ for scheme:%@.",
+ @"%@:%@ for scheme:%@.",
kGTMBundleURLClassKey, class, typeScheme];
[self addError:errAECorruptData
withDescription:errorString
toDescriptor:replyEvent];
+ } else {
+ if (![cls respondsToSelector:@selector(gtm_openURL:)]) {
+ NSString *errorString
+ = [NSString stringWithFormat:@"Class %@:%@ for scheme:%@ does not"
+ @"respond to gtm_openURL:",
+ kGTMBundleURLClassKey, class, typeScheme];
+ [self addError:errAECorruptData
+ withDescription:errorString
+ toDescriptor:replyEvent];
+ cls = Nil;
+ }
}
} else {
+ // COV_NF_START
+ // Don't know how to force an URL that we don't respond to upon ourselves.
NSString *errorString
= [NSString stringWithFormat:@"Unable to find handler for scheme %@.",
scheme];
[self addError:errAECorruptData
withDescription:errorString
toDescriptor:replyEvent];
+ // COV_NF_END
+
}
return cls;
}
- (void)getUrl:(NSAppleEventDescriptor *)event
-withReplyEvent:(NSAppleEventDescriptor *)replyEvent {
+withReplyEvent:(NSAppleEventDescriptor *)replyEvent {
NSURL *url = [self extractURLFromEvent:event withReplyEvent:replyEvent];
if (!url) {
return;
diff --git a/AppKit/GTMGetURLHandlerTest.m b/AppKit/GTMGetURLHandlerTest.m
new file mode 100644
index 0000000..98d3c89
--- /dev/null
+++ b/AppKit/GTMGetURLHandlerTest.m
@@ -0,0 +1,85 @@
+// GTMGetURLHandlerTest.m
+//
+// Copyright 2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy
+// of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+//
+
+#import "GTMSenTestCase.h"
+#import "GTMUnitTestingUtilities.h"
+#import "GTMUnitTestDevLog.h"
+
+static BOOL sURLHandlerWasHit;
+
+@interface GTMGetURLHandlerBadClassWarning : NSObject
+@end
+
+@implementation GTMGetURLHandlerBadClassWarning : NSObject
+@end
+
+@interface GTMGetURLHandlerTest : GTMTestCase
+@end
+
+@implementation GTMGetURLHandlerTest
+- (BOOL)openURLString:(NSString *)url {
+ ProcessSerialNumber psn = { 0, kCurrentProcess };
+ NSAppleEventDescriptor *currentProcess
+ = [NSAppleEventDescriptor descriptorWithDescriptorType:typeProcessSerialNumber
+ bytes:&psn
+ length:sizeof(ProcessSerialNumber)];
+ NSAppleEventDescriptor *event
+ = [NSAppleEventDescriptor appleEventWithEventClass:kInternetEventClass
+ eventID:kAEGetURL
+ targetDescriptor:currentProcess
+ returnID:kAutoGenerateReturnID
+ transactionID:kAnyTransactionID];
+ NSAppleEventDescriptor *keyDesc
+ = [NSAppleEventDescriptor descriptorWithString:url];
+ [event setParamDescriptor:keyDesc forKeyword:keyDirectObject];
+ AppleEvent replyEvent = { typeNull, NULL };
+ OSStatus err = AESendMessage([event aeDesc], &replyEvent, kAEWaitReply, 60);
+ return err == noErr ? YES : NO;
+}
+
++ (BOOL)gtm_openURL:(NSURL*)url {
+ sURLHandlerWasHit = !sURLHandlerWasHit;
+ return sURLHandlerWasHit;
+}
+
+- (void)testURLCall {
+ sURLHandlerWasHit = NO;
+
+ [GTMUnitTestDevLog expectPattern:@"Class GTMGetURLHandlerBadClassWarning "
+ @"for URL handler GTMGetURLHandlerBadClassURL .*"];
+ [GTMUnitTestDevLog expectPattern:@"Unable to get class "
+ @"GTMGetURLHandlerMissingClassWarning for URL handler "
+ @"GTMGetURLHandlerMissingClassURL .*"];
+ [GTMUnitTestDevLog expectPattern:@"Missing GTMBundleURLClass for URL handler "
+ @"GTMGetURLHandlerMissingHandlerURL .*"];
+ STAssertTrue([self openURLString:@"gtmgeturlhandlertest://test.foo"], nil);
+ STAssertTrue(sURLHandlerWasHit, @"URL handler not called");
+
+ STAssertTrue([self openURLString:@"gtmgeturlhandlertest://test.foo"], nil);
+ STAssertFalse(sURLHandlerWasHit, @"URL handler not called 2");
+
+ // test the two URL schemes with bad entries
+ STAssertTrue([self openURLString:@"gtmgeturlhandlerbadclasstest://test.foo"],
+ nil);
+
+ STAssertTrue([self openURLString:@"gtmgeturlhandlermissingclasstest://test.foo"],
+ nil);
+
+ STAssertTrue([self openURLString:@"gtmgeturlhandlermissinghandlerurl://test.foo"],
+ nil);
+}
+@end
diff --git a/AppKit/GTMHotKeyTextField.h b/AppKit/GTMHotKeyTextField.h
new file mode 100644
index 0000000..4177f49
--- /dev/null
+++ b/AppKit/GTMHotKeyTextField.h
@@ -0,0 +1,127 @@
+//
+// GTMHotKeyTextField.h
+//
+// Copyright 2006-2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy
+// of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+//
+
+// Text field for capturing hot key entry. This is intended to be similar to the
+// Apple key editor in their Keyboard pref pane.
+
+// NOTE: There are strings that need to be localized to use this field. See the
+// code in stringForKeycode the the keys. The keys are all the English versions
+// so you'll get reasonable things if you don't have a strings file.
+
+#import <Cocoa/Cocoa.h>
+#import "GTMDefines.h"
+
+// Dictionary key for hot key configuration information modifier flags.
+// NSNumber of a unsigned int. Modifier flags are stored using Cocoa constants
+// (same as NSEvent) you will need to translate them to Carbon modifier flags
+// for use with RegisterEventHotKey()
+#define kGTMHotKeyModifierFlagsKey @"Modifiers"
+
+// Dictionary key for hot key configuration of virtual key code. NSNumber of
+// unsigned int. For double-modifier hotkeys (see below) this value is ignored.
+#define kGTMHotKeyKeyCodeKey @"KeyCode"
+
+// Dictionary key for hot key configuration of double-modifier tap. NSNumber
+// BOOL value. Double-tap modifier keys cannot be used with
+// RegisterEventHotKey(), you must implement your own Carbon event handler.
+#define kGTMHotKeyDoubledModifierKey @"DoubleModifier"
+
+// Custom text field class used for hot key entry. In order to use this class
+// you will need to configure your window's delegate, to return the related
+// field editor.
+//
+// Sample window delegate method:
+//
+// -(id)windowWillReturnFieldEditor:(NSWindow *)sender toObject:(id)anObject {
+//
+// if ([anObject isKindOfClass:[GTMHotKeyTextField class]]) {
+// return [GTMHotKeyFieldEditor sharedHotKeyFieldEditor];
+// } else {
+// return nil; // Window will use the AppKit shared editor
+// }
+//
+// }
+//
+//
+// Other notes:
+// - Though you are free to implement control:textShouldEndEditing: in your
+// delegate its return is always ignored. The field always accepts only
+// one hotkey keystroke before editing ends.
+// - The "value" binding of this control is to the dictionary describing the
+// hotkey. At this time binding options are not supported.
+// - The field does not attempt to consume all hotkeys. Hotkeys which are
+// already bound in Apple prefs or other applications will have their
+// normal effect.
+//
+
+@interface GTMHotKeyTextField : NSTextField {
+ @private
+ NSDictionary *hotKeyDict_;
+ // Bindings
+ NSObject *boundObject_;
+ NSString *boundKeyPath_;
+}
+
+// Set/Get the hot key dictionary for the field. See above for key names.
+- (void)setHotKeyValue:(NSDictionary *)hotKey;
+- (NSDictionary *)hotKeyValue;
+
+// Convert Cocoa modifier flags (-[NSEvent modifierFlags]) into a string for
+// display. Modifiers are represented in the string in the same order they would
+// appear in the Menu Manager.
+//
+// Args:
+// flags: -[NSEvent modifierFlags]
+//
+// Returns:
+// Autoreleased NSString
+//
++ (NSString *)stringForModifierFlags:(unsigned int)flags;
+
+// Convert a keycode into a string that would result from typing the keycode in
+// the current keyboard layout. This may be one or more characters.
+//
+// Args:
+// keycode: Virtual keycode such as one obtained from NSEvent
+// useGlyph: In many cases the glyphs are confusing, and a string is clearer.
+// However, if you want to display in a menu item, use must
+// have a glyph. Set useGlyph to FALSE to get localized strings
+// which are better for UI display in places other than menus.
+// bundle: Localization bundle to use for localizable key names
+//
+// Returns:
+// Autoreleased NSString
+//
++ (NSString *)stringForKeycode:(UInt16)keycode
+ useGlyph:(BOOL)useGlyph
+ resourceBundle:(NSBundle *)bundle;
+
+@end
+
+// Custom field editor for use with hotkey entry fields (GTMHotKeyTextField).
+// See the GTMHotKeyTextField for instructions on using from the window
+// delegate.
+@interface GTMHotKeyFieldEditor : NSTextView {
+ @private
+ NSDictionary *hotKeyDict_; // strong
+}
+
+// Get the shared field editor for all hot key fields
++ (GTMHotKeyFieldEditor *)sharedHotKeyFieldEditor;
+
+@end
diff --git a/AppKit/GTMHotKeyTextField.m b/AppKit/GTMHotKeyTextField.m
new file mode 100644
index 0000000..3a604be
--- /dev/null
+++ b/AppKit/GTMHotKeyTextField.m
@@ -0,0 +1,1009 @@
+// GTMHotKeyTextField.m
+//
+// Copyright 2006-2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy
+// of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+//
+
+#import "GTMHotKeyTextField.h"
+
+#import <Carbon/Carbon.h>
+#import "GTMSystemVersion.h"
+#import "GTMObjectSingleton.h"
+
+#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
+typedef struct __TISInputSource* TISInputSourceRef;
+static TISInputSourceRef(*GTM_TISCopyCurrentKeyboardLayoutInputSource)(void) = NULL;
+static void * (*GTM_TISGetInputSourceProperty)(TISInputSourceRef inputSource,
+ CFStringRef propertyKey) = NULL;
+static CFStringRef kGTM_TISPropertyUnicodeKeyLayoutData = NULL;
+#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
+
+
+@interface GTMHotKeyTextField (PrivateMethods)
+- (void)setupBinding:(id)bound withPath:(NSString *)path;
+- (void)updateDisplayedPrettyString;
++ (BOOL)isValidHotKey:(NSDictionary *)hotKey;
++ (NSString *)displayStringForHotKey:(NSDictionary *)hotKey;
+@end
+
+@interface GTMHotKeyFieldEditor (PrivateMethods)
+- (NSDictionary *)hotKeyDictionary;
+- (void)setHotKeyDictionary:(NSDictionary *)hotKey;
+- (BOOL)shouldBypassEvent:(NSEvent *)theEvent;
+- (void)processEventToHotKeyAndString:(NSEvent *)theEvent;
+- (void)windowResigned:(NSNotification *)notification;
+- (NSDictionary *)hotKeyDictionaryForEvent:(NSEvent *)event;
+@end
+
+@implementation GTMHotKeyTextField
+
+- (void)finalize {
+ if (boundObject_ && boundKeyPath_) {
+ [boundObject_ removeObserver:self forKeyPath:boundKeyPath_];
+ }
+ [super finalize];
+}
+
+- (void)dealloc {
+
+ if (boundObject_ && boundKeyPath_) {
+ [boundObject_ removeObserver:self forKeyPath:boundKeyPath_];
+ }
+ [boundObject_ release];
+ [boundKeyPath_ release];
+ [hotKeyDict_ release];
+ [super dealloc];
+
+}
+
+#pragma mark Bindings
+
+
+- (void)bind:(NSString *)binding toObject:(id)observableController
+ withKeyPath:(NSString *)keyPath
+ options:(NSDictionary *)options {
+
+ if ([binding isEqualToString:NSValueBinding]) {
+ // Update to our new binding
+ [self setupBinding:observableController withPath:keyPath];
+ // TODO: Should deal with the bind options
+ }
+ [super bind:binding
+ toObject:observableController
+ withKeyPath:keyPath
+ options:options];
+
+}
+
+- (void)unbind:(NSString *)binding {
+
+ // Clean up value on unbind
+ if ([binding isEqualToString:NSValueBinding]) {
+ if (boundObject_ && boundKeyPath_) {
+ [boundObject_ removeObserver:self forKeyPath:boundKeyPath_];
+ }
+ [boundObject_ release];
+ boundObject_ = nil;
+ [boundKeyPath_ release];
+ boundKeyPath_ = nil;
+ }
+ [super unbind:binding];
+
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
+ change:(NSDictionary *)change context:(void *)context {
+
+ if ((object == boundObject_) && [boundKeyPath_ isEqualToString:keyPath]) {
+ // Our binding has changed, update
+ id changedValue = [change objectForKey:NSKeyValueChangeNewKey];
+ // NSUserDefaultsController does not appear to pass on the new object and,
+ // perhaps other controllers may not, so if we get a nil or NSNull back
+ // here let's directly retrieve the hotKeyDict_ from the object.
+ if (!changedValue || changedValue == [NSNull null]) {
+ changedValue = [object valueForKeyPath:keyPath];
+ }
+ [hotKeyDict_ autorelease];
+ hotKeyDict_ = [changedValue copy];
+ [self updateDisplayedPrettyString];
+ }
+
+}
+
+// Private convenience method for attaching to a new binding
+- (void)setupBinding:(id)bound withPath:(NSString *)path {
+
+ // Release previous
+ if (boundObject_ && boundKeyPath_) {
+ [boundObject_ removeObserver:self forKeyPath:boundKeyPath_];
+ }
+ [boundObject_ release];
+ [boundKeyPath_ release];
+ // Set new
+ boundObject_ = [bound retain];
+ boundKeyPath_ = [path copy];
+ // Make ourself an observer
+ [boundObject_ addObserver:self
+ forKeyPath:boundKeyPath_
+ options:NSKeyValueObservingOptionNew
+ context:nil];
+ // Pull in any current value
+ [hotKeyDict_ autorelease];
+ hotKeyDict_ = [[boundObject_ valueForKeyPath:boundKeyPath_] copy];
+ // Update the display string
+ [self updateDisplayedPrettyString];
+
+}
+
+#pragma mark Defeating NSControl
+
+- (double)doubleValue {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't take numbers.");
+ return 0.0;
+
+}
+
+- (void)setDoubleValue:(double)value {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't take numbers.");
+ return;
+
+}
+
+- (float)floatValue {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't take numbers.");
+ return 0.0f;
+
+}
+
+- (void)setFloatValue:(float)value {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't take numbers.");
+ return;
+
+}
+
+- (int)intValue {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't take numbers.");
+ return 0;
+
+}
+
+- (void)setIntValue:(int)value {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't take numbers.");
+ return;
+
+}
+
+#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5
+
+- (int)integerValue {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't take numbers.");
+ return 0;
+
+}
+
+- (void)setIntegerValue:(NSInteger)value {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't take numbers.");
+ return;
+
+}
+
+#endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5
+
+- (id)objectValue {
+
+ return [self hotKeyValue];
+
+}
+
+- (void)setObjectValue:(id)object {
+
+ [self setHotKeyValue:object];
+
+}
+
+- (NSString *)stringValue {
+
+ return [[self class] displayStringForHotKey:hotKeyDict_];
+
+}
+
+- (void)setStringValue:(NSString *)string {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields want dictionaries, not strings.");
+ return;
+
+}
+
+- (NSAttributedString *)attributedStringValue {
+
+ NSString *prettyString = [self stringValue];
+ if (!prettyString) return nil;
+ return [[[NSAttributedString alloc] initWithString:prettyString] autorelease];
+
+}
+
+- (void)setAttributedStringValue:(NSAttributedString *)string {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields want dictionaries, not strings.");
+ return;
+
+}
+
+- (void)takeDoubleValueFrom:(id)sender {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't take numbers.");
+ return;
+
+}
+
+- (void)takeFloatValueFrom:(id)sender {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't take numbers.");
+ return;
+
+}
+
+- (void)takeIntValueFrom:(id)sender {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't take numbers.");
+ return;
+
+}
+
+- (void)takeObjectValueFrom:(id)sender {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO,
+ @"Hot key fields want dictionaries via bindings, not from controls.");
+ return;
+
+}
+
+- (void)takeStringValueFrom:(id)sender {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields want dictionaries, not strings.");
+ return;
+
+}
+
+- (id)formatter {
+
+ return nil;
+
+}
+
+- (void)setFormatter:(NSFormatter *)newFormatter {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't accept formatters.");
+ return;
+
+}
+
+#pragma mark Hot Key Support
+
++ (BOOL)isValidHotKey:(NSDictionary *)hotKeyDict {
+ if (!hotKeyDict ||
+ ![hotKeyDict isKindOfClass:[NSDictionary class]] ||
+ ![hotKeyDict objectForKey:kGTMHotKeyModifierFlagsKey] ||
+ ![hotKeyDict objectForKey:kGTMHotKeyKeyCodeKey] ||
+ ![hotKeyDict objectForKey:kGTMHotKeyDoubledModifierKey]) {
+ return NO;
+ }
+ return YES;
+}
+
+- (void)setHotKeyValue:(NSDictionary *)hotKey {
+
+ // Sanity only if set, nil is OK
+ if (hotKey && ![[self class] isValidHotKey:hotKey]) {
+ return;
+ }
+
+ // If we are bound we want to round trip through that interface
+ if (boundObject_ && boundKeyPath_) {
+ // If the change is accepted this will call us back as an observer
+ [boundObject_ setValue:hotKey forKeyPath:boundKeyPath_];
+ return;
+ }
+
+ // Otherwise we directly update ourself
+ [hotKeyDict_ autorelease];
+ hotKeyDict_ = [hotKey copy];
+ [self updateDisplayedPrettyString];
+
+}
+
+- (NSDictionary *)hotKeyValue {
+
+ return hotKeyDict_;
+
+}
+
+// Private method to update the displayed text of the field with the
+// user-readable representation.
+- (void)updateDisplayedPrettyString {
+
+ // Basic validation
+ if (![[self class] isValidHotKey:hotKeyDict_]) {
+ [super setStringValue:@""];
+ return;
+ }
+
+ // Pretty string
+ NSString *prettyString = [[self class] displayStringForHotKey:hotKeyDict_];
+ if (!prettyString) {
+ prettyString = @"";
+ }
+ [super setStringValue:prettyString];
+
+}
+
++ (NSString *)displayStringForHotKey:(NSDictionary *)hotKeyDict {
+
+ if (!hotKeyDict) return nil;
+
+ NSBundle *bundle = [NSBundle bundleForClass:[self class]];
+
+ // Modifiers
+ unsigned int flags
+ = [[hotKeyDict objectForKey:kGTMHotKeyModifierFlagsKey] unsignedIntValue];
+ NSString *mods = [GTMHotKeyTextField stringForModifierFlags:flags];
+ if (!mods || ![mods length]) return nil;
+ // Handle double modifier case
+ if ([[hotKeyDict objectForKey:kGTMHotKeyDoubledModifierKey] boolValue]) {
+ return [NSString stringWithFormat:@"%@ + %@", mods, mods];
+ }
+ // Keycode
+ unsigned int keycode
+ = [[hotKeyDict objectForKey:kGTMHotKeyKeyCodeKey] unsignedIntValue];
+ NSString *keystroke = [GTMHotKeyTextField stringForKeycode:keycode
+ useGlyph:NO
+ resourceBundle:bundle];
+ if (!keystroke || ![keystroke length]) return nil;
+ return [NSString stringWithFormat:@"%@%@", mods, keystroke];
+
+}
+
+
+#pragma mark Field Editor Callbacks
+
+- (BOOL)textShouldBeginEditing:(GTMHotKeyFieldEditor *)fieldEditor {
+
+ // Sanity
+ if (![fieldEditor isKindOfClass:[GTMHotKeyFieldEditor class]]) {
+ _GTMDevLog(@"Field editor not appropriate for field, check window delegate");
+ return NO;
+ }
+
+ // We don't call super from here, because we are defeating default behavior
+ // as a result we have to call the delegate ourself.
+ id myDelegate = [self delegate];
+ SEL selector = @selector(control:textShouldBeginEditing:);
+ if ([myDelegate respondsToSelector:selector]) {
+ if (![myDelegate control:self textShouldBeginEditing:fieldEditor]) return NO;
+ }
+
+ // Update the field editor internal hotkey representation
+ [fieldEditor setHotKeyDictionary:hotKeyDict_]; // OK if its nil
+ return YES;
+
+}
+
+- (void)textDidChange:(NSNotification *)notification {
+
+ // Sanity
+ GTMHotKeyFieldEditor *fieldEditor = [notification object];
+ if (![fieldEditor isKindOfClass:[GTMHotKeyFieldEditor class]]) {
+ _GTMDevLog(@"Field editor not appropriate for field, check window delegate");
+ return;
+ }
+
+ // When the field changes we want to read in the current hotkey value so
+ // bindings can validate
+ [self setHotKeyValue:[fieldEditor hotKeyDictionary]];
+
+ // Let super handle the notifications
+ [super textDidChange:notification];
+
+}
+
+- (BOOL)textShouldEndEditing:(GTMHotKeyFieldEditor *)fieldEditor {
+
+ // Sanity
+ if (![fieldEditor isKindOfClass:[GTMHotKeyFieldEditor class]]) {
+ _GTMDevLog(@"Field editor not appropriate for field, check window delegate");
+ return NO;
+ }
+
+ // Again we are defeating default behavior so we have to do delegate handling
+ // ourself. In this case our goal is simply to prevent the superclass from
+ // doing its own KVO, but we can also skip [[self cell] isEntryAcceptable:].
+ // We'll also ignore the delegate control:textShouldEndEditing:. The field
+ // editor is done whether they like it or not.
+ id myDelegate = [self delegate];
+ SEL selector = @selector(control:textShouldEndEditing:);
+ if ([myDelegate respondsToSelector:selector]) {
+ [myDelegate control:self textShouldEndEditing:fieldEditor];
+ }
+
+ // The end is always allowed, so set new value
+ [self setHotKeyValue:[fieldEditor hotKeyDictionary]];
+
+ return YES;
+
+}
+
+#pragma mark Class methods building strings for use w/in the UI.
+
+#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
++ (void)initialize {
+ if (!GTM_TISCopyCurrentKeyboardLayoutInputSource
+ && [GTMSystemVersion isLeopardOrGreater]) {
+ CFBundleRef hiToolbox
+ = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.HIToolbox"));
+ if (hiToolbox) {
+ kGTM_TISPropertyUnicodeKeyLayoutData
+ = *(CFStringRef*)CFBundleGetDataPointerForName(hiToolbox,
+ CFSTR("kTISPropertyUnicodeKeyLayoutData"));
+ GTM_TISCopyCurrentKeyboardLayoutInputSource
+ = CFBundleGetFunctionPointerForName(hiToolbox,
+ CFSTR("TISCopyCurrentKeyboardLayoutInputSource"));
+ GTM_TISGetInputSourceProperty
+ = CFBundleGetFunctionPointerForName(hiToolbox,
+ CFSTR("TISGetInputSourceProperty"));
+ }
+ }
+}
+#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
+
+#pragma mark Useful String Class Methods
+
+// These are not in a category on NSString because this class could be used
+// within multiple preference panes at the same time. If we put it in a category
+// it would require setting up some magic so that the categories didn't conflict
+// between the multiple pref panes. By putting it in the class, you can just
+// #define the class name to something else, and then you won't have any
+// conflicts.
+
++ (NSString *)stringForModifierFlags:(unsigned int)flags {
+
+ UniChar modChars[4]; // We only look for 4 flags
+ unsigned int charCount = 0;
+ // These are in the same order as the menu manager shows them
+ if (flags & NSControlKeyMask) modChars[charCount++] = kControlUnicode;
+ if (flags & NSAlternateKeyMask) modChars[charCount++] = kOptionUnicode;
+ if (flags & NSShiftKeyMask) modChars[charCount++] = kShiftUnicode;
+ if (flags & NSCommandKeyMask) modChars[charCount++] = kCommandUnicode;
+ if (charCount == 0) return nil;
+ return [NSString stringWithCharacters:modChars length:charCount];
+
+}
+
++ (NSString *)stringForKeycode:(UInt16)keycode
+ useGlyph:(BOOL)useGlyph
+ resourceBundle:(NSBundle *)bundle {
+
+ // Some keys never move in any layout (to the best of our knowledge at least)
+ // so we can hard map them.
+ UniChar key = 0;
+ NSString *localizedKey = nil;
+
+ switch (keycode) {
+
+ // Of the hard mapped keys some can be represented with pretty and obvioous
+ // Unicode or simple strings without localization.
+
+ // Arrow keys
+ case 123: key = NSLeftArrowFunctionKey; break;
+ case 124: key = NSRightArrowFunctionKey; break;
+ case 125: key = NSDownArrowFunctionKey; break;
+ case 126: key = NSUpArrowFunctionKey; break;
+ case 122: key = NSF1FunctionKey; break;
+ case 120: key = NSF2FunctionKey; break;
+ case 99: key = NSF3FunctionKey; break;
+ case 118: key = NSF4FunctionKey; break;
+ case 96: key = NSF5FunctionKey; break;
+ case 97: key = NSF6FunctionKey; break;
+ case 98: key = NSF7FunctionKey; break;
+ case 100: key = NSF8FunctionKey; break;
+ case 101: key = NSF9FunctionKey; break;
+ case 109: key = NSF10FunctionKey; break;
+ case 103: key = NSF11FunctionKey; break;
+ case 111: key = NSF12FunctionKey; break;
+ case 105: key = NSF13FunctionKey; break;
+ case 107: key = NSF14FunctionKey; break;
+ case 113: key = NSF15FunctionKey; break;
+ case 106: key = NSF16FunctionKey; break;
+ // Forward delete is a terrible name so we'll use the glyph Apple puts on
+ // their current keyboards
+ case 117: key = 0x2326; break;
+
+ // Now we have keys that can be hard coded but don't have good glyph
+ // representations. Sure, the Apple menu manager has glyphs for them, but
+ // an informal poll of Google developers shows no one really knows what
+ // they mean, so its probably a good idea to use strings. Unfortunately
+ // this also means localization (*sigh*). We'll use the real English
+ // strings here as keys so that even if localization is missed we'll do OK
+ // in output.
+
+ // Whitespace
+ case 36: key = '\r'; localizedKey = @"Return"; break;
+ case 76: key = 0x3; localizedKey = @"Enter"; break;
+ case 48: key = 0x9; localizedKey = @"Tab"; break;
+ case 49: key = 0xA0; localizedKey = @"Space"; break;
+ // Control keys
+ case 51: key = 0x8; localizedKey = @"Delete"; break;
+ case 71: key = NSClearDisplayFunctionKey; localizedKey = @"Clear"; break;
+ case 53: key = 0x1B; localizedKey = @"Esc"; break;
+ case 115: key = NSHomeFunctionKey; localizedKey = @"Home"; break;
+ case 116: key = NSPageUpFunctionKey; localizedKey = @"Page Up"; break;
+ case 119: key = NSEndFunctionKey; localizedKey = @"End"; break;
+ case 121: key = NSPageDownFunctionKey; localizedKey = @"Page Down"; break;
+ case 114: key = NSHelpFunctionKey; localizedKey = @"Help"; break;
+ // Keypad keys
+ // There is no good way we could find to glyph these. We tried a variety
+ // of Unicode glyphs, and the menu manager wouldn't take them. We tried
+ // subscript numbers, circled numbers and superscript numbers with no
+ // luck. It may be a bit confusing to the user, but we're happy to hear
+ // any suggestions.
+ case 65: key = '.'; localizedKey = @"Keypad ."; break;
+ case 67: key = '*'; localizedKey = @"Keypad *"; break;
+ case 69: key = '+'; localizedKey = @"Keypad +"; break;
+ case 75: key = '/'; localizedKey = @"Keypad /"; break;
+ case 78: key = '-'; localizedKey = @"Keypad -"; break;
+ case 81: key = '='; localizedKey = @"Keypad ="; break;
+ case 82: key = '0'; localizedKey = @"Keypad 0"; break;
+ case 83: key = '1'; localizedKey = @"Keypad 1"; break;
+ case 84: key = '2'; localizedKey = @"Keypad 2"; break;
+ case 85: key = '3'; localizedKey = @"Keypad 3"; break;
+ case 86: key = '4'; localizedKey = @"Keypad 4"; break;
+ case 87: key = '5'; localizedKey = @"Keypad 5"; break;
+ case 88: key = '6'; localizedKey = @"Keypad 6"; break;
+ case 89: key = '7'; localizedKey = @"Keypad 7"; break;
+ case 91: key = '8'; localizedKey = @"Keypad 8"; break;
+ case 92: key = '9'; localizedKey = @"Keypad 9"; break;
+
+ }
+
+ // If they asked for strings, and we have one return it. Otherwise, return
+ // any key we've picked.
+ if (!useGlyph && localizedKey) {
+ return NSLocalizedStringFromTableInBundle(localizedKey, @"KeyCode",
+ bundle, @"");
+ } else if (key != 0) {
+ return [NSString stringWithFormat:@"%C", key];
+ }
+
+ // Everything else should be printable so look it up in the current keyboard
+ UCKeyboardLayout *uchrData = NULL;
+
+ OSStatus err = noErr;
+#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
+ // layout
+ KeyboardLayoutRef currentLayout = NULL;
+ // Get the layout kind
+ SInt32 currentLayoutKind = -1;
+ if ([GTMSystemVersion isLeopardOrGreater]
+ && kGTM_TISPropertyUnicodeKeyLayoutData
+ && GTM_TISGetInputSourceProperty
+ && GTM_TISCopyCurrentKeyboardLayoutInputSource) {
+ // On Leopard we use the new improved TIS interfaces which work for input
+ // sources as well as keyboard layouts.
+ TISInputSourceRef inputSource
+ = GTM_TISCopyCurrentKeyboardLayoutInputSource();
+ if (inputSource) {
+ CFDataRef uchrDataRef
+ = GTM_TISGetInputSourceProperty(inputSource,
+ kGTM_TISPropertyUnicodeKeyLayoutData);
+ if(uchrDataRef) {
+ uchrData = (UCKeyboardLayout*)CFDataGetBytePtr(uchrDataRef);
+ }
+ CFRelease(inputSource);
+ }
+ } else {
+ // Tiger we use keyboard layouts as it's the best we can officially do.
+ err = KLGetCurrentKeyboardLayout(&currentLayout);
+ if (err != noErr) { // COV_NF_START
+ _GTMDevLog(@"failed to fetch the keyboard layout, err=%d", err);
+ return nil;
+ } // COV_NF_END
+
+ err = KLGetKeyboardLayoutProperty(currentLayout,
+ kKLKind,
+ (const void **)&currentLayoutKind);
+ if (err != noErr) { // COV_NF_START
+ _GTMDevLog(@"failed to fetch the keyboard layout kind property, err=%d",
+ err);
+ return nil;
+ } // COV_NF_END
+
+ if (currentLayoutKind != kKLKCHRKind) {
+ err = KLGetKeyboardLayoutProperty(currentLayout,
+ kKLuchrData,
+ (const void **)&uchrData);
+ if (err != noErr) { // COV_NF_START
+ _GTMDevLog(@"failed to fetch the keyboard layout uchar data, err=%d",
+ err);
+ return nil;
+ } // COV_NF_END
+ }
+ }
+#else
+ TISInputSourceRef inputSource = TISCopyCurrentKeyboardLayoutInputSource();
+ if (inputSource) {
+ CFDataRef uchrDataRef
+ = TISGetInputSourceProperty(inputSource, kTISPropertyUnicodeKeyLayoutData);
+ if(uchrDataRef) {
+ uchrData = (UCKeyboardLayout*)CFDataGetBytePtr(uchrDataRef);
+ }
+ CFRelease(inputSource);
+ }
+#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
+
+ NSString *keystrokeString = nil;
+ if (uchrData) {
+ // uchr layout data is available, this is our preference
+ UniCharCount uchrCharLength = 0;
+ UniChar uchrChars[256] = { 0 };
+ UInt32 uchrDeadKeyState = 0;
+ err = UCKeyTranslate(uchrData,
+ keycode,
+ kUCKeyActionDisplay,
+ 0, // No modifiers
+ LMGetKbdType(),
+ kUCKeyTranslateNoDeadKeysMask,
+ &uchrDeadKeyState,
+ sizeof(uchrChars) / sizeof(UniChar),
+ &uchrCharLength,
+ uchrChars);
+ if (err != noErr) { // COV_NF_START
+ _GTMDevLog(@"failed to translate the keycode, err=%d", err);
+ return nil;
+ } // COV_NF_END
+ if (uchrCharLength < 1) return nil;
+ keystrokeString = [NSString stringWithCharacters:uchrChars
+ length:uchrCharLength];
+ }
+#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
+ else if (currentLayoutKind == kKLKCHRKind) {
+ // Only KCHR layout data is available, go old school
+ void *KCHRData = NULL;
+ err = KLGetKeyboardLayoutProperty(currentLayout, kKLKCHRData,
+ (const void **)&KCHRData);
+ if (err != noErr) { // COV_NF_START
+ _GTMDevLog(@"failed to fetch the keyboard layout uchar data, err=%d",
+ err);
+ return nil;
+ } // COV_NF_END
+ // Turn into character code
+ UInt32 keyTranslateState = 0;
+ UInt32 twoKCHRChars = KeyTranslate(KCHRData, keycode, &keyTranslateState);
+ if (!twoKCHRChars) return nil;
+ // Unpack the fields
+ char firstChar = (char)((twoKCHRChars & 0x00FF0000) >> 16);
+ char secondChar = (char)(twoKCHRChars & 0x000000FF);
+ // May have one or two characters
+ if (firstChar && secondChar) {
+ NSString *str1
+ = [[[NSString alloc] initWithBytes:&firstChar
+ length:1
+ encoding:NSMacOSRomanStringEncoding] autorelease];
+ NSString *str2
+ = [[[NSString alloc] initWithBytes:&secondChar
+ length:1
+ encoding:NSMacOSRomanStringEncoding] autorelease];
+ keystrokeString = [NSString stringWithFormat:@"%@%@",
+ [str1 uppercaseString],
+ [str2 uppercaseString]];
+ } else {
+ keystrokeString = [[[NSString alloc] initWithBytes:&secondChar
+ length:1
+ encoding:NSMacOSRomanStringEncoding] autorelease];
+ [keystrokeString uppercaseString];
+ }
+ }
+#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
+
+ // Sanity we got a stroke
+ if (!keystrokeString || ![keystrokeString length]) return nil;
+
+ // Sanity check the keystroke string for unprintable characters
+ NSMutableCharacterSet *validChars
+ = [[[NSCharacterSet alphanumericCharacterSet] mutableCopy] autorelease];
+ [validChars formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]];
+ [validChars formUnionWithCharacterSet:[NSCharacterSet symbolCharacterSet]];
+ for (unsigned int i = 0; i < [keystrokeString length]; i++) {
+ if (![validChars characterIsMember:[keystrokeString characterAtIndex:i]]) {
+ return nil;
+ }
+ }
+
+ if (!useGlyph) {
+ // menus want glyphs in the original lowercase forms, so we only upper this
+ // if we aren't using it as a glyph.
+ keystrokeString = [keystrokeString uppercaseString];
+ }
+
+ return keystrokeString;
+
+}
+
+@end
+
+@implementation GTMHotKeyFieldEditor
+
+GTMOBJECT_SINGLETON_BOILERPLATE(GTMHotKeyFieldEditor, sharedHotKeyFieldEditor)
+
+- (id)init {
+
+ self = [super init];
+ if (!self) return nil;
+ [self setFieldEditor:YES]; // We are a field editor
+
+ return self;
+
+}
+
+- (void)dealloc {
+
+ [hotKeyDict_ release];
+ [super dealloc];
+
+}
+
+- (NSArray *)acceptableDragTypes {
+
+ // Don't take drags
+ return [NSArray array];
+
+}
+
+- (NSArray *)readablePasteboardTypes {
+
+ // No pasting
+ return [NSArray array];
+
+}
+
+- (NSArray *)writablePasteboardTypes {
+
+ // No copying
+ return [NSArray array];
+
+}
+
+- (BOOL)becomeFirstResponder {
+
+ // We need to lose focus any time the window is not key
+ NSNotificationCenter *dc = [NSNotificationCenter defaultCenter];
+ [dc addObserver:self
+ selector:@selector(windowResigned:)
+ name:NSWindowDidResignKeyNotification
+ object:[self window]];
+ return [super becomeFirstResponder];
+
+}
+
+- (BOOL)resignFirstResponder {
+
+ // No longer interested in window resign
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ return [super resignFirstResponder];
+
+}
+
+// Private method we use to get out of global hotkey capture when the window
+// is no longer front
+- (void)windowResigned:(NSNotification *)notification {
+
+ // Lose our focus
+ [[self window] makeFirstResponder:[self window]];
+
+}
+
+- (BOOL)shouldDrawInsertionPoint {
+
+ // Show an insertion point, because we'll kill our own focus after
+ // each entry
+ return YES;
+
+}
+
+- (NSRange)selectionRangeForProposedRange:(NSRange)proposedSelRange
+ granularity:(NSSelectionGranularity)granularity {
+
+ // Always select everything
+ return NSMakeRange(0, [[self textStorage] length]);
+
+}
+
+- (void)keyDown:(NSEvent *)theEvent {
+
+ if ([self shouldBypassEvent:theEvent]) {
+ [super keyDown:theEvent];
+ } else {
+ // Try to eat the event
+ [self processEventToHotKeyAndString:theEvent];
+ }
+
+}
+
+- (BOOL)performKeyEquivalent:(NSEvent *)theEvent {
+
+ if ([self shouldBypassEvent:theEvent]) {
+ return [super performKeyEquivalent:theEvent];
+ } else {
+ // We always eat these key strokes while we have focus
+ [self processEventToHotKeyAndString:theEvent];
+ return YES;
+ }
+
+}
+
+// Private do method that tell us to ignore certain events
+- (BOOL)shouldBypassEvent:(NSEvent *)theEvent {
+
+ UInt16 keyCode = [theEvent keyCode];
+ NSUInteger modifierFlags
+ = [theEvent modifierFlags] & NSDeviceIndependentModifierFlagsMask;
+
+ // Ignore all events containing tabs. They have special meaning to fields
+ // and some (Cmd Tab variants) are always consumed by the Dock, so users
+ // just shouldn't be able to use them.
+ if (keyCode == 48) { // Tab
+ // Just to be extra clear if the user is trying to use Dock hotkeys beep
+ // at them
+ if ((modifierFlags == NSCommandKeyMask) ||
+ (modifierFlags == (NSCommandKeyMask | NSShiftKeyMask))) {
+ NSBeep();
+ }
+ return YES;
+ }
+
+ // Don't eat Cmd-Q. Users could have it as a hotkey, but its more likely
+ // they're trying to quit
+ if ((keyCode == 12) && (modifierFlags == NSCommandKeyMask)) {
+ return YES;
+ }
+ // Same for Cmd-W, user is probably trying to close the window
+ if ((keyCode == 13) && (modifierFlags == NSCommandKeyMask)) {
+ return YES;
+ }
+
+ return NO;
+
+}
+
+// Private method that turns events into strings and dictionaries for our
+// hotkey plumbing.
+- (void)processEventToHotKeyAndString:(NSEvent *)theEvent {
+
+ // Construct a dictionary of the event as a hotkey pref
+ NSDictionary *newHotKey = [self hotKeyDictionaryForEvent:theEvent];
+ if (!newHotKey) {
+ NSBeep();
+ return; // No action, but don't give up focus
+ }
+ NSString *prettyString = [GTMHotKeyTextField displayStringForHotKey:newHotKey];
+ if (!prettyString) {
+ NSBeep();
+ return;
+ }
+
+ // Replacement range
+ NSRange replaceRange = NSMakeRange(0, [[self textStorage] length]);
+
+ // Ask for permission to replace
+ if (![self shouldChangeTextInRange:replaceRange
+ replacementString:prettyString]) {
+ // If replacement was disallowed, change nothing, including hotKeyDict_
+ NSBeep();
+ return;
+ }
+
+ // Replacement was allowed, update
+ [hotKeyDict_ autorelease];
+ hotKeyDict_ = [newHotKey retain];
+
+ // Set string on self, allowing super to handle attribute copying
+ [self setString:prettyString];
+
+ // Finish the change
+ [self didChangeText];
+
+ // Force editing to end. This sends focus off into space slightly, but
+ // its better than constantly capturing user events. This is exactly
+ // like the Apple editor in their Keyboard pref pane.
+ [[[self delegate] cell] endEditing:self];
+
+}
+
+- (NSDictionary *)hotKeyDictionary {
+
+ return hotKeyDict_;
+
+}
+
+- (void)setHotKeyDictionary:(NSDictionary *)hotKey {
+
+ [hotKeyDict_ autorelease];
+ hotKeyDict_ = [hotKey copy];
+ // Update content
+ NSString *prettyString = nil;
+ if (hotKeyDict_) {
+ prettyString = [GTMHotKeyTextField displayStringForHotKey:hotKey];
+ }
+ if (!prettyString) {
+ prettyString = @"";
+ }
+ [self setString:prettyString];
+
+}
+
+- (NSDictionary *)hotKeyDictionaryForEvent:(NSEvent *)event{
+
+ if (!event) return nil;
+
+ // Check event
+ NSUInteger flags = [event modifierFlags];
+ UInt16 keycode = [event keyCode];
+ // If the event has no modifiers do nothing
+ NSUInteger allModifiers = (NSCommandKeyMask | NSAlternateKeyMask |
+ NSControlKeyMask | NSShiftKeyMask);
+ if (!(flags & allModifiers)) return nil;
+ // If the event has high bits in keycode do nothing
+ if (keycode & 0xFF00) return nil;
+
+ // Clean the flags to only contain things we care about
+ UInt32 cleanFlags = 0;
+ if (flags & NSCommandKeyMask) cleanFlags |= NSCommandKeyMask;
+ if (flags & NSAlternateKeyMask) cleanFlags |= NSAlternateKeyMask;
+ if (flags & NSControlKeyMask) cleanFlags |= NSControlKeyMask;
+ if (flags & NSShiftKeyMask) cleanFlags |= NSShiftKeyMask;
+
+ return [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSNumber numberWithBool:NO],
+ kGTMHotKeyDoubledModifierKey,
+ [NSNumber numberWithUnsignedInt:keycode],
+ kGTMHotKeyKeyCodeKey,
+ [NSNumber numberWithUnsignedInt:cleanFlags],
+ kGTMHotKeyModifierFlagsKey,
+ nil];
+
+}
+@end
+
diff --git a/AppKit/GTMHotKeyTextFieldTest.m b/AppKit/GTMHotKeyTextFieldTest.m
new file mode 100644
index 0000000..ee1bfc2
--- /dev/null
+++ b/AppKit/GTMHotKeyTextFieldTest.m
@@ -0,0 +1,204 @@
+// GTMHotKeyTextFieldTest.m
+//
+// Copyright 2006-2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy
+// of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+//
+
+#import "GTMHotKeyTextField.h"
+
+#import "GTMSenTestCase.h"
+
+@interface GTMHotKeyTextField (PrivateMethods)
+// Private methods which we want to access to test
++ (BOOL)isValidHotKey:(NSDictionary *)hotKey;
++ (NSString *)displayStringForHotKey:(NSDictionary *)hotKey;
+@end
+
+@interface GTMHotKeyTextFieldTest : GTMTestCase
+@end
+
+@implementation GTMHotKeyTextFieldTest
+
+- (void)testStringForModifierFlags {
+
+ // Make sure only the flags we expect generate things in their strings
+ STAssertEquals([[GTMHotKeyTextField stringForModifierFlags:NSAlphaShiftKeyMask] length],
+ (NSUInteger)0, nil);
+ STAssertEquals([[GTMHotKeyTextField stringForModifierFlags:NSShiftKeyMask] length],
+ (NSUInteger)1, nil);
+ STAssertEquals([[GTMHotKeyTextField stringForModifierFlags:NSControlKeyMask] length],
+ (NSUInteger)1, nil);
+ STAssertEquals([[GTMHotKeyTextField stringForModifierFlags:NSAlternateKeyMask] length],
+ (NSUInteger)1, nil);
+ STAssertEquals([[GTMHotKeyTextField stringForModifierFlags:NSCommandKeyMask] length],
+ (NSUInteger)1, nil);
+ STAssertEquals([[GTMHotKeyTextField stringForModifierFlags:NSNumericPadKeyMask] length],
+ (NSUInteger)0, nil);
+ STAssertEquals([[GTMHotKeyTextField stringForModifierFlags:NSHelpKeyMask] length],
+ (NSUInteger)0, nil);
+ STAssertEquals([[GTMHotKeyTextField stringForModifierFlags:NSFunctionKeyMask] length],
+ (NSUInteger)0, nil);
+
+ // And some quick checks combining flags to make sure the string gets longer
+ STAssertEquals([[GTMHotKeyTextField stringForModifierFlags:(NSShiftKeyMask |
+ NSAlternateKeyMask)] length],
+ (NSUInteger)2, nil);
+ STAssertEquals([[GTMHotKeyTextField stringForModifierFlags:(NSShiftKeyMask |
+ NSAlternateKeyMask |
+ NSCommandKeyMask)] length],
+ (NSUInteger)3, nil);
+ STAssertEquals([[GTMHotKeyTextField stringForModifierFlags:(NSShiftKeyMask |
+ NSAlternateKeyMask |
+ NSCommandKeyMask |
+ NSControlKeyMask)] length],
+ (NSUInteger)4, nil);
+
+}
+
+- (void)testStringForKeycode_useGlyph_resourceBundle {
+ NSBundle *bundle = [NSBundle bundleForClass:[self class]];
+ STAssertNotNil(bundle, @"failed to get our bundle?");
+ NSString *str;
+
+ // We need a better test, but for now, we'll just loop through things we know
+ // we handle.
+
+ // TODO: we need to force the pre leopard code path during tests.
+
+ UInt16 testData[] = {
+ 123, 124, 125, 126, 122, 120, 99, 118, 96, 97, 98, 100, 101, 109, 103, 111,
+ 105, 107, 113, 106, 117, 36, 76, 48, 49, 51, 71, 53, 115, 116, 119, 121,
+ 114, 65, 67, 69, 75, 78, 81, 82, 83, 84, 85, 86, 87, 88, 89, 91, 92,
+ };
+ for (int useGlyph = 0 ; useGlyph < 2 ; ++useGlyph) {
+ for (size_t i = 0; i < (sizeof(testData) / sizeof(UInt16)); ++i) {
+ UInt16 keycode = testData[i];
+
+ str = [GTMHotKeyTextField stringForKeycode:keycode
+ useGlyph:useGlyph
+ resourceBundle:bundle];
+ STAssertNotNil(str,
+ @"failed to get a string for keycode %u (useGlyph:%@)",
+ keycode, (useGlyph ? @"YES" : @"NO"));
+ STAssertGreaterThan([str length], (NSUInteger)0,
+ @"got an empty string for keycode %u (useGlyph:%@)",
+ keycode, (useGlyph ? @"YES" : @"NO"));
+ }
+ }
+}
+
+- (void)testGTMHotKeyPrettyString {
+ NSDictionary *hkDict;
+
+ hkDict = [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSNumber numberWithBool:NO],
+ kGTMHotKeyDoubledModifierKey,
+ [NSNumber numberWithUnsignedInt:114],
+ kGTMHotKeyKeyCodeKey,
+ [NSNumber numberWithUnsignedInt:NSCommandKeyMask],
+ kGTMHotKeyModifierFlagsKey,
+ nil];
+ STAssertNotNil(hkDict, nil);
+ STAssertNotNil([GTMHotKeyTextField displayStringForHotKey:hkDict], nil);
+
+ hkDict = [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSNumber numberWithUnsignedInt:114],
+ kGTMHotKeyKeyCodeKey,
+ [NSNumber numberWithUnsignedInt:NSCommandKeyMask],
+ kGTMHotKeyModifierFlagsKey,
+ nil];
+ STAssertNotNil(hkDict, nil);
+ STAssertNotNil([GTMHotKeyTextField displayStringForHotKey:hkDict], nil);
+
+ hkDict = [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSNumber numberWithBool:NO],
+ kGTMHotKeyDoubledModifierKey,
+ [NSNumber numberWithUnsignedInt:NSCommandKeyMask],
+ kGTMHotKeyModifierFlagsKey,
+ nil];
+ STAssertNotNil(hkDict, nil);
+ STAssertNotNil([GTMHotKeyTextField displayStringForHotKey:hkDict], nil);
+
+ hkDict = [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSNumber numberWithBool:NO],
+ kGTMHotKeyDoubledModifierKey,
+ [NSNumber numberWithUnsignedInt:114],
+ kGTMHotKeyKeyCodeKey,
+ nil];
+ STAssertNotNil(hkDict, nil);
+ STAssertNil([GTMHotKeyTextField displayStringForHotKey:hkDict], nil);
+
+ hkDict = [NSDictionary dictionary];
+ STAssertNotNil(hkDict, nil);
+ STAssertNil([GTMHotKeyTextField displayStringForHotKey:hkDict], nil);
+
+ STAssertNil([GTMHotKeyTextField displayStringForHotKey:nil], nil);
+
+}
+
+- (void)testGTMHotKeyDictionaryAppearsValid {
+ NSDictionary *hkDict;
+
+ hkDict = [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSNumber numberWithBool:NO],
+ kGTMHotKeyDoubledModifierKey,
+ [NSNumber numberWithUnsignedInt:114],
+ kGTMHotKeyKeyCodeKey,
+ [NSNumber numberWithUnsignedInt:NSCommandKeyMask],
+ kGTMHotKeyModifierFlagsKey,
+ nil];
+ STAssertNotNil(hkDict, nil);
+ STAssertTrue([GTMHotKeyTextField isValidHotKey:hkDict], nil);
+
+ hkDict = [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSNumber numberWithUnsignedInt:114],
+ kGTMHotKeyKeyCodeKey,
+ [NSNumber numberWithUnsignedInt:NSCommandKeyMask],
+ kGTMHotKeyModifierFlagsKey,
+ nil];
+ STAssertNotNil(hkDict, nil);
+ STAssertFalse([GTMHotKeyTextField isValidHotKey:hkDict], nil);
+
+ hkDict = [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSNumber numberWithBool:NO],
+ kGTMHotKeyDoubledModifierKey,
+ [NSNumber numberWithUnsignedInt:NSCommandKeyMask],
+ kGTMHotKeyModifierFlagsKey,
+ nil];
+ STAssertNotNil(hkDict, nil);
+ STAssertFalse([GTMHotKeyTextField isValidHotKey:hkDict], nil);
+
+ hkDict = [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSNumber numberWithBool:NO],
+ kGTMHotKeyDoubledModifierKey,
+ [NSNumber numberWithUnsignedInt:114],
+ kGTMHotKeyKeyCodeKey,
+ nil];
+ STAssertNotNil(hkDict, nil);
+ STAssertFalse([GTMHotKeyTextField isValidHotKey:hkDict], nil);
+
+ hkDict = [NSDictionary dictionary];
+ STAssertNotNil(hkDict, nil);
+ STAssertFalse([GTMHotKeyTextField isValidHotKey:hkDict], nil);
+
+ STAssertFalse([GTMHotKeyTextField isValidHotKey:nil], nil);
+
+ // Make sure it doesn't choke w/ an object of the wrong time (since the dicts
+ // have to be saved/reloaded.
+ hkDict = (id)[NSString string];
+ STAssertNotNil(hkDict, nil);
+ STAssertFalse([GTMHotKeyTextField isValidHotKey:hkDict], nil);
+}
+
+@end
diff --git a/AppKit/GTMLoginItems.h b/AppKit/GTMLoginItems.h
index 1531bd0..b4375b4 100644
--- a/AppKit/GTMLoginItems.h
+++ b/AppKit/GTMLoginItems.h
@@ -17,15 +17,16 @@
//
#import <Foundation/Foundation.h>
+#import "GTMDefines.h"
/// Login items key constants, used as keys in |+loginItems|
//
// Item name
-extern NSString * const kGTMLoginItemsNameKey;
+GTM_EXTERN NSString * const kGTMLoginItemsNameKey;
// Item path
-extern NSString * const kGTMLoginItemsPathKey;
+GTM_EXTERN NSString * const kGTMLoginItemsPathKey;
// Hidden (NSNumber bool)
-extern NSString * const kGTMLoginItemsHiddenKey;
+GTM_EXTERN NSString * const kGTMLoginItemsHiddenKey;
/// GTMLoginItems
//