blob: 8a83f81dea505c3c395a21a78d9c5b6bd8cfb2c3 [file] [log] [blame]
//
// GTMAppKitUnitTestingUtilities.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 "GTMAppKitUnitTestingUtilities.h"
#import "GTMDefines.h"
static CGKeyCode GTMKeyCodeForCharCode(CGCharCode charCode);
@implementation GTMAppKitUnitTestingUtilities
+ (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];
}
@end
// 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;
}
@implementation NSApplication (GTMUnitTestingRunAdditions)
- (BOOL)gtm_runUntilDate:(NSDate *)date
context:(id<GTMUnitTestingRunLoopContext>)context {
BOOL contextShouldStop = NO;
while (1) {
contextShouldStop = [context shouldStop];
if (contextShouldStop) break;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSEvent *event = [NSApp nextEventMatchingMask:NSAnyEventMask
untilDate:date
inMode:NSDefaultRunLoopMode
dequeue:YES];
if (!event) {
[pool drain];
break;
}
[NSApp sendEvent:event];
[pool drain];
}
return contextShouldStop;
}
- (BOOL)gtm_runUpToSixtySecondsWithContext:(id<GTMUnitTestingRunLoopContext>)context {
return [self gtm_runUntilDate:[NSDate dateWithTimeIntervalSinceNow:60]
context:context];
}
@end