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 | |
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.
47 files changed, 4985 insertions, 197 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 // diff --git a/Foundation/GTMFileSystemKQueue.h b/Foundation/GTMFileSystemKQueue.h new file mode 100644 index 0000000..a81d433 --- /dev/null +++ b/Foundation/GTMFileSystemKQueue.h @@ -0,0 +1,78 @@ +// +// GTMFileSystemKQueue.h +// +// 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 <Foundation/Foundation.h> + +#import <sys/event.h> // for kqueue() and kevent and the NOTE_* constants + +// Event constants +enum { + kGTMFileSystemKQueueDeleteEvent = NOTE_DELETE, + kGTMFileSystemKQueueWriteEvent = NOTE_WRITE, + kGTMFileSystemKQueueExtendEvent = NOTE_EXTEND, + kGTMFileSystemKQueueAttributeChangeEvent = NOTE_ATTRIB, + kGTMFileSystemKQueueLinkChangeEvent = NOTE_LINK, + kGTMFileSystemKQueueRenameEvent = NOTE_RENAME, + kGTMFileSystemKQueueRevokeEvent = NOTE_REVOKE, + kGTMFileSystemKQueueAllEvents = NOTE_DELETE | NOTE_WRITE | NOTE_EXTEND | + NOTE_ATTRIB | NOTE_LINK | NOTE_RENAME | + NOTE_REVOKE, +}; +typedef unsigned int GTMFileSystemKQueueEvents; + +// GTMFileSystemKQueue. +// +// This is a very simple, easy-to-use class for registering handlers that get +// called when a events happen to a given file system path. +// +// The default runloop for the first path kqueued is used for notification +// delivery, so keep that in mind when you're using this class. This class +// explicitly does not handle arbitrary runloops and threading. +// +@interface GTMFileSystemKQueue : NSObject { + @private + NSString *path_; + int fd_; + GTMFileSystemKQueueEvents events_; + BOOL acrossReplace_; + __weak id target_; + SEL action_; +} + +// |path| is the full path to monitor. |events| is a combination of events +// listed above that you want notification of. |acrossReplace| will cause this +// object to reattach when a the file is deleted & recreated or moved out of the +// way and a new one put in place. |selector| should be of the signature: +// - (void)fileSystemKQueue:(GTMFileSystemKQueue *)fskq +// events:(GTMFileSystemKQueueEvents)events; +// where the events can be one or more of the events listed above ORed together. +// +// NOTE: |acrossReplace| is not fool proof. If the file is renamed/deleted, +// then the object will make one attempt at the time it gets the rename/delete +// to reopen the file. If the new file has not been created, no more action is +// taken. To handle the file coming into existance later, you need to monitor +// the directory in some other way. +- (id)initWithPath:(NSString *)path + forEvents:(GTMFileSystemKQueueEvents)events + acrossReplace:(BOOL)acrossReplace + target:(id)target + action:(SEL)action; + +- (NSString *)path; + +@end diff --git a/Foundation/GTMFileSystemKQueue.m b/Foundation/GTMFileSystemKQueue.m new file mode 100644 index 0000000..e2c399e --- /dev/null +++ b/Foundation/GTMFileSystemKQueue.m @@ -0,0 +1,250 @@ +// +// GTMFileSystemKQueue.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 "GTMFileSystemKQueue.h" +#import <unistd.h> + +#import "GTMDebugSelectorValidation.h" + + +// File descriptor for the kqueue that will hold all of our file system events. +static int gFileSystemKQueueFileDescriptor = 0; + +// A wrapper around the kqueue file descriptor so we can put it into a +// runloop. +static CFSocketRef gRunLoopSocket = NULL; + + +@interface GTMFileSystemKQueue (PrivateMethods) +- (void)notify:(GTMFileSystemKQueueEvents)eventFFlags; +- (void)addFileDescriptorMonitor:(int)fd; +- (int)registerWithKQueue; +- (void)unregisterWithKQueue; +@end + + +@implementation GTMFileSystemKQueue + +-(id)init { + // Folks shouldn't call init directly, so they get what they deserve. + _GTMDevLog(@"Don't call init, use " + @"initWithPath:forEvents:acrossReplace:target:action:"); + return [self initWithPath:nil forEvents:0 acrossReplace:NO + target:nil action:nil]; +} + + +- (id)initWithPath:(NSString *)path + forEvents:(GTMFileSystemKQueueEvents)events + acrossReplace:(BOOL)acrossReplace + target:(id)target + action:(SEL)action { + + if ((self = [super init])) { + + fd_ = -1; + path_ = [path copy]; + events_ = events; + acrossReplace_ = acrossReplace; + + target_ = target; // Don't retain since target will most likely retain us. + action_ = action; + if ([path_ length] == 0 || !events_ || !target_ || !action_) { + [self release]; + return nil; + } + + // Make sure it imples what we expect + GTMAssertSelectorNilOrImplementedWithArguments(target_, + action_, + @encode(GTMFileSystemKQueue*), + @encode(GTMFileSystemKQueueEvents), + NULL); + + fd_ = [self registerWithKQueue]; + if (fd_ < 0) { + [self release]; + return nil; + } + } + return self; +} + +- (void)finalize { + [self unregisterWithKQueue]; + + [super finalize]; +} + +- (void)dealloc { + [self unregisterWithKQueue]; + [path_ release]; + + [super dealloc]; +} + +- (NSString *)path { + return path_; +} + +// Cribbed from Advanced Mac OS X Programming. +static void SocketCallBack(CFSocketRef socketref, CFSocketCallBackType type, + CFDataRef address, const void *data, void *info) { + struct kevent event; + + if (kevent(gFileSystemKQueueFileDescriptor, NULL, 0, &event, 1, NULL) == -1) { + _GTMDevLog(@"could not pick up kqueue event. Errno %d", errno); // COV_NF_LINE + } else { + GTMFileSystemKQueue *fskq = (GTMFileSystemKQueue *)event.udata; + [fskq notify:event.fflags]; + } + +} + +// Cribbed from Advanced Mac OS X Programming +- (void)addFileDescriptorMonitor:(int)fd { + _GTMDevAssert(gRunLoopSocket == NULL, @"socket should be NULL at this point"); + + CFSocketContext context = { 0, NULL, NULL, NULL, NULL }; + gRunLoopSocket = CFSocketCreateWithNative(kCFAllocatorDefault, + fd, + kCFSocketReadCallBack, + SocketCallBack, + &context); + if (gRunLoopSocket == NULL) { + _GTMDevLog(@"could not CFSocketCreateWithNative"); // COV_NF_LINE + goto bailout; // COV_NF_LINE + } + + CFRunLoopSourceRef rls; + rls = CFSocketCreateRunLoopSource(NULL, gRunLoopSocket, 0); + if (rls == NULL) { + _GTMDevLog(@"could not create a run loop source"); // COV_NF_LINE + goto bailout; // COV_NF_LINE + } + + CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, + kCFRunLoopDefaultMode); + CFRelease(rls); + + bailout: + return; + +} + +// Returns the FD we got in registering +- (int)registerWithKQueue { + + // Make sure we have our kqueue. + if (gFileSystemKQueueFileDescriptor == 0) { + gFileSystemKQueueFileDescriptor = kqueue(); + + if (gFileSystemKQueueFileDescriptor == -1) { + // COV_NF_START + _GTMDevLog(@"could not make filesystem kqueue. Errno %d", errno); + return -1; + // COV_NF_END + } + + // Add the kqueue file descriptor to the runloop. + [self addFileDescriptorMonitor:gFileSystemKQueueFileDescriptor]; + } + + int newFD = open([path_ fileSystemRepresentation], O_EVTONLY, 0); + if (newFD >= 0) { + + // Add a new event for the file. + struct kevent filter; + EV_SET(&filter, newFD, EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_CLEAR, + events_, 0, self); + + const struct timespec noWait = { 0, 0 }; + if (kevent(gFileSystemKQueueFileDescriptor, &filter, 1, NULL, 0, &noWait) == -1) { + // COV_NF_START + _GTMDevLog(@"could not add event for path %@. Errno %d", path_, errno); + close(newFD); + newFD = -1; + // COV_NF_END + } + } + + return newFD; +} + +- (void)unregisterWithKQueue { + // Short-circuit cases where we didn't actually register a kqueue event. + if (fd_ < 0) return; + + struct kevent filter; + EV_SET(&filter, fd_, EVFILT_VNODE, EV_DELETE, 0, 0, self); + + const struct timespec noWait = { 0, 0 }; + if (kevent(gFileSystemKQueueFileDescriptor, &filter, 1, NULL, 0, &noWait) != 0) { + _GTMDevLog(@"could not remove event for path %@. Errno %d", path_, errno); // COV_NF_LINE + } + + // Now close the file down + close(fd_); + fd_ = -1; +} + + +- (void)notify:(GTMFileSystemKQueueEvents)eventFFlags { + + // Some notifications get a little bit of overhead first + + if (eventFFlags & NOTE_REVOKE) { + // COV_NF_START - no good way to do this in a unittest + // Assume revoke means unmount and give up + [self unregisterWithKQueue]; + // COV_NF_END + } + + if (eventFFlags & NOTE_DELETE) { + [self unregisterWithKQueue]; + if (acrossReplace_) { + fd_ = [self registerWithKQueue]; + } + } + + if (eventFFlags & NOTE_RENAME) { + // If we're doing it across replace, we move to the new fd for the new file + // that might have come onto the path. if we aren't doing accross replace, + // nothing to do, just stay on the file. + if (acrossReplace_) { + int newFD = [self registerWithKQueue]; + if (newFD >= 0) { + [self unregisterWithKQueue]; + fd_ = newFD; + } + } + } + + // Now, fire the selector + NSMethodSignature *methodSig = [target_ methodSignatureForSelector:action_]; + _GTMDevAssert(methodSig != nil, @"failed to get the signature?"); + NSInvocation *invocation + = [NSInvocation invocationWithMethodSignature:methodSig]; + [invocation setTarget:target_]; + [invocation setSelector:action_]; + [invocation setArgument:self atIndex:2]; + [invocation setArgument:&eventFFlags atIndex:3]; + [invocation invoke]; +} + +@end diff --git a/Foundation/GTMFileSystemKQueueTest.m b/Foundation/GTMFileSystemKQueueTest.m new file mode 100644 index 0000000..c2ded10 --- /dev/null +++ b/Foundation/GTMFileSystemKQueueTest.m @@ -0,0 +1,394 @@ +// +// GTMFileSystemKQueueTest.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 "GTMFileSystemKQueue.h" +#import "GTMUnitTestDevLog.h" + +@interface GTMFileSystemKQueueTest : GTMTestCase { + @private + NSString *testPath_; + NSString *testPath2_; +} +@end + + +// Helper class to serve as callback target of the kqueue test +@interface GTMFSKQTestHelper : NSObject { + @private + int writes_, renames_, deletes_; +} +@end + +@implementation GTMFSKQTestHelper + +- (void)callbackForQueue:(GTMFileSystemKQueue *)queue + events:(GTMFileSystemKQueueEvents)event { + if (event & kGTMFileSystemKQueueWriteEvent) { + ++writes_; + } + if (event & kGTMFileSystemKQueueDeleteEvent) { + ++deletes_; + } + if (event & kGTMFileSystemKQueueRenameEvent) { + ++renames_; + } +} +- (int)totals { + return writes_ + renames_ + deletes_; +} +- (int)writes { + return writes_; +} +- (int)renames { + return renames_; +} +- (int)deletes { + return deletes_; +} +@end + + +@implementation GTMFileSystemKQueueTest + +- (void)setUp { + NSString *temp = NSTemporaryDirectory(); + testPath_ + = [[temp stringByAppendingPathComponent: + @"GTMFileSystemKQueueTest.testfile"] retain]; + testPath2_ = [[testPath_ stringByAppendingPathExtension:@"2"] retain]; + + // make sure the files aren't in the way of the test + NSFileManager *fm = [NSFileManager defaultManager]; + [fm removeFileAtPath:testPath_ handler:nil]; + [fm removeFileAtPath:testPath2_ handler:nil]; +} + +- (void)tearDown { + // make sure we clean up the files from a failed test + NSFileManager *fm = [NSFileManager defaultManager]; + [fm removeFileAtPath:testPath_ handler:nil]; + [fm removeFileAtPath:testPath2_ handler:nil]; + + [testPath_ release]; + testPath_ = nil; + [testPath2_ release]; + testPath2_ = nil; +} + +- (void)testInit { + GTMFileSystemKQueue *testKQ; + GTMFSKQTestHelper *helper = [[[GTMFSKQTestHelper alloc] init] autorelease]; + STAssertNotNil(helper, nil); + + // init should fail + [GTMUnitTestDevLog expectString:@"Don't call init, use " + @"initWithPath:forEvents:acrossReplace:target:action:"]; + testKQ = [[[GTMFileSystemKQueue alloc] init] autorelease]; + STAssertNil(testKQ, nil); + + // no path + testKQ + = [[[GTMFileSystemKQueue alloc] initWithPath:nil + forEvents:kGTMFileSystemKQueueAllEvents + acrossReplace:YES + target:helper + action:@selector(callbackForQueue:events:)] autorelease]; + STAssertNil(testKQ, nil); + + // not events + testKQ + = [[[GTMFileSystemKQueue alloc] initWithPath:@"/var/log/system.log" + forEvents:0 + acrossReplace:YES + target:helper + action:@selector(callbackForQueue:events:)] autorelease]; + STAssertNil(testKQ, nil); + + // no target + testKQ + = [[[GTMFileSystemKQueue alloc] initWithPath:@"/var/log/system.log" + forEvents:kGTMFileSystemKQueueAllEvents + acrossReplace:YES + target:nil + action:@selector(callbackForQueue:events:)] autorelease]; + STAssertNil(testKQ, nil); + + // no handler + testKQ + = [[[GTMFileSystemKQueue alloc] initWithPath:@"/var/log/system.log" + forEvents:0 + acrossReplace:YES + target:helper + action:nil] autorelease]; + STAssertNil(testKQ, nil); + + + // path that doesn't exist + testKQ + = [[[GTMFileSystemKQueue alloc] initWithPath:@"/path/that/does/not/exist" + forEvents:kGTMFileSystemKQueueAllEvents + acrossReplace:YES + target:helper + action:@selector(callbackForQueue:events:)] autorelease]; + STAssertNil(testKQ, nil); +} + +- (void)spinForEvents:(GTMFSKQTestHelper *)helper { + // Spin the runloop for a second so that the helper callbacks fire + unsigned int attempts = 0; + int initialTotals = [helper totals]; + while (([helper totals] == initialTotals) && (attempts < 10)) { // Try for up to 2s + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.2]]; + attempts++; + } +} + +- (void)testWriteAndDelete { + + NSFileManager *fm = [NSFileManager defaultManager]; + GTMFSKQTestHelper *helper = [[[GTMFSKQTestHelper alloc] init] autorelease]; + STAssertNotNil(helper, nil); + + STAssertTrue([fm createFileAtPath:testPath_ contents:nil attributes:nil], nil); + NSFileHandle *testFH = [NSFileHandle fileHandleForWritingAtPath:testPath_]; + STAssertNotNil(testFH, nil); + + // Start monitoring the file + GTMFileSystemKQueue *testKQ + = [[GTMFileSystemKQueue alloc] initWithPath:testPath_ + forEvents:kGTMFileSystemKQueueAllEvents + acrossReplace:YES + target:helper + action:@selector(callbackForQueue:events:)]; + STAssertNotNil(testKQ, nil); + STAssertEqualObjects([testKQ path], testPath_, nil); + + // Write to the file + [testFH writeData:[@"doh!" dataUsingEncoding:NSUnicodeStringEncoding]]; + + // Spin the runloop for a second so that the helper callbacks fire + [self spinForEvents:helper]; + STAssertEquals([helper totals], 1, nil); + + // Close and delete + [testFH closeFile]; + STAssertTrue([fm removeFileAtPath:testPath_ handler:nil], nil); + + [self spinForEvents:helper]; + STAssertEquals([helper totals], 2, nil); + + // Clean up the kqueue + [testKQ release]; + testKQ = nil; + + STAssertEquals([helper writes], 1, nil); + STAssertEquals([helper deletes], 1, nil); + STAssertEquals([helper renames], 0, nil); +} + +- (void)testWriteAndDeleteAndWrite { + + // One will pass YES to |acrossReplace|, the other will pass NO. + + NSFileManager *fm = [NSFileManager defaultManager]; + GTMFSKQTestHelper *helper = [[[GTMFSKQTestHelper alloc] init] autorelease]; + STAssertNotNil(helper, nil); + GTMFSKQTestHelper *helper2 = [[[GTMFSKQTestHelper alloc] init] autorelease]; + STAssertNotNil(helper, nil); + + // Create a temp file path + STAssertTrue([fm createFileAtPath:testPath_ contents:nil attributes:nil], nil); + NSFileHandle *testFH = [NSFileHandle fileHandleForWritingAtPath:testPath_]; + STAssertNotNil(testFH, nil); + + // Start monitoring the file + GTMFileSystemKQueue *testKQ + = [[GTMFileSystemKQueue alloc] initWithPath:testPath_ + forEvents:kGTMFileSystemKQueueAllEvents + acrossReplace:YES + target:helper + action:@selector(callbackForQueue:events:)]; + STAssertNotNil(testKQ, nil); + STAssertEqualObjects([testKQ path], testPath_, nil); + GTMFileSystemKQueue *testKQ2 + = [[GTMFileSystemKQueue alloc] initWithPath:testPath_ + forEvents:kGTMFileSystemKQueueAllEvents + acrossReplace:NO + target:helper2 + action:@selector(callbackForQueue:events:)]; + STAssertNotNil(testKQ2, nil); + STAssertEqualObjects([testKQ2 path], testPath_, nil); + + // Write to the file + [testFH writeData:[@"doh!" dataUsingEncoding:NSUnicodeStringEncoding]]; + + // Spin the runloop for a second so that the helper callbacks fire + [self spinForEvents:helper]; + STAssertEquals([helper totals], 1, nil); + STAssertEquals([helper2 totals], 1, nil); + + // Close and delete + [testFH closeFile]; + STAssertTrue([fm removeFileAtPath:testPath_ handler:nil], nil); + + // Recreate + STAssertTrue([fm createFileAtPath:testPath_ contents:nil attributes:nil], nil); + testFH = [NSFileHandle fileHandleForWritingAtPath:testPath_]; + STAssertNotNil(testFH, nil); + [testFH writeData:[@"ha!" dataUsingEncoding:NSUnicodeStringEncoding]]; + + // Spin the runloop for a second so that the helper callbacks fire + [self spinForEvents:helper]; + STAssertEquals([helper totals], 2, nil); + STAssertEquals([helper2 totals], 2, nil); + + // Write to it again + [testFH writeData:[@"continued..." dataUsingEncoding:NSUnicodeStringEncoding]]; + + // Spin the runloop for a second so that the helper callbacks fire + [self spinForEvents:helper]; + STAssertEquals([helper totals], 3, nil); + STAssertEquals([helper2 totals], 2, nil); + + // Close and delete + [testFH closeFile]; + STAssertTrue([fm removeFileAtPath:testPath_ handler:nil], nil); + + // Spin the runloop for a second so that the helper callbacks fire + [self spinForEvents:helper]; + STAssertEquals([helper totals], 4, nil); + STAssertEquals([helper2 totals], 2, nil); + + // Clean up the kqueue + [testKQ release]; + testKQ = nil; + [testKQ2 release]; + testKQ2 = nil; + + STAssertEquals([helper writes], 2, nil); + STAssertEquals([helper deletes], 2, nil); + STAssertEquals([helper renames], 0, nil); + STAssertEquals([helper2 writes], 1, nil); + STAssertEquals([helper2 deletes], 1, nil); + STAssertEquals([helper2 renames], 0, nil); +} + +- (void)testWriteAndRenameAndWrite { + + // One will pass YES to |acrossReplace|, the other will pass NO. + + NSFileManager *fm = [NSFileManager defaultManager]; + GTMFSKQTestHelper *helper = [[[GTMFSKQTestHelper alloc] init] autorelease]; + STAssertNotNil(helper, nil); + GTMFSKQTestHelper *helper2 = [[[GTMFSKQTestHelper alloc] init] autorelease]; + STAssertNotNil(helper2, nil); + + // Create a temp file path + STAssertTrue([fm createFileAtPath:testPath_ contents:nil attributes:nil], nil); + NSFileHandle *testFH = [NSFileHandle fileHandleForWritingAtPath:testPath_]; + STAssertNotNil(testFH, nil); + + // Start monitoring the file + GTMFileSystemKQueue *testKQ + = [[GTMFileSystemKQueue alloc] initWithPath:testPath_ + forEvents:kGTMFileSystemKQueueAllEvents + acrossReplace:YES + target:helper + action:@selector(callbackForQueue:events:)]; + STAssertNotNil(testKQ, nil); + STAssertEqualObjects([testKQ path], testPath_, nil); + GTMFileSystemKQueue *testKQ2 + = [[GTMFileSystemKQueue alloc] initWithPath:testPath_ + forEvents:kGTMFileSystemKQueueAllEvents + acrossReplace:NO + target:helper2 + action:@selector(callbackForQueue:events:)]; + STAssertNotNil(testKQ2, nil); + STAssertEqualObjects([testKQ2 path], testPath_, nil); + + // Write to the file + [testFH writeData:[@"doh!" dataUsingEncoding:NSUnicodeStringEncoding]]; + + // Spin the runloop for a second so that the helper callbacks fire + [self spinForEvents:helper]; + STAssertEquals([helper totals], 1, nil); + STAssertEquals([helper2 totals], 1, nil); + + // Move it and create the file again + STAssertTrue([fm movePath:testPath_ toPath:testPath2_ handler:nil], nil); + STAssertTrue([fm createFileAtPath:testPath_ contents:nil attributes:nil], nil); + NSFileHandle *testFHPrime + = [NSFileHandle fileHandleForWritingAtPath:testPath_]; + STAssertNotNil(testFHPrime, nil); + [testFHPrime writeData:[@"eh?" dataUsingEncoding:NSUnicodeStringEncoding]]; + + // Spin the runloop for a second so that the helper callbacks fire + [self spinForEvents:helper]; + STAssertEquals([helper totals], 2, nil); + STAssertEquals([helper2 totals], 2, nil); + + // Write to the new file + [testFHPrime writeData:[@"continue..." dataUsingEncoding:NSUnicodeStringEncoding]]; + + // Spin the runloop for a second so that the helper callbacks fire + [self spinForEvents:helper]; + STAssertEquals([helper totals], 3, nil); + STAssertEquals([helper2 totals], 2, nil); + + // Write to the old file + [testFH writeData:[@"continue old..." dataUsingEncoding:NSUnicodeStringEncoding]]; + + // Spin the runloop for a second so that the helper callbacks fire + [self spinForEvents:helper]; + STAssertEquals([helper totals], 3, nil); + STAssertEquals([helper2 totals], 3, nil); + + // and now close old + [testFH closeFile]; + STAssertTrue([fm removeFileAtPath:testPath2_ handler:nil], nil); + + // Spin the runloop for a second so that the helper callbacks fire + [self spinForEvents:helper]; + STAssertEquals([helper totals], 3, nil); + STAssertEquals([helper2 totals], 4, nil); + + // and now close new + [testFHPrime closeFile]; + STAssertTrue([fm removeFileAtPath:testPath_ handler:nil], nil); + + // Spin the runloop for a second so that the helper callbacks fire + [self spinForEvents:helper]; + STAssertEquals([helper totals], 4, nil); + STAssertEquals([helper2 totals], 4, nil); + + // Clean up the kqueue + [testKQ release]; + testKQ = nil; + [testKQ2 release]; + testKQ2 = nil; + + STAssertEquals([helper writes], 2, nil); + STAssertEquals([helper deletes], 1, nil); + STAssertEquals([helper renames], 1, nil); + STAssertEquals([helper2 writes], 2, nil); + STAssertEquals([helper2 deletes], 1, nil); + STAssertEquals([helper2 renames], 1, nil); +} + +@end diff --git a/Foundation/GTMHTTPFetcher.h b/Foundation/GTMHTTPFetcher.h index fadd9b0..5abbe1a 100644 --- a/Foundation/GTMHTTPFetcher.h +++ b/Foundation/GTMHTTPFetcher.h @@ -202,7 +202,7 @@ #define _EXTERN #define _INITIALIZE_AS(x) =x #else -#define _EXTERN extern +#define _EXTERN GTM_EXTERN #define _INITIALIZE_AS(x) #endif diff --git a/Foundation/GTMHTTPServer.h b/Foundation/GTMHTTPServer.h index 71522df..0caa149 100644 --- a/Foundation/GTMHTTPServer.h +++ b/Foundation/GTMHTTPServer.h @@ -43,7 +43,7 @@ #define _EXTERN #define _INITIALIZE_AS(x) =x #else -#define _EXTERN extern +#define _EXTERN GTM_EXTERN #define _INITIALIZE_AS(x) #endif diff --git a/Foundation/GTMLightweightProxy.m b/Foundation/GTMLightweightProxy.m index ad7e0a1..39f5f5c 100644 --- a/Foundation/GTMLightweightProxy.m +++ b/Foundation/GTMLightweightProxy.m @@ -48,9 +48,9 @@ // Even though we don't retain this object, we hang it on the lifetime // of the calling threads pool so it's lifetime is safe for at least that // long. - repObject = [[representedObject_ retain] autorelease]; + repObject = [representedObject_ retain]; } - return repObject; + return [repObject autorelease]; } - (void)setRepresentedObject:(id)object { diff --git a/Foundation/GTMLightweightProxyTest.m b/Foundation/GTMLightweightProxyTest.m index ad0961e..e50a9dc 100644 --- a/Foundation/GTMLightweightProxyTest.m +++ b/Foundation/GTMLightweightProxyTest.m @@ -31,13 +31,16 @@ @implementation GTMLightweightProxyTest - (void)testProxy { - id proxy = [[GTMLightweightProxy alloc] initWithRepresentedObject:self]; - STAssertEqualObjects(self, [proxy representedObject], @"Represented object setup failed"); + id proxy + = [[[GTMLightweightProxy alloc] initWithRepresentedObject:self] autorelease]; + STAssertEqualObjects(self, [proxy representedObject], + @"Represented object setup failed"); // Check that it identifies itself as a proxy. STAssertTrue([proxy isProxy], @"Should identify as a proxy"); // Check that it passes class requests on - STAssertTrue([proxy isMemberOfClass:[self class]], @"Should pass class requests through"); + STAssertTrue([proxy isMemberOfClass:[self class]], + @"Should pass class requests through"); // Check that it claims to respond to its selectors. STAssertTrue([proxy respondsToSelector:@selector(initWithRepresentedObject:)], @@ -50,7 +53,8 @@ STAssertTrue([proxy respondsToSelector:@selector(returnYes)], @"Claims not to respond to returnYes"); // ... but not to made up selectors. - STAssertThrows([proxy someMadeUpMethod], @"Calling a bogus method should throw"); + STAssertThrows([proxy someMadeUpMethod], + @"Calling a bogus method should throw"); // Check that callthrough works. STAssertTrue([proxy returnYes], @@ -59,14 +63,18 @@ // Check that nilling out the represented object works. [proxy setRepresentedObject:nil]; STAssertTrue([proxy respondsToSelector:@selector(setRepresentedObject:)], - @"Claims not to respond to setRepresentedObject: after nilling out represented object"); + @"Claims not to respond to setRepresentedObject: after nilling" + @" out represented object"); STAssertFalse([proxy respondsToSelector:@selector(returnYes)], - @"Claims to respond to returnYes after nilling out represented object"); + @"Claims to respond to returnYes after nilling out represented" + @" object"); // Calling through once the represented object is nil should fail silently STAssertNoThrow([proxy returnYes], - @"Calling through without a represented object should fail silently"); + @"Calling through without a represented object should fail" + @" silently"); // ... even when they are made up. - STAssertNoThrow([proxy someMadeUpMethod], @"Calling a bogus method on a nilled proxy should not throw"); + STAssertNoThrow([proxy someMadeUpMethod], + @"Calling a bogus method on a nilled proxy should not throw"); } // Simple method to test calling through the proxy. diff --git a/Foundation/GTMNSAppleScript+HandlerTest.m b/Foundation/GTMNSAppleScript+HandlerTest.m index 02f62e0..16bbd81 100644 --- a/Foundation/GTMNSAppleScript+HandlerTest.m +++ b/Foundation/GTMNSAppleScript+HandlerTest.m @@ -455,15 +455,20 @@ NSAppleScript *script = [[[NSAppleScript alloc] initWithSource:@"david hasselhoff"] autorelease]; [GTMUnitTestDevLog expectPattern:@"Unable to compile script: .*"]; - [GTMUnitTestDevLog expectPattern:@"Unable to coerce script -2147450879"]; + [GTMUnitTestDevLog expectString:@"Unable to coerce script -2147450879"]; NSSet *handlers = [script gtm_handlers]; STAssertEquals([handlers count], (NSUInteger)0, @"Should have no handlers"); [GTMUnitTestDevLog expectPattern:@"Unable to compile script: .*"]; - [GTMUnitTestDevLog expectPattern:@"Unable to coerce script -2147450879"]; + [GTMUnitTestDevLog expectString:@"Unable to coerce script -2147450879"]; NSSet *properties = [script gtm_properties]; STAssertEquals([properties count], (NSUInteger)0, @"Should have no properties"); + [GTMUnitTestDevLog expectPattern:@"Unable to compile script: .*"]; + [GTMUnitTestDevLog expectString:@"Unable to get script info about " + @"open handler -2147450879"]; + STAssertFalse([script gtm_hasOpenDocumentsHandler], + @"Has an opendoc handler?"); } - (void)testScriptDescriptors { diff --git a/Foundation/GTMNSArray+Merge.h b/Foundation/GTMNSArray+Merge.h new file mode 100644 index 0000000..8140f80 --- /dev/null +++ b/Foundation/GTMNSArray+Merge.h @@ -0,0 +1,47 @@ +// +// GTMNSArray+Merge.h +// +// 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 <Foundation/Foundation.h> + +// Extension to NSArray to allow merging of arrays. +// +@interface NSArray (GTMNSArrayMergingAdditions) + +// Merge our array with |newArray| by sorting each array then merging the +// two arrays. If |merger| is provided then call that method on any old +// items that compare as equal to a new item, passing the new item as +// the only argument. If |merger| is not provided, then insert new items +// in front of matching old items. If neither array has any items then +// nil is returned. +// +// The signature of the |merger| is: +// - (id)merge:(id)newItem; +// +// Returns a new, sorted array. +- (NSArray *)gtm_mergeArray:(NSArray *)newArray + mergeSelector:(SEL)merger; + +// Same as above, only |comparer| is used to sort/compare the objects, just like +// -[NSArray sortedArrayUsingSelector]. If |comparer| is nil, nil is returned. +- (NSArray *)gtm_mergeArray:(NSArray *)newArray + compareSelector:(SEL)comparer + mergeSelector:(SEL)merger; + +@end + + diff --git a/Foundation/GTMNSArray+Merge.m b/Foundation/GTMNSArray+Merge.m new file mode 100644 index 0000000..725aa8a --- /dev/null +++ b/Foundation/GTMNSArray+Merge.m @@ -0,0 +1,112 @@ +// +// GTMNSArray+Merge.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 "GTMNSArray+Merge.h" + +#import "GTMDefines.h" + +#if GTM_IPHONE_SDK +#import <objc/message.h> +#else // GTM_IPHONE_SDK +#import <objc/objc-runtime.h> +#endif // GTM_IPHONE_SDK + +@implementation NSArray (GTMNSArrayMergingAdditions) + +- (NSArray *)gtm_mergeArray:(NSArray *)newArray + mergeSelector:(SEL)merger { + return [self gtm_mergeArray:newArray + compareSelector:@selector(compare:) + mergeSelector:merger]; +} + +- (NSArray *)gtm_mergeArray:(NSArray *)newArray + compareSelector:(SEL)comparer + mergeSelector:(SEL)merger { + // must have a compare selector + if (!comparer) return nil; + + // Sort and merge the contents of |self| with |newArray|. + NSArray *sortedMergedArray = nil; + if ([self count] && [newArray count]) { + NSMutableArray *mergingArray = [self mutableCopy]; + [mergingArray sortUsingSelector:comparer]; + NSArray *sortedNewArray + = [newArray sortedArrayUsingSelector:comparer]; + + NSUInteger oldIndex = 0; + NSUInteger oldCount = [mergingArray count]; + id oldItem = (oldIndex < oldCount) + ? [mergingArray objectAtIndex:0] + : nil; + + id newItem = nil; + NSEnumerator *itemEnum = [sortedNewArray objectEnumerator]; + while ((newItem = [itemEnum nextObject])) { + BOOL stillLooking = YES; + while (oldIndex < oldCount && stillLooking) { + // We must take care here, since Intel leaves junk in high bytes of + // return register for predicates that return BOOL. + // For details see: + // http://developer.apple.com/documentation/MacOSX/Conceptual/universal_binary/universal_binary_tips/chapter_5_section_23.html + // and + // http://www.red-sweater.com/blog/320/abusing-objective-c-with-class#comment-83187 + NSComparisonResult result + = ((NSComparisonResult (*)(id, SEL, id))objc_msgSend)(newItem, comparer, oldItem); + if (result == NSOrderedSame && merger) { + // It's a match! + id repItem = [oldItem performSelector:merger + withObject:newItem]; + [mergingArray replaceObjectAtIndex:oldIndex + withObject:repItem]; + ++oldIndex; + oldItem = (oldIndex < oldCount) + ? [mergingArray objectAtIndex:oldIndex] + : nil; + stillLooking = NO; + } else if (result == NSOrderedAscending + || (result == NSOrderedSame && !merger)) { + // This is either a new item and belongs right here, or it's + // a match to an existing item but we're not merging. + [mergingArray insertObject:newItem + atIndex:oldIndex]; + ++oldIndex; + ++oldCount; + stillLooking = NO; + } else { + ++oldIndex; + oldItem = (oldIndex < oldCount) + ? [mergingArray objectAtIndex:oldIndex] + : nil; + } + } + if (stillLooking) { + // Once we get here, the rest of the new items get appended. + [mergingArray addObject:newItem]; + } + } + sortedMergedArray = mergingArray; + } else if ([self count]) { + sortedMergedArray = [self sortedArrayUsingSelector:comparer]; + } else if ([newArray count]) { + sortedMergedArray = [newArray sortedArrayUsingSelector:comparer]; + } + return sortedMergedArray; +} + +@end diff --git a/Foundation/GTMNSArray+MergeTest.m b/Foundation/GTMNSArray+MergeTest.m new file mode 100644 index 0000000..0d8eb81 --- /dev/null +++ b/Foundation/GTMNSArray+MergeTest.m @@ -0,0 +1,219 @@ +// +// GTMNSArray+MergeTest.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 "GTMNSArray+Merge.h" + +@interface GTMNSArray_MergeTest : GTMTestCase +@end + + +@interface NSString (GTMStringMergingTestAdditions) + +- (NSString *)mergeString:(NSString *)stringB; + +@end + + +@implementation GTMNSArray_MergeTest + +- (void)testMergingTwoEmptyArrays { + NSArray *emptyArrayA = [NSArray array]; + NSArray *emptyArrayB = [NSArray array]; + NSArray *mergedArray = [emptyArrayA gtm_mergeArray:emptyArrayB + mergeSelector:nil]; + STAssertNil(mergedArray, + @"merge of two empty arrays with no merger should render nil"); +} + +- (void)testMergingTwoEmptyArraysWithMerger { + NSArray *emptyArrayA = [NSArray array]; + NSArray *emptyArrayB = [NSArray array]; + NSArray *mergedArray + = [emptyArrayA gtm_mergeArray:emptyArrayB + mergeSelector:@selector(mergeString:)]; + STAssertNil(mergedArray, + @"merge of two empty arrays with merger should render nil"); +} + +- (void)testMergingEmptyWithNilArray { + NSArray *emptyArrayA = [NSArray array]; + NSArray *nilArrayB = nil; + NSArray *mergedArray = [emptyArrayA gtm_mergeArray:nilArrayB + mergeSelector:nil]; + STAssertNil(mergedArray, + @"merge of empty with nil array with no merger should render nil"); +} + +- (void)testMergingEmptyWithNilArrayWithMerger { + NSArray *emptyArrayA = [NSArray array]; + NSArray *nilArrayB = nil; + NSArray *mergedArray + = [emptyArrayA gtm_mergeArray:nilArrayB + mergeSelector:@selector(mergeObject:)]; + STAssertNil(mergedArray, + @"merge of empty with nil array with merger should render nil"); +} + +- (void)testMergingTwoOneItemArraysThatDontMatch { + NSArray *arrayA = [NSArray arrayWithObject:@"abc.def"]; + NSArray *arrayB = [NSArray arrayWithObject:@"abc.ghi"]; + NSArray *mergedArray = [arrayA gtm_mergeArray:arrayB + mergeSelector:nil]; + STAssertNotNil(mergedArray, + @"merge of two non empty arrays with no merger should render " + @"an array"); + STAssertEquals([mergedArray count], (NSUInteger)2, + @"merged array should have two items"); + STAssertEqualObjects([mergedArray objectAtIndex:0], @"abc.def", nil); + STAssertEqualObjects([mergedArray objectAtIndex:1], @"abc.ghi", nil); +} + +- (void)testMergingTwoOneItemArraysThatDontMatchWithMerger { + NSArray *arrayA = [NSArray arrayWithObject:@"abc.def"]; + NSArray *arrayB = [NSArray arrayWithObject:@"abc.ghi"]; + NSArray *mergedArray = [arrayA gtm_mergeArray:arrayB + mergeSelector:@selector(mergeString:)]; + STAssertNotNil(mergedArray, + @"merge of two non empty arrays with merger should render " + @"an array"); + STAssertEquals([mergedArray count], (NSUInteger)2, + @"merged array should have two items"); + STAssertEqualObjects([mergedArray objectAtIndex:0], @"abc.def", nil); + STAssertEqualObjects([mergedArray objectAtIndex:1], @"abc.ghi", nil); +} + +- (void)testMergingTwoOneItemArraysThatMatch { + NSArray *arrayA = [NSArray arrayWithObject:@"abc.def"]; + NSArray *arrayB = [NSArray arrayWithObject:@"abc.def"]; + NSArray *mergedArray = [arrayA gtm_mergeArray:arrayB + mergeSelector:nil]; + STAssertNotNil(mergedArray, + @"merge of two matching arrays with no merger should render " + @"an array"); + STAssertEquals([mergedArray count], (NSUInteger)2, + @"merged array with no merger should have two items"); + STAssertEqualObjects([mergedArray objectAtIndex:0], @"abc.def", nil); + STAssertEqualObjects([mergedArray objectAtIndex:1], @"abc.def", nil); +} + +- (void)testMergingTwoOneItemArraysThatMatchWithMerger { + NSArray *arrayA = [NSArray arrayWithObject:@"abc.def"]; + NSArray *arrayB = [NSArray arrayWithObject:@"abc.def"]; + NSArray *mergedArray = [arrayA gtm_mergeArray:arrayB + mergeSelector:@selector(mergeString:)]; + STAssertNotNil(mergedArray, + @"merge of two matching arrays with merger should render " + @"an array"); + STAssertEquals([mergedArray count], (NSUInteger)1, + @"merged array with merger should have one items"); + STAssertEqualObjects([mergedArray objectAtIndex:0], @"abc.def", nil); +} + +- (void)testMergingMultipleItemArray { + NSArray *arrayA = [NSArray arrayWithObjects: + @"Kansas", + @"Arkansas", + @"Wisconson", + @"South Carolina", + nil]; + NSArray *arrayB = [NSArray arrayWithObjects: + @"South Carolina", + @"Quebec", + @"British Columbia", + @"Arkansas", + @"South Hamptom", + nil]; + NSArray *mergedArray = [arrayA gtm_mergeArray:arrayB + mergeSelector:nil]; + STAssertNotNil(mergedArray, + @"merge of two non empty arrays with no merger should render " + @"an array"); + STAssertEquals([mergedArray count], (NSUInteger)9, + @"merged array should have 9 items"); +} + +- (void)testMergingMultipleItemArrayWithMerger { + NSArray *arrayA = [NSArray arrayWithObjects: + @"Kansas", + @"Arkansas", + @"Wisconson", + @"South Carolina", + nil]; + NSArray *arrayB = [NSArray arrayWithObjects: + @"South Carolina", + @"Quebec", + @"British Columbia", + @"Arkansas", + @"South Hamptom", + nil]; + NSArray *mergedArray = [arrayA gtm_mergeArray:arrayB + mergeSelector:@selector(mergeString:)]; + STAssertNotNil(mergedArray, + @"merge of two non empty arrays with merger should render " + @"an array"); + STAssertEquals([mergedArray count], (NSUInteger)7, + @"merged array should have 7 items"); +} + +- (void)testMergeWithEmptyArrays { + NSArray *arrayA = [NSArray arrayWithObjects:@"xyz", @"abc", @"mno", nil]; + NSArray *arrayB = [NSArray array]; + NSArray *expected = [NSArray arrayWithObjects:@"abc", @"mno", @"xyz", nil]; + STAssertNotNil(arrayA, nil); + STAssertNotNil(arrayB, nil); + STAssertNotNil(expected, nil); + NSArray *mergedArray; + + // no merger + mergedArray = [arrayA gtm_mergeArray:arrayB + mergeSelector:nil]; + STAssertNotNil(mergedArray, nil); + STAssertEqualObjects(mergedArray, expected, nil); + + // w/ merger + mergedArray = [arrayA gtm_mergeArray:arrayB + mergeSelector:@selector(mergeString:)]; + STAssertNotNil(mergedArray, nil); + STAssertEqualObjects(mergedArray, expected, nil); + + // no merger and array args reversed + mergedArray = [arrayB gtm_mergeArray:arrayA + mergeSelector:nil]; + STAssertNotNil(mergedArray, nil); + STAssertEqualObjects(mergedArray, expected, nil); + + // w/ merger and array args reversed + mergedArray = [arrayB gtm_mergeArray:arrayA + mergeSelector:@selector(mergeString:)]; + STAssertNotNil(mergedArray, nil); + STAssertEqualObjects(mergedArray, expected, nil); + +} + +@end + + +@implementation NSString (GTMStringMergingTestAdditions) + +- (NSString *)mergeString:(NSString *)stringB { + return stringB; +} + +@end + diff --git a/Foundation/GTMNSFileManager+Carbon.h b/Foundation/GTMNSFileManager+Carbon.h new file mode 100644 index 0000000..86215ea --- /dev/null +++ b/Foundation/GTMNSFileManager+Carbon.h @@ -0,0 +1,60 @@ +// +// GTMNSFileManager+Carbon.h +// +// 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 <Foundation/Foundation.h> + + +// A few useful methods for dealing with paths and carbon structures +@interface NSFileManager (GTMFileManagerCarbonAdditions) + +// Converts a path to an alias +// Args: +// path - the path to convert +// +// Returns: +// An alias wrapped up in an autoreleased NSData. Nil on failure. +// +- (NSData *)gtm_aliasDataForPath:(NSString *)path; + +// Converts an alias to a path +// Args: +// alias - an alias wrapped up in an NSData +// +// Returns: +// The path. Nil on failure. +// +- (NSString *)gtm_pathFromAliasData:(NSData *)alias; + +// Converts a path to an FSRef * +// Args: +// path - the path to convert +// +// Returns: +// An autoreleased FSRef *. Nil on failure. +// +- (FSRef *)gtm_FSRefForPath:(NSString *)path; + +// Converts an FSRef to a path +// Args: +// fsRef - the FSRef to convert +// +// Returns: +// The path. Nil on failure. +// +- (NSString *)gtm_pathFromFSRef:(FSRef *)fsRef; +@end diff --git a/Foundation/GTMNSFileManager+Carbon.m b/Foundation/GTMNSFileManager+Carbon.m new file mode 100644 index 0000000..4f8ba8b --- /dev/null +++ b/Foundation/GTMNSFileManager+Carbon.m @@ -0,0 +1,99 @@ +// +// GTMNSFileManager+Carbon.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 "GTMNSFileManager+Carbon.h" +#import <CoreServices/CoreServices.h> +#import <sys/param.h> +#import "GTMDefines.h" + +@implementation NSFileManager (GTMFileManagerCarbonAdditions) + +- (NSData *)gtm_aliasDataForPath:(NSString *)path { + NSData *data = nil; + FSRef ref; + AliasHandle alias = NULL; + + require_quiet([path length], CantUseParams); + require_noerr(FSPathMakeRef((UInt8 *)[path fileSystemRepresentation], + &ref, NULL), CantMakeRef); + require_noerr(FSNewAlias(NULL, &ref, &alias), CantMakeAlias); + + Size length = GetAliasSize(alias); + data = [NSData dataWithBytes:*alias length:length]; + + DisposeHandle((Handle)alias); + +CantMakeAlias: +CantMakeRef: +CantUseParams: + return data; +} + +- (NSString *)gtm_pathFromAliasData:(NSData *)data { + NSString *path = nil; + require_quiet(data, CantUseParams); + + AliasHandle alias; + const void *bytes = [data bytes]; + NSUInteger length = [data length]; + require_noerr(PtrToHand(bytes, (Handle *)&alias, length), CantMakeHandle); + + FSRef ref; + Boolean wasChanged; + // we don't use a require here because it is quite legitimate for an alias + // resolve to fail. + if (FSResolveAlias(NULL, alias, &ref, &wasChanged) == noErr) { + path = [self gtm_pathFromFSRef:&ref]; + } + DisposeHandle((Handle)alias); +CantMakeHandle: +CantUseParams: + return path; +} + +- (FSRef *)gtm_FSRefForPath:(NSString *)path { + FSRef* fsRef = NULL; + require_quiet([path length], CantUseParams); + NSMutableData *fsRefData = [NSMutableData dataWithLength:sizeof(FSRef)]; + require(fsRefData, CantAllocateFSRef); + fsRef = (FSRef*)[fsRefData mutableBytes]; + Boolean isDir = FALSE; + const UInt8 *filePath = (const UInt8 *)[path fileSystemRepresentation]; + require_noerr_action(FSPathMakeRef(filePath, fsRef, &isDir), + CantMakeRef, fsRef = NULL); +CantMakeRef: +CantAllocateFSRef: +CantUseParams: + return fsRef; +} + +- (NSString *)gtm_pathFromFSRef:(FSRef *)fsRef { + NSString *nsPath = nil; + require_quiet(fsRef, CantUseParams); + + char path[MAXPATHLEN]; + require_noerr(FSRefMakePath(fsRef, (UInt8 *)path, MAXPATHLEN), CantMakePath); + nsPath = [self stringWithFileSystemRepresentation:path length:strlen(path)]; + nsPath = [nsPath stringByStandardizingPath]; + +CantMakePath: +CantUseParams: + return nsPath; +} + +@end diff --git a/Foundation/GTMNSFileManager+CarbonTest.m b/Foundation/GTMNSFileManager+CarbonTest.m new file mode 100644 index 0000000..ae58840 --- /dev/null +++ b/Foundation/GTMNSFileManager+CarbonTest.m @@ -0,0 +1,69 @@ +// +// GTMNSFileManager+CarbonTest.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 "GTMNSFileManager+Carbon.h" +#import "GTMUnitTestDevLog.h" +#import <CoreServices/CoreServices.h> + +@interface GTMNSFileManager_CarbonTest : GTMTestCase +@end + +@implementation GTMNSFileManager_CarbonTest + +- (void)testAliasPathFSRefConversion { + NSString *path = NSHomeDirectory(); + STAssertNotNil(path, nil); + NSFileManager *fileManager = [NSFileManager defaultManager]; + FSRef *fsRef = [fileManager gtm_FSRefForPath:path]; + STAssertNotNULL(fsRef, nil); + AliasHandle alias; + STAssertNoErr(FSNewAlias(nil, fsRef, &alias), nil); + STAssertNotNULL(alias, nil); + NSData *aliasData = [NSData dataWithBytes:*alias + length:GetAliasSize(alias)]; + STAssertNotNil(aliasData, nil); + NSString *path2 = [fileManager gtm_pathFromAliasData:aliasData]; + STAssertEqualObjects(path, path2, nil); + NSData *aliasData2 = [fileManager gtm_aliasDataForPath:path2]; + STAssertNotNil(aliasData2, nil); + NSString *path3 = [fileManager gtm_pathFromAliasData:aliasData2]; + STAssertEqualObjects(path2, path3, nil); + NSString *path4 = [fileManager gtm_pathFromFSRef:fsRef]; + STAssertEqualObjects(path, path4, nil); + + // Failure cases + [GTMUnitTestDevLog expectPattern:@"DebugAssert: " + @"GoogleToolboxForMac: FSPathMakeRef.*"]; + STAssertNULL([fileManager gtm_FSRefForPath:@"/ptah/taht/dosent/esixt/"], + nil); + + STAssertNULL([fileManager gtm_FSRefForPath:@""], nil); + STAssertNULL([fileManager gtm_FSRefForPath:nil], nil); + STAssertNil([fileManager gtm_pathFromFSRef:nil], nil); + STAssertNil([fileManager gtm_pathFromAliasData:nil], nil); + STAssertNil([fileManager gtm_pathFromAliasData:[NSData data]], nil); + + [GTMUnitTestDevLog expectPattern:@"DebugAssert: " + @"GoogleToolboxForMac: FSPathMakeRef.*"]; + STAssertNil([fileManager gtm_aliasDataForPath:@"/ptah/taht/dosent/esixt/"], nil); + STAssertNil([fileManager gtm_aliasDataForPath:@""], nil); + STAssertNil([fileManager gtm_aliasDataForPath:nil], nil); +} + +@end diff --git a/Foundation/GTMObjC2Runtime.h b/Foundation/GTMObjC2Runtime.h index f901d1e..8d62abf 100644 --- a/Foundation/GTMObjC2Runtime.h +++ b/Foundation/GTMObjC2Runtime.h @@ -49,11 +49,8 @@ #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5 #import "objc/Protocol.h" +#import <libkern/OSAtomic.h> -#ifdef __cplusplus -extern "C" { -#endif - OBJC_EXPORT Class object_getClass(id obj); OBJC_EXPORT const char *class_getName(Class cls); OBJC_EXPORT BOOL class_conformsToProtocol(Class cls, Protocol *protocol); @@ -67,7 +64,41 @@ OBJC_EXPORT struct objc_method_description protocol_getMethodDescription(Protoco SEL aSel, BOOL isRequiredMethod, BOOL isInstanceMethod); -#ifdef __cplusplus + +// If building for 10.4 but using the 10.5 SDK, don't include these. +#if MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_5 +// atomics +// On Leopard these are GC aware +// Intentionally did not include the non-barrier versions, because I couldn't +// come up with a case personally where you wouldn't want to use the +// barrier versions. +GTM_INLINE bool OSAtomicCompareAndSwapPtrBarrier(void *predicate, + void *replacement, + volatile void *theValue) { +#if defined(__LP64__) && __LP64__ + return OSAtomicCompareAndSwap64Barrier((int64_t)predicate, + (int64_t)replacement, + (int64_t *)theValue); +#else // defined(__LP64__) && __LP64__ + return OSAtomicCompareAndSwap32Barrier((int32_t)predicate, + (int32_t)replacement, + (int32_t *)theValue); +#endif // defined(__LP64__) && __LP64__ +} + +GTM_INLINE BOOL objc_atomicCompareAndSwapGlobalBarrier(id predicate, + id replacement, + volatile id *objectLocation) { + return OSAtomicCompareAndSwapPtrBarrier(predicate, + replacement, + objectLocation); +} +GTM_INLINE BOOL objc_atomicCompareAndSwapInstanceVariableBarrier(id predicate, + id replacement, + volatile id *objectLocation) { + return OSAtomicCompareAndSwapPtrBarrier(predicate, + replacement, + objectLocation); } #endif diff --git a/Foundation/GTMRegex.h b/Foundation/GTMRegex.h index 3313e0e..c32eee2 100644 --- a/Foundation/GTMRegex.h +++ b/Foundation/GTMRegex.h @@ -60,7 +60,7 @@ typedef NSUInteger GTMRegexOptions; #define _EXTERN #define _INITIALIZE_AS(x) =x #else -#define _EXTERN extern +#define _EXTERN GTM_EXTERN #define _INITIALIZE_AS(x) #endif diff --git a/Foundation/GTMStackTrace.h b/Foundation/GTMStackTrace.h index 9726da5..d8cc642 100644 --- a/Foundation/GTMStackTrace.h +++ b/Foundation/GTMStackTrace.h @@ -51,9 +51,7 @@ struct GTMAddressDescriptor { // #6 0x000025b9 tart () [/Users/me/./StackLog] // -#ifdef GTM_MACOS_SDK // currently not supported on iPhone NSString *GTMStackTrace(void); -#endif #if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 // Returns a string containing a nicely formatted stack trace from the @@ -63,12 +61,15 @@ NSString *GTMStackTrace(void); NSString *GTMStackTraceFromException(NSException *e); #endif +#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5 // Returns an array of program counters from the current thread's stack. // *** You should probably use GTMStackTrace() instead of this function *** // However, if you actually want all the PCs in "void *" form, then this // funtion is more convenient. This will include PCs of GTMStaceTrace and // its inner utility functions that you may want to strip out. // +// You can use +[NSThread callStackReturnAddresses] in 10.5 or later. +// // Args: // outPcs - an array of "void *" pointers to the program counters found on the // current thread's stack. @@ -77,9 +78,8 @@ NSString *GTMStackTraceFromException(NSException *e); // Returns: // The number of program counters actually added to outPcs. // -#ifdef GTM_MACOS_SDK // currently not supported on iPhone NSUInteger GTMGetStackProgramCounters(void *outPcs[], NSUInteger count); -#endif +#endif // MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5 // Returns an array of GTMAddressDescriptors from the current thread's stack. // *** You should probably use GTMStackTrace() instead of this function *** @@ -97,10 +97,8 @@ NSUInteger GTMGetStackProgramCounters(void *outPcs[], NSUInteger count); // Returns: // The number of program counters actually added to outPcs. // -#ifdef GTM_MACOS_SDK // currently not supported on iPhone NSUInteger GTMGetStackAddressDescriptors(struct GTMAddressDescriptor outDescs[], NSUInteger count); -#endif #ifdef __cplusplus } diff --git a/Foundation/GTMStackTrace.m b/Foundation/GTMStackTrace.m index c22c153..0b28743 100644 --- a/Foundation/GTMStackTrace.m +++ b/Foundation/GTMStackTrace.m @@ -22,16 +22,6 @@ #include "GTMStackTrace.h" #include "GTMObjC2Runtime.h" -// Structure representing a small portion of a stack, starting from the saved -// frame pointer, and continuing through the saved program counter. -struct GTMStackFrame { - void *saved_fp; -#if defined (__ppc__) || defined(__ppc64__) - void *padding; -#endif - void *saved_pc; -}; - struct GTMClassDescription { const char *class_name; Method *class_methods; @@ -187,13 +177,26 @@ static NSString *GTMStackTraceFromAddressDescriptors(struct GTMAddressDescriptor #pragma mark Public functions +#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5 +// Before 10.5, we have to do this ourselves. 10.5 adds +// +[NSThread callStackReturnAddresses]. + +// Structure representing a small portion of a stack, starting from the saved +// frame pointer, and continuing through the saved program counter. +struct GTMStackFrame { + void *saved_fp; +#if defined (__ppc__) || defined(__ppc64__) + void *padding; +#endif + void *saved_pc; +}; + // __builtin_frame_address(0) is a gcc builtin that returns a pointer to the // current frame pointer. We then use the frame pointer to walk the stack // picking off program counters and other saved frame pointers. This works // great on i386, but PPC requires a little more work because the PC (or link // register) isn't always stored on the stack. // -#ifdef GTM_MACOS_SDK // currently not supported on iPhone NSUInteger GTMGetStackProgramCounters(void *outPcs[], NSUInteger count) { if (!outPcs || (count < 1)) return 0; @@ -220,41 +223,106 @@ NSUInteger GTMGetStackProgramCounters(void *outPcs[], NSUInteger count) { return level; } +#endif // MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5 NSUInteger GTMGetStackAddressDescriptors(struct GTMAddressDescriptor outDescs[], NSUInteger count) { if (count < 1 || !outDescs) return 0; + NSUInteger result = 0; +#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5 + // Before 10.5, we collect the stack ourselves. + void **pcs = calloc(count, sizeof(void*)); if (!pcs) return 0; NSUInteger newSize = GTMGetStackProgramCounters(pcs, count); - NSUInteger result - = GTMGetStackAddressDescriptorsForAddresses(pcs, outDescs, newSize); + result = GTMGetStackAddressDescriptorsForAddresses(pcs, outDescs, newSize); free(pcs); + +#else // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 + // Use +[NSThread callStackReturnAddresses] + + NSArray *addresses = [NSThread callStackReturnAddresses]; + NSUInteger addrCount = [addresses count]; + if (addrCount) { + void **pcs = calloc(addrCount, sizeof(void*)); + if (pcs) { + void **pcsScanner = pcs; + for (NSNumber *address in addresses) { + NSUInteger addr = [address unsignedIntegerValue]; + *pcsScanner = (void *)addr; + ++pcsScanner; + } + if (count < addrCount) { + addrCount = count; + } + // Fill in the desc structures + result = GTMGetStackAddressDescriptorsForAddresses(pcs, outDescs, addrCount); + } + if (pcs) free(pcs); + } +#endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 + return result; } NSString *GTMStackTrace(void) { + // If we don't have enough frames, return an empty string + NSString *result = @""; + +#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5 + // Before 10.5, we collect the stack ourselves. + // The maximum number of stack frames that we will walk. We limit this so // that super-duper recursive functions (or bugs) don't send us for an // infinite loop. struct GTMAddressDescriptor descs[100]; size_t depth = sizeof(descs) / sizeof(struct GTMAddressDescriptor); depth = GTMGetStackAddressDescriptors(descs, depth); - + // Start at the second item so that GTMStackTrace and it's utility calls (of // which there is currently 1) is not included in the output. const size_t kTracesToStrip = 2; if (depth > kTracesToStrip) { - return GTMStackTraceFromAddressDescriptors(&descs[kTracesToStrip], - (depth - kTracesToStrip)); + result = GTMStackTraceFromAddressDescriptors(&descs[kTracesToStrip], + (depth - kTracesToStrip)); } - // If we didn't have enough frames, return an empty string - return @""; +#else // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 + // Use +[NSThread callStackReturnAddresses] + + NSArray *addresses = [NSThread callStackReturnAddresses]; + NSUInteger count = [addresses count]; + if (count) { + void **pcs = calloc(count, sizeof(void*)); + struct GTMAddressDescriptor *descs + = calloc(count, sizeof(struct GTMAddressDescriptor)); + if (pcs && descs) { + void **pcsScanner = pcs; + for (NSNumber *address in addresses) { + NSUInteger addr = [address unsignedIntegerValue]; + *pcsScanner = (void *)addr; + ++pcsScanner; + } + // Fill in the desc structures + count = GTMGetStackAddressDescriptorsForAddresses(pcs, descs, count); + // Build the trace + // We skip 1 frame because the +[NSThread callStackReturnAddresses] will + // start w/ this frame. + const size_t kTracesToStrip = 1; + if (count > kTracesToStrip) { + result = GTMStackTraceFromAddressDescriptors(&descs[kTracesToStrip], + (count - kTracesToStrip)); + } + } + if (pcs) free(pcs); + if (descs) free(descs); + } +#endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 + + return result; } -#endif // GTM_MACOS_SDK #if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 @@ -287,4 +355,4 @@ NSString *GTMStackTraceFromException(NSException *e) { return trace; } -#endif // MAC_OS_X_VERSION_MIN_REQUIRED +#endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 diff --git a/Foundation/GTMStackTraceTest.m b/Foundation/GTMStackTraceTest.m index 01b02a3..457f3e9 100644 --- a/Foundation/GTMStackTraceTest.m +++ b/Foundation/GTMStackTraceTest.m @@ -25,7 +25,6 @@ @implementation GTMStackTraceTest -#ifdef GTM_MACOS_SDK // currently not supported on iPhone - (void)testStackTraceBasic { NSString *stacktrace = GTMStackTrace(); NSArray *stacklines = [stacktrace componentsSeparatedByString:@"\n"]; @@ -45,7 +44,6 @@ @"First frame should contain #0, stack trace: %@", stacktrace); } -#endif // GTM_MACOS_SDK #if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 @@ -84,7 +82,8 @@ #endif -#ifdef GTM_MACOS_SDK // currently not supported on iPhone +#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5 + - (void)testProgramCountersBasic { void *pcs[10]; NSUInteger depth = 10; @@ -120,6 +119,7 @@ void *current_pc = __builtin_return_address(0); STAssertEquals(pcs2[1], current_pc, @"pcs[1] should equal the current PC"); } -#endif // GTM_MACOS_SDK + +#endif // MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5 @end diff --git a/Foundation/GTMSystemVersion.h b/Foundation/GTMSystemVersion.h index 959d2af..07580a7 100644 --- a/Foundation/GTMSystemVersion.h +++ b/Foundation/GTMSystemVersion.h @@ -72,14 +72,15 @@ // Architecture Strings // TODO: Should probably break iPhone up into iPhone_ARM and iPhone_Simulator // but haven't found a need yet. -extern NSString *const kGTMArch_iPhone; -extern NSString *const kGTMArch_ppc; -extern NSString *const kGTMArch_ppc64; -extern NSString *const kGTMArch_x86_64; -extern NSString *const kGTMArch_i386; +GTM_EXTERN NSString *const kGTMArch_iPhone; +GTM_EXTERN NSString *const kGTMArch_ppc; +GTM_EXTERN NSString *const kGTMArch_ppc64; +GTM_EXTERN NSString *const kGTMArch_x86_64; +GTM_EXTERN NSString *const kGTMArch_i386; // System Build Number constants -extern NSString *const kGTMSystemBuild10_5_5; -extern NSString *const kGTMSystemBuild10_6_0_WWDC; +GTM_EXTERN NSString *const kGTMSystemBuild10_5_5; +GTM_EXTERN NSString *const kGTMSystemBuild10_6_0_WWDC; +GTM_EXTERN NSString *const kGTMSystemBuild10_6_0_10A190; diff --git a/Foundation/GTMSystemVersion.m b/Foundation/GTMSystemVersion.m index 868f6b8..f9c7861 100644 --- a/Foundation/GTMSystemVersion.m +++ b/Foundation/GTMSystemVersion.m @@ -37,6 +37,7 @@ static NSString *const kSystemVersionPlistPath = @"/System/Library/CoreServices/ NSString *const kGTMSystemBuild10_5_5 = @"9F33"; NSString *const kGTMSystemBuild10_6_0_WWDC = @"10A96"; +NSString *const kGTMSystemBuild10_6_0_10A190 = @"10A190"; @implementation GTMSystemVersion + (void)initialize { diff --git a/GTM.xcodeproj/project.pbxproj b/GTM.xcodeproj/project.pbxproj index 745f920..8b241aa 100644 --- a/GTM.xcodeproj/project.pbxproj +++ b/GTM.xcodeproj/project.pbxproj @@ -42,6 +42,9 @@ 33C374380DD8D44800E97817 /* GTMNSDictionary+URLArguments.h in Headers */ = {isa = PBXBuildFile; fileRef = 33C374360DD8D44800E97817 /* GTMNSDictionary+URLArguments.h */; settings = {ATTRIBUTES = (Public, ); }; }; 33C374390DD8D44800E97817 /* GTMNSDictionary+URLArguments.m in Sources */ = {isa = PBXBuildFile; fileRef = 33C374370DD8D44800E97817 /* GTMNSDictionary+URLArguments.m */; }; 33C3745F0DD8D85B00E97817 /* GTMNSDictionary+URLArgumentsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 33C3745E0DD8D85B00E97817 /* GTMNSDictionary+URLArgumentsTest.m */; }; + 629445400EDDF647009295EA /* GTMNSArray+Merge.h in Headers */ = {isa = PBXBuildFile; fileRef = 6294453E0EDDF647009295EA /* GTMNSArray+Merge.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 629445410EDDF647009295EA /* GTMNSArray+Merge.m in Sources */ = {isa = PBXBuildFile; fileRef = 6294453F0EDDF647009295EA /* GTMNSArray+Merge.m */; }; + 6294454C0EDDF89A009295EA /* GTMNSArray+MergeTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6294454B0EDDF89A009295EA /* GTMNSArray+MergeTest.m */; }; 7F3EB38E0E5E09C700A7A75E /* GTMNSImage+Scaling.h in Headers */ = {isa = PBXBuildFile; fileRef = 7F3EB38C0E5E09C700A7A75E /* GTMNSImage+Scaling.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7F3EB38F0E5E09C700A7A75E /* GTMNSImage+Scaling.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F3EB38D0E5E09C700A7A75E /* GTMNSImage+Scaling.m */; }; 7F3EB3940E5E0A2100A7A75E /* GTMNSImage+ScalingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F3EB3930E5E0A2100A7A75E /* GTMNSImage+ScalingTest.m */; }; @@ -62,13 +65,16 @@ 8B3344210DBF7A36009FD32C /* GTMNSAppleScript+HandlerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B3344170DBF7A36009FD32C /* GTMNSAppleScript+HandlerTest.m */; }; 8B3344230DBF7A36009FD32C /* GTMNSAppleEventDescriptor+HandlerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B33441A0DBF7A36009FD32C /* GTMNSAppleEventDescriptor+HandlerTest.m */; }; 8B3344250DBF7A36009FD32C /* GTMNSAppleEventDescriptor+FoundationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B33441D0DBF7A36009FD32C /* GTMNSAppleEventDescriptor+FoundationTest.m */; }; - 8B33455E0DBF8844009FD32C /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F42E09AD0D19A62F00D5DDE0 /* Carbon.framework */; }; 8B3345890DBF8A55009FD32C /* GTMNSAppleEvent+HandlerTest.applescript in AppleScript */ = {isa = PBXBuildFile; fileRef = 8B3344200DBF7A36009FD32C /* GTMNSAppleEvent+HandlerTest.applescript */; settings = {ATTRIBUTES = (Debug, ); }; }; 8B3590160E8190FA0041E21C /* GTMTestTimer.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B3590150E8190FA0041E21C /* GTMTestTimer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8B35901B0E8191750041E21C /* GTMTestTimerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B35901A0E8191750041E21C /* GTMTestTimerTest.m */; }; 8B3AA9F10E033E23007E31B5 /* GTMValidatingContainers.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B3AA9EF0E033E23007E31B5 /* GTMValidatingContainers.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8B3AA9F20E033E23007E31B5 /* GTMValidatingContainers.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B3AA9F00E033E23007E31B5 /* GTMValidatingContainers.m */; }; 8B3AA9F80E033E5F007E31B5 /* GTMValidatingContainersTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B3AA9F70E033E5F007E31B5 /* GTMValidatingContainersTest.m */; }; + 8B3E292E0EEB53F8000681D8 /* GTMCarbonEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B3E292A0EEB53F3000681D8 /* GTMCarbonEvent.m */; }; + 8B3E292F0EEB53F8000681D8 /* GTMCarbonEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B3E292B0EEB53F3000681D8 /* GTMCarbonEvent.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8B3E2A3F0EEB564D000681D8 /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F42E09AD0D19A62F00D5DDE0 /* Carbon.framework */; }; + 8B3E2A410EEB565B000681D8 /* GTMUnitTestingUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B45A2680DA498A0001148C5 /* GTMUnitTestingUtilities.m */; }; 8B45A03A0DA46A2A001148C5 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0867D69BFE84028FC02AAC07 /* Foundation.framework */; }; 8B45A0B80DA46A2F001148C5 /* SenTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F42E089B0D199B1800D5DDE0 /* SenTestingKit.framework */; }; 8B45A0D50DA46A57001148C5 /* GTMNSObject+UnitTesting.m in Sources */ = {isa = PBXBuildFile; fileRef = F48FE29C0D198D36009257D2 /* GTMNSObject+UnitTesting.m */; }; @@ -120,6 +126,14 @@ 8B7DCE1B0DFF39850017E983 /* GTMSenTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B7DCE180DFF39850017E983 /* GTMSenTestCase.m */; }; 8B7DCEF10E002C210017E983 /* GTMDevLogUnitTestingBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B7DCBE10DFF18720017E983 /* GTMDevLogUnitTestingBridge.m */; }; 8B7E35750E048E2D00EF70C8 /* GTMHTTPFetcherTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F435E3930DC8CAAF0069CDE8 /* GTMHTTPFetcherTest.m */; }; + 8B8B10290EEB8B1600E543D0 /* GTMHotKeyTextFieldTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F4A420EE0EDDF8E000397A11 /* GTMHotKeyTextFieldTest.m */; }; + 8B8B102A0EEB8B2900E543D0 /* GTMCarbonEventTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B3E29290EEB53F3000681D8 /* GTMCarbonEventTest.m */; }; + 8B8B10F90EEB8B9E00E543D0 /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F42E09AD0D19A62F00D5DDE0 /* Carbon.framework */; }; + 8B8B10FD0EEB8BC300E543D0 /* GTMUnitTestingUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B45A2680DA498A0001148C5 /* GTMUnitTestingUtilities.m */; }; + 8B8B11000EEB8CD000E543D0 /* GTMGetURLHandlerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B8B10FF0EEB8CD000E543D0 /* GTMGetURLHandlerTest.m */; }; + 8B8EC87D0EF17C270044D13F /* GTMNSFileManager+Carbon.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B8EC87B0EF17C270044D13F /* GTMNSFileManager+Carbon.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8B8EC87E0EF17C270044D13F /* GTMNSFileManager+Carbon.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B8EC87C0EF17C270044D13F /* GTMNSFileManager+Carbon.m */; }; + 8B8EC8800EF17C2F0044D13F /* GTMNSFileManager+CarbonTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B8EC87F0EF17C2F0044D13F /* GTMNSFileManager+CarbonTest.m */; }; 8BC045C20DAE899100C2D1CA /* GTMGeometryUtilsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F48FE2800D198D0E009257D2 /* GTMGeometryUtilsTest.m */; }; 8BC046B90DAE8C4B00C2D1CA /* ApplicationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8BC046B80DAE8C4B00C2D1CA /* ApplicationServices.framework */; }; 8BC04CD80DB003D800C2D1CA /* GTMMethodCheck.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B6F31F40DA3489B0052CA40 /* GTMMethodCheck.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -219,6 +233,11 @@ F47F1D300D4914AD00925B8F /* GTMCalculatedRange.h in Headers */ = {isa = PBXBuildFile; fileRef = F47F1D2D0D4914AD00925B8F /* GTMCalculatedRange.h */; settings = {ATTRIBUTES = (Public, ); }; }; F47F1D310D4914AD00925B8F /* GTMCalculatedRange.m in Sources */ = {isa = PBXBuildFile; fileRef = F47F1D2E0D4914AD00925B8F /* GTMCalculatedRange.m */; }; F47F1D350D4914B600925B8F /* GTMCalculatedRangeTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F47F1D2F0D4914AD00925B8F /* GTMCalculatedRangeTest.m */; }; + F49FA8440EEF2AB700077669 /* GTMFileSystemKQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = F49FA8420EEF2AB700077669 /* GTMFileSystemKQueue.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F49FA8450EEF2AB700077669 /* GTMFileSystemKQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = F49FA8430EEF2AB700077669 /* GTMFileSystemKQueue.m */; }; + F49FA88B0EEF303D00077669 /* GTMFileSystemKQueueTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F49FA88A0EEF303D00077669 /* GTMFileSystemKQueueTest.m */; }; + F4A420EF0EDDF8E000397A11 /* GTMHotKeyTextField.h in Headers */ = {isa = PBXBuildFile; fileRef = F4A420EC0EDDF8E000397A11 /* GTMHotKeyTextField.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F4A420F00EDDF8E000397A11 /* GTMHotKeyTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = F4A420ED0EDDF8E000397A11 /* GTMHotKeyTextField.m */; }; F4BC1C880DDDD45D00108B7D /* GTMHTTPServer.h in Headers */ = {isa = PBXBuildFile; fileRef = F4BC1C860DDDD45D00108B7D /* GTMHTTPServer.h */; settings = {ATTRIBUTES = (Public, ); }; }; F4BC1C890DDDD45D00108B7D /* GTMHTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = F4BC1C870DDDD45D00108B7D /* GTMHTTPServer.m */; }; F4BC1E8D0DE1FC4A00108B7D /* GTMHTTPServerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F4BC1E8C0DE1FC4A00108B7D /* GTMHTTPServerTest.m */; }; @@ -309,6 +328,9 @@ 33C374360DD8D44800E97817 /* GTMNSDictionary+URLArguments.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GTMNSDictionary+URLArguments.h"; sourceTree = "<group>"; }; 33C374370DD8D44800E97817 /* GTMNSDictionary+URLArguments.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSDictionary+URLArguments.m"; sourceTree = "<group>"; }; 33C3745E0DD8D85B00E97817 /* GTMNSDictionary+URLArgumentsTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSDictionary+URLArgumentsTest.m"; sourceTree = "<group>"; }; + 6294453E0EDDF647009295EA /* GTMNSArray+Merge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GTMNSArray+Merge.h"; sourceTree = "<group>"; }; + 6294453F0EDDF647009295EA /* GTMNSArray+Merge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSArray+Merge.m"; sourceTree = "<group>"; }; + 6294454B0EDDF89A009295EA /* GTMNSArray+MergeTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSArray+MergeTest.m"; sourceTree = "<group>"; }; 7F3EB38C0E5E09C700A7A75E /* GTMNSImage+Scaling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GTMNSImage+Scaling.h"; sourceTree = "<group>"; }; 7F3EB38D0E5E09C700A7A75E /* GTMNSImage+Scaling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSImage+Scaling.m"; sourceTree = "<group>"; }; 7F3EB3930E5E0A2100A7A75E /* GTMNSImage+ScalingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSImage+ScalingTest.m"; sourceTree = "<group>"; }; @@ -345,6 +367,9 @@ 8B3AA9EF0E033E23007E31B5 /* GTMValidatingContainers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMValidatingContainers.h; sourceTree = "<group>"; }; 8B3AA9F00E033E23007E31B5 /* GTMValidatingContainers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMValidatingContainers.m; sourceTree = "<group>"; }; 8B3AA9F70E033E5F007E31B5 /* GTMValidatingContainersTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMValidatingContainersTest.m; sourceTree = "<group>"; }; + 8B3E29290EEB53F3000681D8 /* GTMCarbonEventTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMCarbonEventTest.m; sourceTree = "<group>"; }; + 8B3E292A0EEB53F3000681D8 /* GTMCarbonEvent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMCarbonEvent.m; sourceTree = "<group>"; }; + 8B3E292B0EEB53F3000681D8 /* GTMCarbonEvent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMCarbonEvent.h; sourceTree = "<group>"; }; 8B45A0280DA4696C001148C5 /* UnitTest - UnitTesting.octest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "UnitTest - UnitTesting.octest"; sourceTree = BUILT_PRODUCTS_DIR; }; 8B45A1990DA46AAA001148C5 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = /System/Library/Frameworks/QuartzCore.framework; sourceTree = "<absolute>"; }; 8B45A2670DA498A0001148C5 /* GTMUnitTestingUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMUnitTestingUtilities.h; sourceTree = "<group>"; }; @@ -381,6 +406,10 @@ 8B7DCBEC0DFF1A4F0017E983 /* GTMUnitTestDevLog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMUnitTestDevLog.m; sourceTree = "<group>"; }; 8B7DCBF00DFF1A610017E983 /* GTMUnitTestDevLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMUnitTestDevLog.h; sourceTree = "<group>"; }; 8B7DCE180DFF39850017E983 /* GTMSenTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMSenTestCase.m; sourceTree = "<group>"; }; + 8B8B10FF0EEB8CD000E543D0 /* GTMGetURLHandlerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMGetURLHandlerTest.m; sourceTree = "<group>"; }; + 8B8EC87B0EF17C270044D13F /* GTMNSFileManager+Carbon.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GTMNSFileManager+Carbon.h"; sourceTree = "<group>"; }; + 8B8EC87C0EF17C270044D13F /* GTMNSFileManager+Carbon.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSFileManager+Carbon.m"; sourceTree = "<group>"; }; + 8B8EC87F0EF17C2F0044D13F /* GTMNSFileManager+CarbonTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSFileManager+CarbonTest.m"; sourceTree = "<group>"; }; 8BC046B80DAE8C4B00C2D1CA /* ApplicationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ApplicationServices.framework; path = /System/Library/Frameworks/ApplicationServices.framework; sourceTree = "<absolute>"; }; 8BC04D140DB0061300C2D1CA /* RunMacOSUnitTests.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = RunMacOSUnitTests.sh; sourceTree = "<group>"; }; 8BE2836B0DED0F130035B3F8 /* GTMFourCharCode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMFourCharCode.m; sourceTree = "<group>"; }; @@ -488,6 +517,12 @@ F48FE29C0D198D36009257D2 /* GTMNSObject+UnitTesting.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSObject+UnitTesting.m"; sourceTree = "<group>"; }; F48FE29F0D198D36009257D2 /* GTMSenTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMSenTestCase.h; sourceTree = "<group>"; }; F48FE2E10D198E4C009257D2 /* GTMSystemVersionTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMSystemVersionTest.m; sourceTree = "<group>"; }; + F49FA8420EEF2AB700077669 /* GTMFileSystemKQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMFileSystemKQueue.h; sourceTree = "<group>"; }; + F49FA8430EEF2AB700077669 /* GTMFileSystemKQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMFileSystemKQueue.m; sourceTree = "<group>"; }; + F49FA88A0EEF303D00077669 /* GTMFileSystemKQueueTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMFileSystemKQueueTest.m; sourceTree = "<group>"; }; + F4A420EC0EDDF8E000397A11 /* GTMHotKeyTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMHotKeyTextField.h; sourceTree = "<group>"; }; + F4A420ED0EDDF8E000397A11 /* GTMHotKeyTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMHotKeyTextField.m; sourceTree = "<group>"; }; + F4A420EE0EDDF8E000397A11 /* GTMHotKeyTextFieldTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMHotKeyTextFieldTest.m; sourceTree = "<group>"; }; F4BC1C860DDDD45D00108B7D /* GTMHTTPServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMHTTPServer.h; sourceTree = "<group>"; }; F4BC1C870DDDD45D00108B7D /* GTMHTTPServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMHTTPServer.m; sourceTree = "<group>"; }; F4BC1E8C0DE1FC4A00108B7D /* GTMHTTPServerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMHTTPServerTest.m; sourceTree = "<group>"; }; @@ -525,6 +560,7 @@ 8B45A03A0DA46A2A001148C5 /* Foundation.framework in Frameworks */, 8B45A19A0DA46AAA001148C5 /* QuartzCore.framework in Frameworks */, 8B7DCBA50DFF0EFF0017E983 /* GoogleToolboxForMac.framework in Frameworks */, + 8B3E2A3F0EEB564D000681D8 /* Carbon.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -544,7 +580,6 @@ F42E08800D199AB500D5DDE0 /* GoogleToolboxForMac.framework in Frameworks */, F42E089D0D199B1800D5DDE0 /* SenTestingKit.framework in Frameworks */, 8BC046B90DAE8C4B00C2D1CA /* ApplicationServices.framework in Frameworks */, - 8B33455E0DBF8844009FD32C /* Carbon.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -566,6 +601,7 @@ F42E08610D199A2B00D5DDE0 /* Cocoa.framework in Frameworks */, F42E087F0D199AB400D5DDE0 /* GoogleToolboxForMac.framework in Frameworks */, F42E089C0D199B1800D5DDE0 /* SenTestingKit.framework in Frameworks */, + 8B8B10F90EEB8B9E00E543D0 /* Carbon.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -702,9 +738,16 @@ F48FE26F0D198CBA009257D2 /* AppKit */ = { isa = PBXGroup; children = ( + 8B3E292B0EEB53F3000681D8 /* GTMCarbonEvent.h */, + 8B3E292A0EEB53F3000681D8 /* GTMCarbonEvent.m */, + 8B3E29290EEB53F3000681D8 /* GTMCarbonEventTest.m */, F48FE27C0D198D0E009257D2 /* GTMDelegatingTableColumn.h */, F48FE27D0D198D0E009257D2 /* GTMDelegatingTableColumn.m */, 8B58E9940E547EB000A0E02E /* GTMGetURLHandler.m */, + 8B8B10FF0EEB8CD000E543D0 /* GTMGetURLHandlerTest.m */, + F4A420EC0EDDF8E000397A11 /* GTMHotKeyTextField.h */, + F4A420ED0EDDF8E000397A11 /* GTMHotKeyTextField.m */, + F4A420EE0EDDF8E000397A11 /* GTMHotKeyTextFieldTest.m */, 8B1801A10E2533D500280961 /* GTMLargeTypeWindow.h */, 8B1801A00E2533D500280961 /* GTMLargeTypeWindow.m */, 8B1801A40E2533DB00280961 /* GTMLargeTypeWindowTest.m */, @@ -751,6 +794,9 @@ 8B1B49160E5F8E2100A08972 /* GTMExceptionalInlines.h */, 8B1B49170E5F8E2100A08972 /* GTMExceptionalInlines.m */, 8B1B491B0E5F904C00A08972 /* GTMExceptionalInlinesTest.m */, + F49FA8420EEF2AB700077669 /* GTMFileSystemKQueue.h */, + F49FA8430EEF2AB700077669 /* GTMFileSystemKQueue.m */, + F49FA88A0EEF303D00077669 /* GTMFileSystemKQueueTest.m */, 8BE2836B0DED0F130035B3F8 /* GTMFourCharCode.m */, 8BE2836C0DED0F130035B3F8 /* GTMFourCharCodeTest.m */, 8BE2836D0DED0F130035B3F8 /* GTMFourCharCode.h */, @@ -779,12 +825,18 @@ F95803F60E2FB0760049A088 /* GTMLoggerRingBufferWriter.h */, F95803F70E2FB0760049A088 /* GTMLoggerRingBufferWriter.m */, F95803F80E2FB0760049A088 /* GTMLoggerRingBufferWriterTest.m */, + 6294453E0EDDF647009295EA /* GTMNSArray+Merge.h */, + 6294453F0EDDF647009295EA /* GTMNSArray+Merge.m */, + 6294454B0EDDF89A009295EA /* GTMNSArray+MergeTest.m */, 33C374360DD8D44800E97817 /* GTMNSDictionary+URLArguments.h */, 33C374370DD8D44800E97817 /* GTMNSDictionary+URLArguments.m */, 33C3745E0DD8D85B00E97817 /* GTMNSDictionary+URLArgumentsTest.m */, F43E4DD60D4E56320041161F /* GTMNSEnumerator+Filter.h */, F43E4DD70D4E56320041161F /* GTMNSEnumerator+Filter.m */, F43E4DD80D4E56320041161F /* GTMNSEnumerator+FilterTest.m */, + 8B8EC87B0EF17C270044D13F /* GTMNSFileManager+Carbon.h */, + 8B8EC87C0EF17C270044D13F /* GTMNSFileManager+Carbon.m */, + 8B8EC87F0EF17C2F0044D13F /* GTMNSFileManager+CarbonTest.m */, F413908C0D75F63C00F72B31 /* GTMNSFileManager+Path.h */, F413908D0D75F63C00F72B31 /* GTMNSFileManager+Path.m */, F413908E0D75F63C00F72B31 /* GTMNSFileManager+PathTest.m */, @@ -968,6 +1020,11 @@ 8B3590160E8190FA0041E21C /* GTMTestTimer.h in Headers */, 8B6F4B630E8856CA00425D9F /* GTMDebugThreadValidation.h in Headers */, F41711350ECDFBD500B9B276 /* GTMLightweightProxy.h in Headers */, + 629445400EDDF647009295EA /* GTMNSArray+Merge.h in Headers */, + F4A420EF0EDDF8E000397A11 /* GTMHotKeyTextField.h in Headers */, + 8B3E292F0EEB53F8000681D8 /* GTMCarbonEvent.h in Headers */, + F49FA8440EEF2AB700077669 /* GTMFileSystemKQueue.h in Headers */, + 8B8EC87D0EF17C270044D13F /* GTMNSFileManager+Carbon.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1247,6 +1304,7 @@ 8B7DCBEF0DFF1A4F0017E983 /* GTMUnitTestDevLog.m in Sources */, 8B7DCE1B0DFF39850017E983 /* GTMSenTestCase.m in Sources */, 8B35901B0E8191750041E21C /* GTMTestTimerTest.m in Sources */, + 8B3E2A410EEB565B000681D8 /* GTMUnitTestingUtilities.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1305,6 +1363,9 @@ 8B1B49260E5F97C800A08972 /* GTMExceptionalInlinesTest.m in Sources */, 8BE839AA0E8AF72E00C611B0 /* GTMDebugThreadValidationTest.m in Sources */, F41711380ECDFBE100B9B276 /* GTMLightweightProxyTest.m in Sources */, + 6294454C0EDDF89A009295EA /* GTMNSArray+MergeTest.m in Sources */, + F49FA88B0EEF303D00077669 /* GTMFileSystemKQueueTest.m in Sources */, + 8B8EC8800EF17C2F0044D13F /* GTMNSFileManager+CarbonTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1356,6 +1417,11 @@ 7F3EB38F0E5E09C700A7A75E /* GTMNSImage+Scaling.m in Sources */, 8B6F4B640E8856CA00425D9F /* GTMDebugThreadValidation.m in Sources */, F41711360ECDFBD500B9B276 /* GTMLightweightProxy.m in Sources */, + 629445410EDDF647009295EA /* GTMNSArray+Merge.m in Sources */, + F4A420F00EDDF8E000397A11 /* GTMHotKeyTextField.m in Sources */, + 8B3E292E0EEB53F8000681D8 /* GTMCarbonEvent.m in Sources */, + F49FA8450EEF2AB700077669 /* GTMFileSystemKQueue.m in Sources */, + 8B8EC87E0EF17C270044D13F /* GTMNSFileManager+Carbon.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1375,6 +1441,10 @@ 8B7DCE190DFF39850017E983 /* GTMSenTestCase.m in Sources */, 7F3EB3940E5E0A2100A7A75E /* GTMNSImage+ScalingTest.m in Sources */, 7F3EB5870E5F0CBB00A7A75E /* GTMLargeTypeWindowTest.m in Sources */, + 8B8B10290EEB8B1600E543D0 /* GTMHotKeyTextFieldTest.m in Sources */, + 8B8B102A0EEB8B2900E543D0 /* GTMCarbonEventTest.m in Sources */, + 8B8B10FD0EEB8BC300E543D0 /* GTMUnitTestingUtilities.m in Sources */, + 8B8B11000EEB8CD000E543D0 /* GTMGetURLHandlerTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1441,6 +1511,11 @@ buildSettings = { GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = GTM_Prefix.pch; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COMPONENT_SIGNATURE=\"\\'GTM \\'\"", + "kComponentSignatureString=\"\\\"GoogleToolboxForMac\\\"\"", + ); GCC_WARN_SHADOW = YES; GTM_EXTRA_WARNING_OVERRIDE_CFLAGS = "-Wno-unused-parameter"; }; @@ -1452,6 +1527,11 @@ buildSettings = { GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = GTM_Prefix.pch; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COMPONENT_SIGNATURE=\"\\'GTM \\'\"", + "kComponentSignatureString=\"\\\"GoogleToolboxForMac\\\"\"", + ); GCC_WARN_SHADOW = YES; GTM_EXTRA_WARNING_OVERRIDE_CFLAGS = "-Wno-unused-parameter"; }; @@ -1463,6 +1543,11 @@ buildSettings = { GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = GTM_Prefix.pch; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COMPONENT_SIGNATURE=\"\\'GTM \\'\"", + "kComponentSignatureString=\"\\\"GoogleToolboxForMac\\\"\"", + ); GCC_WARN_SHADOW = YES; GTM_EXTRA_WARNING_OVERRIDE_CFLAGS = "-Wno-unused-parameter"; }; @@ -1499,6 +1584,7 @@ GCC_ENABLE_OBJC_GC = supported; INFOPLIST_FILE = "UnitTest-Info.plist"; PRODUCT_NAME = "UnitTest - AppKit"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GTMUIUnitTestingHarness.app/Contents/MacOS/GTMUIUnitTestingHarness"; }; name = "SnowLeopardOrLater-Debug"; }; @@ -1563,6 +1649,11 @@ GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = GTM_Prefix.pch; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COMPONENT_SIGNATURE=\"\\'GTM \\'\"", + "kComponentSignatureString=\"\\\"GoogleToolboxForMac\\\"\"", + ); GCC_WARN_SHADOW = YES; GTM_EXTRA_WARNING_OVERRIDE_CFLAGS = "-Wno-unused-parameter"; OTHER_LDFLAGS = "-lgcov"; @@ -1575,6 +1666,11 @@ buildSettings = { GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = GTM_Prefix.pch; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COMPONENT_SIGNATURE=\"\\'GTM \\'\"", + "kComponentSignatureString=\"\\\"GoogleToolboxForMac\\\"\"", + ); GCC_WARN_SHADOW = YES; GTM_EXTRA_WARNING_OVERRIDE_CFLAGS = "-Wno-unused-parameter"; }; @@ -1631,6 +1727,7 @@ GCC_ENABLE_OBJC_GC = supported; INFOPLIST_FILE = "UnitTest-Info.plist"; PRODUCT_NAME = "UnitTest - AppKit"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GTMUIUnitTestingHarness.app/Contents/MacOS/GTMUIUnitTestingHarness"; }; name = "SnowLeopardOrLater-Debug-gcov"; }; @@ -1645,6 +1742,7 @@ GCC_ENABLE_OBJC_GC = supported; INFOPLIST_FILE = "UnitTest-Info.plist"; PRODUCT_NAME = "UnitTest - AppKit"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GTMUIUnitTestingHarness.app/Contents/MacOS/GTMUIUnitTestingHarness"; }; name = "SnowLeopardOrLater-Release"; }; @@ -1980,6 +2078,11 @@ GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = GTM_Prefix.pch; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COMPONENT_SIGNATURE=\"\\'GTM \\'\"", + "kComponentSignatureString=\"\\\"GoogleToolboxForMac\\\"\"", + ); GCC_WARN_SHADOW = YES; GTM_EXTRA_WARNING_OVERRIDE_CFLAGS = "-Wno-unused-parameter"; LIBRARY_SEARCH_PATHS = ( @@ -2020,6 +2123,7 @@ ); INFOPLIST_FILE = "UnitTest-Info.plist"; PRODUCT_NAME = "UnitTest - AppKit"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GTMUIUnitTestingHarness.app/Contents/MacOS/GTMUIUnitTestingHarness"; }; name = "TigerOrLater-Debug-gcov"; }; @@ -2047,6 +2151,7 @@ ); INFOPLIST_FILE = "UnitTest-Info.plist"; PRODUCT_NAME = "UnitTest - AppKit"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GTMUIUnitTestingHarness.app/Contents/MacOS/GTMUIUnitTestingHarness"; }; name = "TigerOrLater-Debug"; }; @@ -2060,6 +2165,7 @@ ); INFOPLIST_FILE = "UnitTest-Info.plist"; PRODUCT_NAME = "UnitTest - AppKit"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GTMUIUnitTestingHarness.app/Contents/MacOS/GTMUIUnitTestingHarness"; }; name = "TigerOrLater-Release"; }; @@ -2074,6 +2180,11 @@ ); GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = GTM_Prefix.pch; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COMPONENT_SIGNATURE=\"\\'GTM \\'\"", + "kComponentSignatureString=\"\\\"GoogleToolboxForMac\\\"\"", + ); GCC_WARN_SHADOW = YES; GTM_EXTRA_WARNING_OVERRIDE_CFLAGS = "-Wno-unused-parameter"; }; @@ -2110,6 +2221,7 @@ GCC_ENABLE_OBJC_GC = supported; INFOPLIST_FILE = "UnitTest-Info.plist"; PRODUCT_NAME = "UnitTest - AppKit"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GTMUIUnitTestingHarness.app/Contents/MacOS/GTMUIUnitTestingHarness"; }; name = "LeopardOrLater-Debug"; }; @@ -2165,6 +2277,11 @@ GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = GTM_Prefix.pch; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COMPONENT_SIGNATURE=\"\\'GTM \\'\"", + "kComponentSignatureString=\"\\\"GoogleToolboxForMac\\\"\"", + ); GCC_WARN_SHADOW = YES; GTM_EXTRA_WARNING_OVERRIDE_CFLAGS = "-Wno-unused-parameter"; OTHER_LDFLAGS = "-lgcov"; @@ -2202,6 +2319,7 @@ GCC_ENABLE_OBJC_GC = supported; INFOPLIST_FILE = "UnitTest-Info.plist"; PRODUCT_NAME = "UnitTest - AppKit"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GTMUIUnitTestingHarness.app/Contents/MacOS/GTMUIUnitTestingHarness"; }; name = "LeopardOrLater-Debug-gcov"; }; @@ -2255,6 +2373,11 @@ ); GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = GTM_Prefix.pch; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COMPONENT_SIGNATURE=\"\\'GTM \\'\"", + "kComponentSignatureString=\"\\\"GoogleToolboxForMac\\\"\"", + ); GCC_WARN_SHADOW = YES; GTM_EXTRA_WARNING_OVERRIDE_CFLAGS = "-Wno-unused-parameter"; }; @@ -2291,6 +2414,7 @@ GCC_ENABLE_OBJC_GC = supported; INFOPLIST_FILE = "UnitTest-Info.plist"; PRODUCT_NAME = "UnitTest - AppKit"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GTMUIUnitTestingHarness.app/Contents/MacOS/GTMUIUnitTestingHarness"; }; name = "LeopardOrLater-Release"; }; diff --git a/GTMDefines.h b/GTMDefines.h index bd37b3e..968f868 100644 --- a/GTMDefines.h +++ b/GTMDefines.h @@ -22,10 +22,10 @@ // Not all MAC_OS_X_VERSION_10_X macros defined in past SDKs #ifndef MAC_OS_X_VERSION_10_5 -# define MAC_OS_X_VERSION_10_5 1050 + #define MAC_OS_X_VERSION_10_5 1050 #endif #ifndef MAC_OS_X_VERSION_10_6 -# define MAC_OS_X_VERSION_10_6 1060 + #define MAC_OS_X_VERSION_10_6 1060 #endif // ---------------------------------------------------------------------------- @@ -39,10 +39,10 @@ // the code by providing your own definitions for these w/in a prefix header. // #ifndef GTM_HTTPFETCHER_ENABLE_LOGGING -# define GTM_HTTPFETCHER_ENABLE_LOGGING 1 + #define GTM_HTTPFETCHER_ENABLE_LOGGING 1 #endif // GTM_HTTPFETCHER_ENABLE_LOGGING #ifndef GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING -# define GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING 0 + #define GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING 0 #endif // GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING // By setting the GTM_CONTAINERS_VALIDATION_FAILED_LOG and @@ -50,7 +50,28 @@ // when a validation fails. If you implement your own validators, you may want // to control their internals using the same macros for consistency. #ifndef GTM_CONTAINERS_VALIDATION_FAILED_ASSERT -#define GTM_CONTAINERS_VALIDATION_FAILED_ASSERT 0 + #define GTM_CONTAINERS_VALIDATION_FAILED_ASSERT 0 +#endif + +// Give ourselves a consistent way to do inlines. Apple's macros even use +// a few different actual definitions, so we're based off of the foundation +// one. +#if !defined(GTM_INLINE) + #if defined (__GNUC__) && (__GNUC__ == 4) + #define GTM_INLINE static __inline__ __attribute__((always_inline)) + #else + #define GTM_INLINE static __inline__ + #endif +#endif + +// Give ourselves a consistent way of doing externs that links up nicely +// when mixing objc and objc++ +#if !defined (GTM_EXTERN) + #if defined __cplusplus + #define GTM_EXTERN extern "C" + #else + #define GTM_EXTERN extern + #endif #endif // _GTMDevLog & _GTMDevAssert @@ -76,9 +97,9 @@ #ifndef _GTMDevLog #ifdef DEBUG - #define _GTMDevLog(...) NSLog(__VA_ARGS__) + #define _GTMDevLog(...) NSLog(__VA_ARGS__) #else - #define _GTMDevLog(...) do { } while (0) + #define _GTMDevLog(...) do { } while (0) #endif #endif // _GTMDevLog @@ -86,24 +107,24 @@ // Declared here so that it can easily be used for logging tracking if // necessary. See GTMUnitTestDevLog.h for details. @class NSString; -extern void _GTMUnitTestDevLog(NSString *format, ...); +GTM_EXTERN void _GTMUnitTestDevLog(NSString *format, ...); #ifndef _GTMDevAssert // we directly invoke the NSAssert handler so we can pass on the varargs // (NSAssert doesn't have a macro we can use that takes varargs) #if !defined(NS_BLOCK_ASSERTIONS) -#define _GTMDevAssert(condition, ...) \ - do { \ - if (!(condition)) { \ - [[NSAssertionHandler currentHandler] \ - handleFailureInFunction:[NSString stringWithCString:__PRETTY_FUNCTION__] \ - file:[NSString stringWithCString:__FILE__] \ - lineNumber:__LINE__ \ - description:__VA_ARGS__]; \ - } \ - } while(0) + #define _GTMDevAssert(condition, ...) \ + do { \ + if (!(condition)) { \ + [[NSAssertionHandler currentHandler] \ + handleFailureInFunction:[NSString stringWithCString:__PRETTY_FUNCTION__] \ + file:[NSString stringWithCString:__FILE__] \ + lineNumber:__LINE__ \ + description:__VA_ARGS__]; \ + } \ + } while(0) #else // !defined(NS_BLOCK_ASSERTIONS) -#define _GTMDevAssert(condition, ...) do { } while (0) + #define _GTMDevAssert(condition, ...) do { } while (0) #endif // !defined(NS_BLOCK_ASSERTIONS) #endif // _GTMDevAssert @@ -119,13 +140,13 @@ extern void _GTMUnitTestDevLog(NSString *format, ...); // Wrapping this in an #ifndef allows external groups to define their own // compile time assert scheme. #ifndef _GTMCompileAssert -// We got this technique from here: -// http://unixjunkie.blogspot.com/2007/10/better-compile-time-asserts_29.html + // We got this technique from here: + // http://unixjunkie.blogspot.com/2007/10/better-compile-time-asserts_29.html -#define _GTMCompileAssertSymbolInner(line, msg) _GTMCOMPILEASSERT ## line ## __ ## msg -#define _GTMCompileAssertSymbol(line, msg) _GTMCompileAssertSymbolInner(line, msg) -#define _GTMCompileAssert(test, msg) \ - typedef char _GTMCompileAssertSymbol(__LINE__, msg) [ ((test) ? 1 : -1) ] + #define _GTMCompileAssertSymbolInner(line, msg) _GTMCOMPILEASSERT ## line ## __ ## msg + #define _GTMCompileAssertSymbol(line, msg) _GTMCompileAssertSymbolInner(line, msg) + #define _GTMCompileAssert(test, msg) \ + typedef char _GTMCompileAssertSymbol(__LINE__, msg) [ ((test) ? 1 : -1) ] #endif // _GTMCompileAssert // ============================================================================ @@ -156,45 +177,33 @@ extern void _GTMUnitTestDevLog(NSString *format, ...); // defines for non Leopard SDKs #if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_4 // NSInteger/NSUInteger and Max/Mins - #ifndef NSINTEGER_DEFINED - #if __LP64__ || NS_BUILD_32_LIKE_64 - typedef long NSInteger; - typedef unsigned long NSUInteger; - #else - typedef int NSInteger; - typedef unsigned int NSUInteger; - #endif - #define NSIntegerMax LONG_MAX - #define NSIntegerMin LONG_MIN - #define NSUIntegerMax ULONG_MAX - #define NSINTEGER_DEFINED 1 - #endif // NSINTEGER_DEFINED - // CGFloat - #ifndef CGFLOAT_DEFINED - #if defined(__LP64__) && __LP64__ - // This really is an untested path (64bit on Tiger?) - typedef double CGFloat; - #define CGFLOAT_MIN DBL_MIN - #define CGFLOAT_MAX DBL_MAX - #define CGFLOAT_IS_DOUBLE 1 - #else /* !defined(__LP64__) || !__LP64__ */ - typedef float CGFloat; - #define CGFLOAT_MIN FLT_MIN - #define CGFLOAT_MAX FLT_MAX - #define CGFLOAT_IS_DOUBLE 0 - #endif /* !defined(__LP64__) || !__LP64__ */ - #define CGFLOAT_DEFINED 1 - #endif // CGFLOAT_DEFINED + #ifndef NSINTEGER_DEFINED + #if __LP64__ || NS_BUILD_32_LIKE_64 + typedef long NSInteger; + typedef unsigned long NSUInteger; + #else + typedef int NSInteger; + typedef unsigned int NSUInteger; + #endif + #define NSIntegerMax LONG_MAX + #define NSIntegerMin LONG_MIN + #define NSUIntegerMax ULONG_MAX + #define NSINTEGER_DEFINED 1 + #endif // NSINTEGER_DEFINED + // CGFloat + #ifndef CGFLOAT_DEFINED + #if defined(__LP64__) && __LP64__ + // This really is an untested path (64bit on Tiger?) + typedef double CGFloat; + #define CGFLOAT_MIN DBL_MIN + #define CGFLOAT_MAX DBL_MAX + #define CGFLOAT_IS_DOUBLE 1 + #else /* !defined(__LP64__) || !__LP64__ */ + typedef float CGFloat; + #define CGFLOAT_MIN FLT_MIN + #define CGFLOAT_MAX FLT_MAX + #define CGFLOAT_IS_DOUBLE 0 + #endif /* !defined(__LP64__) || !__LP64__ */ + #define CGFLOAT_DEFINED 1 + #endif // CGFLOAT_DEFINED #endif // MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_4 - - -// Give ourselves a consistent way to do inlines. Apple's macros even use -// a few different actual definitions, so we're based off of the foundation -// one. -#if !defined(GTM_INLINE) - #if defined (__GNUC__) && (__GNUC__ == 4) - #define GTM_INLINE static __inline__ __attribute__((always_inline)) - #else - #define GTM_INLINE static __inline__ - #endif -#endif diff --git a/GTMiPhone.xcodeproj/project.pbxproj b/GTMiPhone.xcodeproj/project.pbxproj index 9c548ec..64e0671 100644 --- a/GTMiPhone.xcodeproj/project.pbxproj +++ b/GTMiPhone.xcodeproj/project.pbxproj @@ -24,6 +24,8 @@ 1D3623EC0D0F72F000981E51 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1D3623EB0D0F72F000981E51 /* CoreGraphics.framework */; }; 1D60589F0D05DD5A006BFB54 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1D30AB110D05D00D00671497 /* Foundation.framework */; }; 1DF5F4E00D08C38300B7A737 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1DF5F4DF0D08C38300B7A737 /* UIKit.framework */; }; + 6294461C0EDE178D009295EA /* GTMNSArray+MergeTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 629446190EDE177A009295EA /* GTMNSArray+MergeTest.m */; }; + 6294461D0EDE17A0009295EA /* GTMNSArray+Merge.m in Sources */ = {isa = PBXBuildFile; fileRef = 629446180EDE177A009295EA /* GTMNSArray+Merge.m */; }; 67A7820C0E00927400EBF506 /* GTMIPhoneUnitTestDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 67A7820B0E00927400EBF506 /* GTMIPhoneUnitTestDelegate.m */; }; 8B308BCE0DAD0B8400183556 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B308BCD0DAD0B8400183556 /* QuartzCore.framework */; }; 8B3AA8F30E032FC7007E31B5 /* GTMNSString+URLArguments.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B3AA8F10E032FC7007E31B5 /* GTMNSString+URLArguments.m */; }; @@ -113,6 +115,9 @@ 1D6058910D05DD3D006BFB54 /* GTMiPhoneTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GTMiPhoneTest.app; sourceTree = BUILT_PRODUCTS_DIR; }; 1DF5F4DF0D08C38300B7A737 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 32CA4F630368D1EE00C91783 /* GTM_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTM_Prefix.pch; sourceTree = "<group>"; }; + 629446170EDE177A009295EA /* GTMNSArray+Merge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GTMNSArray+Merge.h"; sourceTree = "<group>"; }; + 629446180EDE177A009295EA /* GTMNSArray+Merge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSArray+Merge.m"; sourceTree = "<group>"; }; + 629446190EDE177A009295EA /* GTMNSArray+MergeTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSArray+MergeTest.m"; sourceTree = "<group>"; }; 67A7820A0E00927400EBF506 /* GTMIPhoneUnitTestDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMIPhoneUnitTestDelegate.h; sourceTree = "<group>"; }; 67A7820B0E00927400EBF506 /* GTMIPhoneUnitTestDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMIPhoneUnitTestDelegate.m; sourceTree = "<group>"; }; 8B308BCD0DAD0B8400183556 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; @@ -349,6 +354,9 @@ F418AFB10E755B4D004FB565 /* GTMLoggerRingBufferWriter.h */, F418AFB20E755B4D004FB565 /* GTMLoggerRingBufferWriter.m */, F418AFB30E755B4D004FB565 /* GTMLoggerRingBufferWriterTest.m */, + 629446170EDE177A009295EA /* GTMNSArray+Merge.h */, + 629446180EDE177A009295EA /* GTMNSArray+Merge.m */, + 629446190EDE177A009295EA /* GTMNSArray+MergeTest.m */, 8BC0477E0DAE928A00C2D1CA /* GTMNSData+zlib.h */, 8BC0477F0DAE928A00C2D1CA /* GTMNSData+zlib.m */, 8BC047800DAE928A00C2D1CA /* GTMNSData+zlibTest.m */, @@ -626,6 +634,8 @@ F4EF8AD80EBFF814008DD6DA /* GTMStackTraceTest.m in Sources */, F417115A0ECDFF0400B9B276 /* GTMLightweightProxy.m in Sources */, F417115B0ECDFF0400B9B276 /* GTMLightweightProxyTest.m in Sources */, + 6294461C0EDE178D009295EA /* GTMNSArray+MergeTest.m in Sources */, + 6294461D0EDE17A0009295EA /* GTMNSArray+Merge.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ReleaseNotes.txt b/ReleaseNotes.txt index 45ec5dc..b0c06e3 100644 --- a/ReleaseNotes.txt +++ b/ReleaseNotes.txt @@ -7,7 +7,12 @@ Discussion group: http://groups.google.com/group/google-toolbox-for-mac Release ?.?.? Changes since 1.5.1 -- Added GTMSignalHandler for simple signal handling (via kqueue/runloop). +- Added GTMNSArray+Merge for merging one array into another with or without + a custom merging function, returning a new array with the merged contents. + +- Added GTMSignalHandler for simple signal handling (via kqueue/runloop). This + has gotten an api tweak, so some code that started using it will need + updating. - Fixed up GTMIPhoneUnitTestDelegate to be pickier about which tests it runs @@ -160,9 +165,39 @@ Changes since 1.5.1 - GTMStackTrace support for building a trace from the call stack in an NSException (for 10.5+ and iPhone). + +- GTMStackTrace works on 10.5+ (and iPhone) using NSThread to build the call + stack. - GTMLightweightProxy for breaking retain cycles. +- 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. + Release 1.5.1 Changes since 1.5.0 diff --git a/UnitTesting/GTMIPhoneUnitTestDelegate.m b/UnitTesting/GTMIPhoneUnitTestDelegate.m index 7ef46fc..ab33932 100644 --- a/UnitTesting/GTMIPhoneUnitTestDelegate.m +++ b/UnitTesting/GTMIPhoneUnitTestDelegate.m @@ -34,11 +34,6 @@ static int MethodSort(const void *a, const void *b) { return strcmp(nameA, nameB); } -@interface UIApplication (iPhoneUnitTestAdditions) -// "Private" method that we need -- (void)terminateWithSuccess; -@end - @implementation GTMIPhoneUnitTestDelegate // Return YES if class is subclass (1 or more generations) of SenTestCase @@ -62,10 +57,7 @@ static int MethodSort(const void *a, const void *b) { // Using private call to end our tests if (!getenv("GTM_DISABLE_TERMINATION")) { - // I call this delayed just to make sure that the stack is clean - [application performSelector:@selector(terminateWithSuccess) - withObject:nil - afterDelay:0.00]; + exit(0); } } diff --git a/UnitTesting/GTMNSObject+UnitTesting.h b/UnitTesting/GTMNSObject+UnitTesting.h index 68fab04..57b53bf 100644 --- a/UnitTesting/GTMNSObject+UnitTesting.h +++ b/UnitTesting/GTMNSObject+UnitTesting.h @@ -414,8 +414,8 @@ do { \ // notification so that objects who want to add data to the encoded objects unit // test state can do so. The Coder will be in the userInfo dictionary for the // notification under the GTMUnitTestingEncoderKey key. -extern NSString *const GTMUnitTestingEncodedObjectNotification; +GTM_EXTERN NSString *const GTMUnitTestingEncodedObjectNotification; // Key for finding the encoder in the userInfo dictionary for // GTMUnitTestingEncodedObjectNotification notifications. -extern NSString *const GTMUnitTestingEncoderKey; +GTM_EXTERN NSString *const GTMUnitTestingEncoderKey; diff --git a/UnitTesting/GTMSenTestCase.h b/UnitTesting/GTMSenTestCase.h index 99f520a..1d35591 100644 --- a/UnitTesting/GTMSenTestCase.h +++ b/UnitTesting/GTMSenTestCase.h @@ -990,10 +990,10 @@ do { \ - (void)failWithException:(NSException*)exception; @end -extern NSString *const SenTestFailureException; +GTM_EXTERN NSString *const SenTestFailureException; -extern NSString *const SenTestFilenameKey; -extern NSString *const SenTestLineNumberKey; +GTM_EXTERN NSString *const SenTestFilenameKey; +GTM_EXTERN NSString *const SenTestLineNumberKey; #endif // GTM_IPHONE_SDK diff --git a/UnitTesting/GTMUIUnitTestingHarness/Info.plist b/UnitTesting/GTMUIUnitTestingHarness/Info.plist index fe3e712..11355f7 100644 --- a/UnitTesting/GTMUIUnitTestingHarness/Info.plist +++ b/UnitTesting/GTMUIUnitTestingHarness/Info.plist @@ -22,6 +22,49 @@ <string>1.0</string> <key>NSMainNibFile</key> <string>MainMenu</string> + <!-- The CFBundleURLTypes are in here specifically to test + GTMGetURLHandler. See GTMGetURLHandlerTest for details --> + <key>CFBundleURLTypes</key> + <array> + <dict> + <key>CFBundleURLName</key> + <string>GTMUIUnitTestingHarnessURL</string> + <key>CFBundleURLSchemes</key> + <array> + <string>gtmgeturlhandlertest</string> + </array> + <key>GTMBundleURLClass</key> + <string>GTMGetURLHandlerTest</string> + </dict> + <dict> + <key>CFBundleURLName</key> + <string>GTMGetURLHandlerBadClassURL</string> + <key>CFBundleURLSchemes</key> + <array> + <string>gtmgeturlhandlerbadclasstest</string> + </array> + <key>GTMBundleURLClass</key> + <string>GTMGetURLHandlerBadClassWarning</string> + </dict> + <dict> + <key>CFBundleURLName</key> + <string>GTMGetURLHandlerMissingClassURL</string> + <key>CFBundleURLSchemes</key> + <array> + <string>gtmgeturlhandlermissingclasstest</string> + </array> + <key>GTMBundleURLClass</key> + <string>GTMGetURLHandlerMissingClassWarning</string> + </dict> + <dict> + <key>CFBundleURLName</key> + <string>GTMGetURLHandlerMissingHandlerURL</string> + <key>CFBundleURLSchemes</key> + <array> + <string>gtmgeturlhandlermissinghandlerurl</string> + </array> + </dict> + </array> <key>NSPrincipalClass</key> <string>NSApplication</string> </dict> diff --git a/UnitTesting/GTMUnitTestDevLog.m b/UnitTesting/GTMUnitTestDevLog.m index 30ab13b..a0c31b3 100644 --- a/UnitTesting/GTMUnitTestDevLog.m +++ b/UnitTesting/GTMUnitTestDevLog.m @@ -17,9 +17,40 @@ // #import "GTMUnitTestDevLog.h" + + #import "GTMRegex.h" #import "GTMSenTestCase.h" +#if !GTM_IPHONE_SDK +// Add support for grabbing messages from Carbon. +#import <CoreServices/CoreServices.h> +static void GTMDevLogDebugAssert(OSType componentSignature, + UInt32 options, + const char *assertionString, + const char *exceptionLabelString, + const char *errorString, + const char *fileName, + long lineNumber, + void *value, + ConstStr255Param outputMsg) { + NSString *outLog = [[[NSString alloc] initWithBytes:&(outputMsg[1]) + length:StrLength(outputMsg) + encoding:NSMacOSRomanStringEncoding] + autorelease]; + _GTMDevLog(outLog); +} +static inline void GTMInstallDebugAssertOutputHandler(void) { + InstallDebugAssertOutputHandler(GTMDevLogDebugAssert); +} +static inline void GTMUninstallDebugAssertOutputHandler(void) { + InstallDebugAssertOutputHandler(NULL); +} +#else // GTM_IPHONE_SDK +static inline void GTMInstallDebugAssertOutputHandler(void) {}; +static inline void GTMUninstallDebugAssertOutputHandler(void) {}; +#endif // GTM_IPHONE_SDK + @implementation GTMUnitTestDevLog // If unittests are ever being run on separate threads, this may need to be // made a thread local variable. @@ -38,10 +69,12 @@ static BOOL gTrackingEnabled = NO; } + (void)enableTracking { + GTMInstallDebugAssertOutputHandler(); gTrackingEnabled = YES; } + (void)disableTracking { + GTMUninstallDebugAssertOutputHandler(); gTrackingEnabled = NO; } diff --git a/UnitTesting/GTMUnitTestingUtilities.h b/UnitTesting/GTMUnitTestingUtilities.h index eca0825..128a4c4 100644 --- a/UnitTesting/GTMUnitTestingUtilities.h +++ b/UnitTesting/GTMUnitTestingUtilities.h @@ -39,5 +39,53 @@ // } + (void)setUpForUIUnitTestsIfBeingTested; +// Check if the screen saver is running. Some unit tests don't work when +// the screen saver is active. ++ (BOOL)isScreenSaverActive; + +// Allows for posting either a keydown or a keyup with all the modifiers being +// applied. Passing a 'g' with NSKeyDown and NSShiftKeyMask +// generates two events (a shift key key down and a 'g' key keydown). Make sure +// to balance this with a keyup, or things could get confused. Events get posted +// using the CGRemoteOperation events which means that it gets posted in the +// system event queue. Thus you can affect other applications if your app isn't +// the active app (or in some cases, such as hotkeys, even if it is). +// Arguments: +// type - Event type. Currently accepts NSKeyDown and NSKeyUp +// keyChar - character on the keyboard to type. Make sure it is lower case. +// If you need upper case, pass in the NSShiftKeyMask in the +// modifiers. i.e. to generate "G" pass in 'g' and NSShiftKeyMask. +// to generate "+" pass in '=' and NSShiftKeyMask. +// cocoaModifiers - an int made up of bit masks. Handles NSAlphaShiftKeyMask, +// NSShiftKeyMask, NSControlKeyMask, NSAlternateKeyMask, and +// NSCommandKeyMask ++ (void)postKeyEvent:(NSEventType)type + character:(CGCharCode)keyChar + modifiers:(UInt32)cocoaModifiers; + +// Syntactic sugar for posting a keydown immediately followed by a key up event +// which is often what you really want. +// Arguments: +// keyChar - character on the keyboard to type. Make sure it is lower case. +// If you need upper case, pass in the NSShiftKeyMask in the +// modifiers. i.e. to generate "G" pass in 'g' and NSShiftKeyMask. +// to generate "+" pass in '=' and NSShiftKeyMask. +// cocoaModifiers - an int made up of bit masks. Handles NSAlphaShiftKeyMask, +// NSShiftKeyMask, NSControlKeyMask, NSAlternateKeyMask, and +// NSCommandKeyMask ++ (void)postTypeCharacterEvent:(CGCharCode)keyChar + modifiers:(UInt32)cocoaModifiers; + +// Runs the event loop in NSDefaultRunLoopMode until date. Can be useful for +// testing user interface responses in a controlled timed event loop. For most +// uses using: +// [[NSRunLoop currentRunLoop] runUntilDate:date] +// will do. The only reason you would want to use this is if you were +// using the postKeyEvent:character:modifiers to send events and wanted to +// receive user input. +// Arguments: +// date - end of execution time ++ (void)runUntilDate:(NSDate*)date; + @end diff --git a/UnitTesting/GTMUnitTestingUtilities.m b/UnitTesting/GTMUnitTestingUtilities.m index 39534f7..e72a921 100644 --- a/UnitTesting/GTMUnitTestingUtilities.m +++ b/UnitTesting/GTMUnitTestingUtilities.m @@ -19,16 +19,19 @@ #import "GTMUnitTestingUtilities.h" #import <AppKit/AppKit.h> #import "GTMDefines.h" +#import "GTMGarbageCollection.h" // The Users profile before we change it on them -static CMProfileRef gCurrentColorProfile = NULL; +static CMProfileRef gGTMCurrentColorProfile = NULL; // Compares two color profiles -static BOOL AreCMProfilesEqual(CMProfileRef a, CMProfileRef b); +static BOOL GTMAreCMProfilesEqual(CMProfileRef a, CMProfileRef b); // Stores the user's color profile away, and changes over to generic. -static void SetColorProfileToGenericRGB(); +static void GTMSetColorProfileToGenericRGB(); // Restores the users profile. -static void RestoreColorProfile(void); +static void GTMRestoreColorProfile(void); + +static CGKeyCode GTMKeyCodeForCharCode(CGCharCode charCode); @implementation GTMUnitTestingUtilities @@ -81,7 +84,7 @@ static void RestoreColorProfile(void); [defaults setFloat:.001f forKey:@"NSWindowResizeTime"]; // Switch over the screen profile to "generic rgb". This installs an // atexit handler to return our profile back when we are done. - SetColorProfileToGenericRGB(); + GTMSetColorProfileToGenericRGB(); } + (void)setUpForUIUnitTestsIfBeingTested { @@ -91,10 +94,98 @@ static void RestoreColorProfile(void); } [pool release]; } -@end ++ (BOOL)isScreenSaverActive { + BOOL answer = NO; + ProcessSerialNumber psn; + if (GetFrontProcess(&psn) == noErr) { + CFDictionaryRef cfProcessInfo + = ProcessInformationCopyDictionary(&psn, + kProcessDictionaryIncludeAllInformationMask); + NSDictionary *processInfo = GTMCFAutorelease(cfProcessInfo); + + NSString *bundlePath = [processInfo objectForKey:@"BundlePath"]; + // ScreenSaverEngine is the frontmost app if the screen saver is actually + // running Security Agent is the frontmost app if the "enter password" + // dialog is showing + NSString *bundleName = [bundlePath lastPathComponent]; + answer = ([bundleName isEqualToString:@"ScreenSaverEngine.app"] + || [bundleName isEqualToString:@"SecurityAgent.app"]); + } + return answer; +} + +// Allows for posting either a keydown or a keyup with all the modifiers being +// applied. Passing a 'g' with NSKeyDown and NSShiftKeyMask +// generates two events (a shift key key down and a 'g' key keydown). Make sure +// to balance this with a keyup, or things could get confused. Events get posted +// using the CGRemoteOperation events which means that it gets posted in the +// system event queue. Thus you can affect other applications if your app isn't +// the active app (or in some cases, such as hotkeys, even if it is). +// Arguments: +// type - Event type. Currently accepts NSKeyDown and NSKeyUp +// keyChar - character on the keyboard to type. Make sure it is lower case. +// If you need upper case, pass in the NSShiftKeyMask in the +// modifiers. i.e. to generate "G" pass in 'g' and NSShiftKeyMask. +// to generate "+" pass in '=' and NSShiftKeyMask. +// cocoaModifiers - an int made up of bit masks. Handles NSAlphaShiftKeyMask, +// NSShiftKeyMask, NSControlKeyMask, NSAlternateKeyMask, and +// NSCommandKeyMask ++ (void)postKeyEvent:(NSEventType)type + character:(CGCharCode)keyChar + modifiers:(UInt32)cocoaModifiers { + require(![self isScreenSaverActive], CantWorkWithScreenSaver); + require(type == NSKeyDown || type == NSKeyUp, CantDoEvent); + CGKeyCode code = GTMKeyCodeForCharCode(keyChar); + verify(code != 256); + CGEventRef event = CGEventCreateKeyboardEvent(NULL, code, type == NSKeyDown); + require(event, CantCreateEvent); + CGEventSetFlags(event, cocoaModifiers); + CGEventPost(kCGSessionEventTap, event); + CFRelease(event); +CantCreateEvent: +CantDoEvent: +CantWorkWithScreenSaver: + return; +} + +// Syntactic sugar for posting a keydown immediately followed by a key up event +// which is often what you really want. +// Arguments: +// keyChar - character on the keyboard to type. Make sure it is lower case. +// If you need upper case, pass in the NSShiftKeyMask in the +// modifiers. i.e. to generate "G" pass in 'g' and NSShiftKeyMask. +// to generate "+" pass in '=' and NSShiftKeyMask. +// cocoaModifiers - an int made up of bit masks. Handles NSAlphaShiftKeyMask, +// NSShiftKeyMask, NSControlKeyMask, NSAlternateKeyMask, and +// NSCommandKeyMask ++ (void)postTypeCharacterEvent:(CGCharCode)keyChar modifiers:(UInt32)cocoaModifiers { + [self postKeyEvent:NSKeyDown character:keyChar modifiers:cocoaModifiers]; + [self postKeyEvent:NSKeyUp character:keyChar modifiers:cocoaModifiers]; +} + +// Runs the event loop in NSDefaultRunLoopMode until date. Can be useful for +// testing user interface responses in a controlled timed event loop. For most +// uses using: +// [[NSRunLoop currentRunLoop] runUntilDate:date] +// will do. The only reason you would want to use this is if you were +// using the postKeyEvent:character:modifiers to send events and wanted to +// receive user input. +// Arguments: +// date - end of execution time ++ (void)runUntilDate:(NSDate*)date { + NSEvent *event; + while ((event = [NSApp nextEventMatchingMask:NSAnyEventMask + untilDate:date + inMode:NSDefaultRunLoopMode + dequeue:YES])) { + [NSApp sendEvent:event]; + } +} + +@end -BOOL AreCMProfilesEqual(CMProfileRef a, CMProfileRef b) { +BOOL GTMAreCMProfilesEqual(CMProfileRef a, CMProfileRef b) { BOOL equal = YES; if (a != b) { CMProfileMD5 aMD5; @@ -102,17 +193,18 @@ BOOL AreCMProfilesEqual(CMProfileRef a, CMProfileRef b) { CMError aMD5Err = CMGetProfileMD5(a, aMD5); CMError bMD5Err = CMGetProfileMD5(b, bMD5); equal = (!aMD5Err && - !bMD5Err && - !memcmp(aMD5, bMD5, sizeof(CMProfileMD5))) ? YES : NO; + !bMD5Err && + !memcmp(aMD5, bMD5, sizeof(CMProfileMD5))) ? YES : NO; } return equal; } -static void RestoreColorProfile(void) { - if (gCurrentColorProfile) { +void GTMRestoreColorProfile(void) { + if (gGTMCurrentColorProfile) { CGDirectDisplayID displayID = CGMainDisplayID(); - CMError error = CMSetProfileByAVID((UInt32)displayID, gCurrentColorProfile); - CMCloseProfile(gCurrentColorProfile); + CMError error = CMSetProfileByAVID((UInt32)displayID, + gGTMCurrentColorProfile); + CMCloseProfile(gGTMCurrentColorProfile); if (error) { // COV_NF_START // No way to force this case in a unittest. @@ -123,11 +215,11 @@ static void RestoreColorProfile(void) { } else { _GTMDevLog(@"Color profile restored"); } - gCurrentColorProfile = NULL; + gGTMCurrentColorProfile = NULL; } } -void SetColorProfileToGenericRGB(void) { +void GTMSetColorProfileToGenericRGB(void) { NSColorSpace *genericSpace = [NSColorSpace genericRGBColorSpace]; CMProfileRef genericProfile = (CMProfileRef)[genericSpace colorSyncProfile]; CMProfileRef previousProfile; @@ -143,7 +235,7 @@ void SetColorProfileToGenericRGB(void) { return; // COV_NF_END } - if (AreCMProfilesEqual(genericProfile, previousProfile)) { + if (GTMAreCMProfilesEqual(genericProfile, previousProfile)) { CMCloseProfile(previousProfile); return; } @@ -166,9 +258,53 @@ void SetColorProfileToGenericRGB(void) { "a result. (Error: %i)", genericProfileName, error); // COV_NF_END } else { - gCurrentColorProfile = previousProfile; - atexit(RestoreColorProfile); + gGTMCurrentColorProfile = previousProfile; + atexit(GTMRestoreColorProfile); } CFRelease(previousProfileName); CFRelease(genericProfileName); } + +// Returns a virtual key code for a given charCode. Handles all of the +// NS*FunctionKeys as well. +static CGKeyCode GTMKeyCodeForCharCode(CGCharCode charCode) { + // character map taken from http://classicteck.com/rbarticles/mackeyboard.php + int characters[] = { + 'a', 's', 'd', 'f', 'h', 'g', 'z', 'x', 'c', 'v', 256, 'b', 'q', 'w', + 'e', 'r', 'y', 't', '1', '2', '3', '4', '6', '5', '=', '9', '7', '-', + '8', '0', ']', 'o', 'u', '[', 'i', 'p', '\n', 'l', 'j', '\'', 'k', ';', + '\\', ',', '/', 'n', 'm', '.', '\t', ' ', '`', '\b', 256, '\e' + }; + + // function key map taken from + // file:///Developer/ADC%20Reference%20Library/documentation/Cocoa/Reference/ApplicationKit/ObjC_classic/Classes/NSEvent.html + int functionKeys[] = { + // NSUpArrowFunctionKey - NSF12FunctionKey + 126, 125, 123, 124, 122, 120, 99, 118, 96, 97, 98, 100, 101, 109, 103, 111, + // NSF13FunctionKey - NSF28FunctionKey + 105, 107, 113, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, + // NSF29FunctionKey - NSScrollLockFunctionKey + 256, 256, 256, 256, 256, 256, 256, 256, 117, 115, 256, 119, 116, 121, 256, 256, + // NSPauseFunctionKey - NSPrevFunctionKey + 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, + // NSNextFunctionKey - NSModeSwitchFunctionKey + 256, 256, 256, 256, 256, 256, 114, 1 + }; + + CGKeyCode outCode = 0; + + // Look in the function keys + if (charCode >= NSUpArrowFunctionKey && charCode <= NSModeSwitchFunctionKey) { + outCode = functionKeys[charCode - NSUpArrowFunctionKey]; + } else { + // Look in our character map + for (size_t i = 0; i < (sizeof(characters) / sizeof (int)); i++) { + if (characters[i] == charCode) { + outCode = i; + break; + } + } + } + return outCode; +} + diff --git a/UnitTesting/RunIPhoneUnitTest.sh b/UnitTesting/RunIPhoneUnitTest.sh index 0b6fe69..bf25714 100755 --- a/UnitTesting/RunIPhoneUnitTest.sh +++ b/UnitTesting/RunIPhoneUnitTest.sh @@ -75,6 +75,13 @@ if [ "$PLATFORM_NAME" == "iphonesimulator" ]; then export NSZombieEnabled=YES fi + # Cleanup user home and documents directory + if [ -d "$CFFIXED_USER_HOME" ]; then + rm -rf "$CFFIXED_USER_HOME" + fi + mkdir "$CFFIXED_USER_HOME" + mkdir "$CFFIXED_USER_HOME/Documents" + # 6251475 iPhone simulator leaks @ CFHTTPCookieStore shutdown if # CFFIXED_USER_HOME empty GTM_LEAKS_SYMBOLS_TO_IGNORE="CFHTTPCookieStore" diff --git a/XcodeConfig/subconfig/Debug.xcconfig b/XcodeConfig/subconfig/Debug.xcconfig index d8fbd1c..7d54d61 100644 --- a/XcodeConfig/subconfig/Debug.xcconfig +++ b/XcodeConfig/subconfig/Debug.xcconfig @@ -40,4 +40,8 @@ GTM_CONFIGURATION_GCC_PREPROCESSOR_DEFINITIONS = _GLIBCXX_DEBUG_PEDANTIC _GLIBCX // Turns on stack protection on debug builds for Leopard and above GTM_CONFIGURATION_OTHER_CFLAGS = -fstack-protector -fstack-protector-all // Stack protection doesn't exist on Tiger -GTM_CONFIGURATION_OTHER_CFLAGS[sdk=macosx10.4*] = +GTM_CONFIGURATION_OTHER_CFLAGS[sdk=macosx10.4*] = + +// And is really broken on the iPhone Device. Works on simulator though. +// rdar://639430 Xcode generates bad Arm code if -fstack-protector -fstack-protector-all +GTM_CONFIGURATION_OTHER_CFLAGS[sdk=iphoneos*] = diff --git a/XcodeConfig/subconfig/General.xcconfig b/XcodeConfig/subconfig/General.xcconfig index 2aea15f..dc3e64f 100644 --- a/XcodeConfig/subconfig/General.xcconfig +++ b/XcodeConfig/subconfig/General.xcconfig @@ -46,6 +46,12 @@ ARCHS[sdk=iphonesimulator*] = i386 // Build only the active architecture on iphone device targets ONLY_ACTIVE_ARCH[sdk=iphoneos*] = YES +// iPhone currently deploys on 10.5 only +MACOSX_DEPLOYMENT_TARGET[sdk=iphone*] = 10.5 + +// We want our pngs compressed when they are copied +COMPRESS_PNG_FILES = YES + // Zerolink prevents link warnings so turn it off ZERO_LINK = NO diff --git a/XcodeConfig/subconfig/iPhone20.xcconfig b/XcodeConfig/subconfig/iPhone20.xcconfig index 2509ba8..0f92c95 100644 --- a/XcodeConfig/subconfig/iPhone20.xcconfig +++ b/XcodeConfig/subconfig/iPhone20.xcconfig @@ -17,7 +17,8 @@ // License for the specific language governing permissions and limitations under // the License. -// Default SDK and minimum OS version is the iphone SDK. +// Set default SDK. SDKROOT = iphoneos2.0 + +// iPhone currently deploys on 10.5 only MACOSX_DEPLOYMENT_TARGET = 10.5 -GCC_VERSION = 4.0 diff --git a/XcodeConfig/subconfig/iPhone21.xcconfig b/XcodeConfig/subconfig/iPhone21.xcconfig index e543ce5..61c59ed 100644 --- a/XcodeConfig/subconfig/iPhone21.xcconfig +++ b/XcodeConfig/subconfig/iPhone21.xcconfig @@ -17,7 +17,8 @@ // License for the specific language governing permissions and limitations under // the License. -// Default SDK and minimum OS version is the iphone SDK. +// Set default SDK. SDKROOT = iphoneos2.1 + +// iPhone currently deploys on 10.5 only MACOSX_DEPLOYMENT_TARGET = 10.5 -GCC_VERSION = 4.0 diff --git a/iPhone/GTMABAddressBook.h b/iPhone/GTMABAddressBook.h index cdcfa7c..828f05a 100644 --- a/iPhone/GTMABAddressBook.h +++ b/iPhone/GTMABAddressBook.h @@ -1,5 +1,5 @@ // -// GTMAddressBook.h +// GTMABAddressBook.h // // Copyright 2008 Google Inc. // @@ -74,7 +74,7 @@ @class GTMABGroup; @class GTMABRecord; -extern NSString *const kGTMABUnknownPropertyName; +GTM_EXTERN NSString *const kGTMABUnknownPropertyName; // Wrapper for an AddressBook on iPhone @interface GTMABAddressBook : NSObject { |