diff options
author | thomasvl@gmail.com <thomasvl@gmail.com@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3> | 2008-12-12 15:24:34 +0000 |
---|---|---|
committer | thomasvl@gmail.com <thomasvl@gmail.com@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3> | 2008-12-12 15:24:34 +0000 |
commit | 2e8516354aacef064d01425808da06d2cdcb4791 (patch) | |
tree | 9da4758828930280d32f18d54ece7a249df742c7 /AppKit | |
parent | 9f64d056dd70f2f938ac6f5adb8e75b650dc2e1a (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.h | 380 | ||||
-rw-r--r-- | AppKit/GTMCarbonEvent.m | 709 | ||||
-rw-r--r-- | AppKit/GTMCarbonEventTest.m | 360 | ||||
-rw-r--r-- | AppKit/GTMGetURLHandler.m | 82 | ||||
-rw-r--r-- | AppKit/GTMGetURLHandlerTest.m | 85 | ||||
-rw-r--r-- | AppKit/GTMHotKeyTextField.h | 127 | ||||
-rw-r--r-- | AppKit/GTMHotKeyTextField.m | 1009 | ||||
-rw-r--r-- | AppKit/GTMHotKeyTextFieldTest.m | 204 | ||||
-rw-r--r-- | AppKit/GTMLoginItems.h | 7 |
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(¤tLayout); + 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 **)¤tLayoutKind); + 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 // |