Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

port of martin's code to objc 2 and added a framework target

  • Loading branch information...
commit a98bf58a0c3c7e963477e3fb8ab9d281fbfc3a3e 0 parents
@mattetti authored
Showing with 8,931 additions and 0 deletions.
  1. +3 −0  .gitignore
  2. +20 −0 AppleRemote-Info.plist
  3. +67 −0 AppleRemote.bridgesupport
  4. +37 −0 AppleRemote.h
  5. +93 −0 AppleRemote.m
  6. BIN  AppleRemote.tiff
  7. BIN  English.lproj/InfoPlist.strings
  8. +48 −0 English.lproj/MainMenu.nib/classes.nib
  9. +20 −0 English.lproj/MainMenu.nib/info.nib
  10. BIN  English.lproj/MainMenu.nib/keyedobjects.nib
  11. +49 −0 GlobalKeyboardDevice.h
  12. +241 −0 GlobalKeyboardDevice.m
  13. +64 −0 HIDRemoteControlDevice.h
  14. +514 −0 HIDRemoteControlDevice.m
  15. +28 −0 Info.plist
  16. BIN  Japanese.lproj/InfoPlist.strings
  17. +4 −0 Japanese.lproj/MainMenu.nib/classes.nib
  18. +17 −0 Japanese.lproj/MainMenu.nib/info.nib
  19. BIN  Japanese.lproj/MainMenu.nib/keyedobjects.nib
  20. +39 −0 KeyspanFrontRowControl.h
  21. +87 −0 KeyspanFrontRowControl.m
  22. +24 −0 MainController.h
  23. +204 −0 MainController.m
  24. +90 −0 MultiClickRemoteBehavior.h
  25. +210 −0 MultiClickRemoteBehavior.m
  26. +102 −0 RemoteControl.h
  27. +103 −0 RemoteControl.m
  28. +954 −0 RemoteControlClasses.graffle
  29. +38 −0 RemoteControlContainer.h
  30. +108 −0 RemoteControlContainer.m
  31. +1,318 −0 RemoteControlWrapper.xcodeproj/martin.mode1
  32. +1,328 −0 RemoteControlWrapper.xcodeproj/martin.mode1v3
  33. +313 −0 RemoteControlWrapper.xcodeproj/martin.pbxuser
  34. +1,456 −0 RemoteControlWrapper.xcodeproj/mattetti.mode1v3
  35. +587 −0 RemoteControlWrapper.xcodeproj/mattetti.pbxuser
  36. +542 −0 RemoteControlWrapper.xcodeproj/project.pbxproj
  37. +7 −0 RemoteControlWrapper_Prefix.pch
  38. +20 −0 RemoteControls-Info.plist
  39. +15 −0 RemoteFeedbackView.h
  40. +140 −0 RemoteFeedbackView.m
  41. +20 −0 license.txt
  42. +14 −0 main.m
  43. +7 −0 readme.txt
3  .gitignore
@@ -0,0 +1,3 @@
+.DS_Store
+*/.DS_Store
+build
20 AppleRemote-Info.plist
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>English</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>com.yourcompany.${PRODUCT_NAME:identifier}</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundlePackageType</key>
+ <string>FMWK</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+</dict>
+</plist>
67 AppleRemote.bridgesupport
@@ -0,0 +1,67 @@
+<?xml version='1.0'?>
+<!DOCTYPE signatures SYSTEM "file://localhost/System/Library/DTDs/BridgeSupport.dtd">
+<signatures version='0.9'>
+<depends_on path='/System/Library/Frameworks/Foundation.framework'/>
+<depends_on path='/System/Library/Frameworks/AppKit.framework'/>
+<depends_on path='/System/Library/Frameworks/Carbon.framework'/>
+<depends_on path='/System/Library/Frameworks/IOKit.framework'/>
+<depends_on path='/System/Library/Frameworks/CoreFoundation.framework'/>
+<constant name='FINISHED_USING_REMOTE_CONTROL_NOTIFICATION' type='@'/>
+<constant name='REQUEST_FOR_REMOTE_CONTROL_NOTIFCATION' type='@'/>
+<constant name='kApplicationIdentifier' type='@'/>
+<constant name='kRemoteControlDeviceName' type='@'/>
+<constant name='kTargetApplicationIdentifier' type='@'/>
+<enum name='EVENT_TO_HOLD_EVENT_OFFSET' value='6'/>
+<enum name='kRemoteButtonLeft' value='64'/>
+<enum name='kRemoteButtonLeft_Hold' value='4096'/>
+<enum name='kRemoteButtonMenu' value='8'/>
+<enum name='kRemoteButtonMenu_Hold' value='512'/>
+<enum name='kRemoteButtonMinus' value='4'/>
+<enum name='kRemoteButtonMinus_Hold' value='256'/>
+<enum name='kRemoteButtonPlay' value='16'/>
+<enum name='kRemoteButtonPlay_Hold' value='1024'/>
+<enum name='kRemoteButtonPlus' value='2'/>
+<enum name='kRemoteButtonPlus_Hold' value='128'/>
+<enum name='kRemoteButtonRight' value='32'/>
+<enum name='kRemoteButtonRight_Hold' value='2048'/>
+<enum name='kRemoteControl_Switched' value='8192'/>
+<class name='HIDRemoteControlDevice'>
+<method class_method='true' selector='isRemoteAvailable'>
+<retval type='B'/>
+</method>
+<method selector='processesBacklog'>
+<retval type='B'/>
+</method>
+<method selector='sendRemoteButtonEvent:pressedDown:'>
+<arg type='B' index='1'/>
+</method>
+<method selector='setProcessesBacklog:'>
+<arg type='B' index='0'/>
+</method>
+</class>
+<class name='NSObject'>
+<method selector='sendRemoteButtonEvent:pressedDown:remoteControl:'>
+<arg type='B' index='1'/>
+</method>
+</class>
+<class name='RemoteControl'>
+<method selector='isListeningToRemote'>
+<retval type='B'/>
+</method>
+<method selector='isOpenInExclusiveMode'>
+<retval type='B'/>
+</method>
+<method selector='sendsEventForButtonIdentifier:'>
+<retval type='B'/>
+</method>
+<method selector='setListeningToRemote:'>
+<arg type='B' index='0'/>
+</method>
+<method selector='setOpenInExclusiveMode:'>
+<arg type='B' index='0'/>
+</method>
+</class>
+<informal_protocol name='RemoteControlDelegate'>
+<method type='v20@0:4i8B12@16' selector='sendRemoteButtonEvent:pressedDown:remoteControl:'/>
+</informal_protocol>
+</signatures>
37 AppleRemote.h
@@ -0,0 +1,37 @@
+/*****************************************************************************
+ * RemoteControlWrapper.h
+ * RemoteControlWrapper
+ *
+ * Created by Martin Kahr on 11.03.06 under a MIT-style license.
+ * Copyright (c) 2006 martinkahr.com. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ *****************************************************************************/
+
+#import <Cocoa/Cocoa.h>
+#import "HIDRemoteControlDevice.h"
+
+/* Interacts with the Apple Remote Control HID device
+ The class is not thread safe
+*/
+@interface AppleRemote : HIDRemoteControlDevice {
+}
+
+@end
93 AppleRemote.m
@@ -0,0 +1,93 @@
+/*****************************************************************************
+ * RemoteControlWrapper.m
+ * RemoteControlWrapper
+ *
+ * Created by Martin Kahr on 11.03.06 under a MIT-style license.
+ * Copyright (c) 2006 martinkahr.com. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ *****************************************************************************/
+
+#import "AppleRemote.h"
+
+#import <mach/mach.h>
+#import <mach/mach_error.h>
+#import <IOKit/IOKitLib.h>
+#import <IOKit/IOCFPlugIn.h>
+#import <IOKit/hid/IOHIDKeys.h>
+
+const char* AppleRemoteDeviceName = "AppleIRController";
+
+// the WWDC 07 Leopard Build is missing the constant
+#ifndef NSAppKitVersionNumber10_4
+ #define NSAppKitVersionNumber10_4 824
+#endif
+
+@implementation AppleRemote
+
++ (const char*) remoteControlDeviceName {
+ return AppleRemoteDeviceName;
+}
+
+- (void) setCookieMappingInDictionary: (NSMutableDictionary*) _cookieToButtonMapping {
+ if (floor(NSAppKitVersionNumber) <= NSAppKitVersionNumber10_4) {
+ // 10.4.x Tiger
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlus] forKey:@"14_12_11_6_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMinus] forKey:@"14_13_11_6_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMenu] forKey:@"14_7_6_14_7_6_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlay] forKey:@"14_8_6_14_8_6_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonRight] forKey:@"14_9_6_14_9_6_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonLeft] forKey:@"14_10_6_14_10_6_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonRight_Hold] forKey:@"14_6_4_2_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonLeft_Hold] forKey:@"14_6_3_2_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMenu_Hold] forKey:@"14_6_14_6_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlay_Hold] forKey:@"18_14_6_18_14_6_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteControl_Switched] forKey:@"19_"];
+ } else {
+ // 10.5.x Leopard
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlus] forKey:@"31_29_28_19_18_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMinus] forKey:@"31_30_28_19_18_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMenu] forKey:@"31_20_19_18_31_20_19_18_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlay] forKey:@"31_21_19_18_31_21_19_18_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonRight] forKey:@"31_22_19_18_31_22_19_18_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonLeft] forKey:@"31_23_19_18_31_23_19_18_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonRight_Hold] forKey:@"31_19_18_4_2_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonLeft_Hold] forKey:@"31_19_18_3_2_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMenu_Hold] forKey:@"31_19_18_31_19_18_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlay_Hold] forKey:@"35_31_19_18_35_31_19_18_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteControl_Switched] forKey:@"19_"];
+ }
+}
+
+- (void) sendRemoteButtonEvent: (RemoteControlEventIdentifier) event pressedDown: (BOOL) pressedDown {
+ if (pressedDown == NO && event == kRemoteButtonMenu_Hold) {
+ // There is no seperate event for pressed down on menu hold. We are simulating that event here
+ [super sendRemoteButtonEvent:event pressedDown:YES];
+ }
+
+ [super sendRemoteButtonEvent:event pressedDown:pressedDown];
+
+ if (pressedDown && (event == kRemoteButtonRight || event == kRemoteButtonLeft || event == kRemoteButtonPlay || event == kRemoteButtonMenu || event == kRemoteButtonPlay_Hold)) {
+ // There is no seperate event when the button is being released. We are simulating that event here
+ [super sendRemoteButtonEvent:event pressedDown:NO];
+ }
+}
+
+@end
BIN  AppleRemote.tiff
Binary file not shown
BIN  English.lproj/InfoPlist.strings
Binary file not shown
48 English.lproj/MainMenu.nib/classes.nib
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IBClasses</key>
+ <array>
+ <dict>
+ <key>CLASS</key>
+ <string>RemoteFeedbackView</string>
+ <key>LANGUAGE</key>
+ <string>ObjC</string>
+ <key>SUPERCLASS</key>
+ <string>NSView</string>
+ </dict>
+ <dict>
+ <key>CLASS</key>
+ <string>FirstResponder</string>
+ <key>LANGUAGE</key>
+ <string>ObjC</string>
+ <key>SUPERCLASS</key>
+ <string>NSObject</string>
+ </dict>
+ <dict>
+ <key>CLASS</key>
+ <string>NSObject</string>
+ <key>LANGUAGE</key>
+ <string>ObjC</string>
+ </dict>
+ <dict>
+ <key>CLASS</key>
+ <string>MainController</string>
+ <key>LANGUAGE</key>
+ <string>ObjC</string>
+ <key>OUTLETS</key>
+ <dict>
+ <key>feedbackText</key>
+ <string>NSTextField</string>
+ <key>feedbackView</key>
+ <string>NSView</string>
+ </dict>
+ <key>SUPERCLASS</key>
+ <string>NSObject</string>
+ </dict>
+ </array>
+ <key>IBVersion</key>
+ <string>1</string>
+</dict>
+</plist>
20 English.lproj/MainMenu.nib/info.nib
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IBFramework Version</key>
+ <string>608</string>
+ <key>IBLastKnownRelativeProjectPath</key>
+ <string>../../RemoteControlWrapper.xcodeproj</string>
+ <key>IBOldestOS</key>
+ <integer>5</integer>
+ <key>IBOpenObjects</key>
+ <array>
+ <integer>21</integer>
+ </array>
+ <key>IBSystem Version</key>
+ <string>9A466</string>
+ <key>targetFramework</key>
+ <string>IBCocoaFramework</string>
+</dict>
+</plist>
BIN  English.lproj/MainMenu.nib/keyedobjects.nib
Binary file not shown
49 GlobalKeyboardDevice.h
@@ -0,0 +1,49 @@
+/*****************************************************************************
+ * GlobalKeyboardDevice.h
+ * RemoteControlWrapper
+ *
+ * Created by Martin Kahr on 11.03.06 under a MIT-style license.
+ * Copyright (c) 2006 martinkahr.com. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ *****************************************************************************/
+
+#import <Cocoa/Cocoa.h>
+#import <Carbon/Carbon.h>
+
+#import "RemoteControl.h"
+
+/*
+ This class registers for a number of global keyboard shortcuts to simulate a remote control
+ */
+@interface GlobalKeyboardDevice : RemoteControl {
+
+ NSMutableDictionary* hotKeyRemoteEventMapping;
+ EventHandlerRef eventHandlerRef;
+
+}
+
+- (void) mapRemoteButton: (RemoteControlEventIdentifier) remoteButtonIdentifier defaultKeycode: (unsigned int) defaultKeycode defaultModifiers: (unsigned int) defaultModifiers;
+
+- (BOOL)registerHotKeyCode: (unsigned int) keycode modifiers: (unsigned int) modifiers remoteEventIdentifier: (RemoteControlEventIdentifier) identifier;
+
+static OSStatus hotKeyEventHandler(EventHandlerCallRef inHandlerRef, EventRef inEvent, void* refCon );
+
+@end
241 GlobalKeyboardDevice.m
@@ -0,0 +1,241 @@
+/*****************************************************************************
+ * GlobalKeyboardDevice.m
+ * RemoteControlWrapper
+ *
+ * Created by Martin Kahr on 11.03.06 under a MIT-style license.
+ * Copyright (c) 2006 martinkahr.com. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ *****************************************************************************/
+
+
+#import "GlobalKeyboardDevice.h"
+
+#define F1 122
+#define F2 120
+#define F3 99
+#define F4 118
+#define F5 96
+#define F6 97
+#define F7 98
+
+/*
+ the following default keys are read and shall be used to change the keyboard mapping
+
+ mac.remotecontrols.GlobalKeyboardDevice.plus_modifiers
+ mac.remotecontrols.GlobalKeyboardDevice.plus_keycode
+ mac.remotecontrols.GlobalKeyboardDevice.minus_modifiers
+ mac.remotecontrols.GlobalKeyboardDevice.minus_keycode
+ mac.remotecontrols.GlobalKeyboardDevice.play_modifiers
+ mac.remotecontrols.GlobalKeyboardDevice.play_keycode
+ mac.remotecontrols.GlobalKeyboardDevice.left_modifiers
+ mac.remotecontrols.GlobalKeyboardDevice.left_keycode
+ mac.remotecontrols.GlobalKeyboardDevice.right_modifiers
+ mac.remotecontrols.GlobalKeyboardDevice.right_keycode
+ mac.remotecontrols.GlobalKeyboardDevice.menu_modifiers
+ mac.remotecontrols.GlobalKeyboardDevice.menu_keycode
+ mac.remotecontrols.GlobalKeyboardDevice.playhold_modifiers
+ mac.remotecontrols.GlobalKeyboardDevice.playhold_keycode
+ */
+
+
+@implementation GlobalKeyboardDevice
+
+- (id) initWithDelegate: (id) _remoteControlDelegate {
+ if (self = [super initWithDelegate: _remoteControlDelegate]) {
+ hotKeyRemoteEventMapping = [[NSMutableDictionary alloc] init];
+
+ unsigned int modifiers = cmdKey + shiftKey /*+ optionKey*/ + controlKey;
+
+ [self mapRemoteButton:kRemoteButtonPlus defaultKeycode:F1 defaultModifiers:modifiers];
+ [self mapRemoteButton:kRemoteButtonMinus defaultKeycode:F2 defaultModifiers:modifiers];
+ [self mapRemoteButton:kRemoteButtonPlay defaultKeycode:F3 defaultModifiers:modifiers];
+ [self mapRemoteButton:kRemoteButtonLeft defaultKeycode:F4 defaultModifiers:modifiers];
+ [self mapRemoteButton:kRemoteButtonRight defaultKeycode:F5 defaultModifiers:modifiers];
+ [self mapRemoteButton:kRemoteButtonMenu defaultKeycode:F6 defaultModifiers:modifiers];
+ [self mapRemoteButton:kRemoteButtonPlay_Hold defaultKeycode:F7 defaultModifiers:modifiers];
+ }
+ return self;
+}
+
+- (void) dealloc {
+ [hotKeyRemoteEventMapping release];
+ [super dealloc];
+}
+
+- (void) mapRemoteButton: (RemoteControlEventIdentifier) remoteButtonIdentifier defaultKeycode: (unsigned int) defaultKeycode defaultModifiers: (unsigned int) defaultModifiers {
+ NSString* defaultsKey;
+
+ switch(remoteButtonIdentifier) {
+ case kRemoteButtonPlus:
+ defaultsKey = @"plus";
+ break;
+ case kRemoteButtonMinus:
+ defaultsKey = @"minus";
+ break;
+ case kRemoteButtonMenu:
+ defaultsKey = @"menu";
+ break;
+ case kRemoteButtonPlay:
+ defaultsKey = @"play";
+ break;
+ case kRemoteButtonRight:
+ defaultsKey = @"right";
+ break;
+ case kRemoteButtonLeft:
+ defaultsKey = @"left";
+ break;
+ case kRemoteButtonPlay_Hold:
+ defaultsKey = @"playhold";
+ break;
+ default:
+ NSLog(@"Unknown global keyboard defaults key for remote button identifier %d", remoteButtonIdentifier);
+ }
+
+ NSNumber* modifiersCfg = [[NSUserDefaults standardUserDefaults] objectForKey: [NSString stringWithFormat: @"mac.remotecontrols.GlobalKeyboardDevice.%@_modifiers", defaultsKey]];
+ NSNumber* keycodeCfg = [[NSUserDefaults standardUserDefaults] objectForKey: [NSString stringWithFormat: @"mac.remotecontrols.GlobalKeyboardDevice.%@_keycode", defaultsKey]];
+
+ unsigned int modifiers = defaultModifiers;
+ if (modifiersCfg) modifiers = [modifiersCfg unsignedIntValue];
+
+ unsigned int keycode = defaultKeycode;
+ if (keycodeCfg) keycode = [keycodeCfg unsignedIntValue];
+
+ [self registerHotKeyCode: keycode modifiers: modifiers remoteEventIdentifier: remoteButtonIdentifier];
+}
+
+- (void) setListeningToRemote: (BOOL) value {
+ if (value == [self isListeningToRemote]) return;
+ if (value) {
+ [self startListening: self];
+ } else {
+ [self stopListening: self];
+ }
+}
+- (BOOL) isListeningToRemote {
+ return (eventHandlerRef!=NULL);
+}
+
+- (IBAction) startListening: (id) sender {
+
+ if (eventHandlerRef) return;
+
+ EventTypeSpec eventSpec[2] = {
+ { kEventClassKeyboard, kEventHotKeyPressed },
+ { kEventClassKeyboard, kEventHotKeyReleased }
+ };
+
+ InstallEventHandler( GetEventDispatcherTarget(),
+ (EventHandlerProcPtr)hotKeyEventHandler,
+ 2, eventSpec, self, &eventHandlerRef);
+}
+- (IBAction) stopListening: (id) sender {
+ RemoveEventHandler(eventHandlerRef);
+ eventHandlerRef = NULL;
+}
+
+- (BOOL) sendsEventForButtonIdentifier: (RemoteControlEventIdentifier) identifier {
+ NSEnumerator* values = [hotKeyRemoteEventMapping objectEnumerator];
+ NSNumber* remoteIdentifier;
+ while( (remoteIdentifier = [values nextObject]) ) {
+ if ([remoteIdentifier unsignedIntValue] == identifier) return YES;
+ }
+ return NO;
+}
+
++ (const char*) remoteControlDeviceName {
+ return "Keyboard";
+}
+
+- (BOOL)registerHotKeyCode: (unsigned int) keycode modifiers: (unsigned int) modifiers remoteEventIdentifier: (RemoteControlEventIdentifier) identifier {
+ OSStatus err;
+ EventHotKeyID hotKeyID;
+ EventHotKeyRef carbonHotKey;
+
+ hotKeyID.signature = 'PTHk';
+ hotKeyID.id = (long)keycode;
+
+ err = RegisterEventHotKey(keycode, modifiers, hotKeyID, GetEventDispatcherTarget(), nil, &carbonHotKey );
+
+ if( err )
+ return NO;
+
+ [hotKeyRemoteEventMapping setObject: [NSNumber numberWithInt:identifier] forKey: [NSNumber numberWithUnsignedInt: hotKeyID.id]];
+
+ return YES;
+}
+/*
+- (void)unregisterHotKey: (PTHotKey*)hotKey
+{
+ OSStatus err;
+ EventHotKeyRef carbonHotKey;
+ NSValue* key;
+
+ if( [[self allHotKeys] containsObject: hotKey] == NO )
+ return;
+
+ carbonHotKey = [self _carbonHotKeyForHotKey: hotKey];
+ NSAssert( carbonHotKey != nil, @"" );
+
+ err = UnregisterEventHotKey( carbonHotKey );
+ //Watch as we ignore 'err':
+
+ key = [NSValue valueWithPointer: carbonHotKey];
+ [mHotKeys removeObjectForKey: key];
+
+ [self _updateEventHandler];
+
+ //See that? Completely ignored
+}
+*/
+
+- (RemoteControlEventIdentifier) remoteControlEventIdentifierForID: (unsigned int) id {
+ NSNumber* remoteEventIdentifier = [hotKeyRemoteEventMapping objectForKey:[NSNumber numberWithUnsignedInt: id]];
+ return [remoteEventIdentifier unsignedIntValue];
+}
+
+- (void) sendRemoteButtonEvent: (RemoteControlEventIdentifier) event pressedDown: (BOOL) pressedDown {
+ [delegate sendRemoteButtonEvent: event pressedDown: pressedDown remoteControl:self];
+}
+
+static RemoteControlEventIdentifier lastEvent;
+
+static OSStatus hotKeyEventHandler(EventHandlerCallRef inHandlerRef, EventRef inEvent, void* userData )
+{
+ GlobalKeyboardDevice* keyboardDevice = (GlobalKeyboardDevice*) userData;
+ EventHotKeyID hkCom;
+ GetEventParameter(inEvent,kEventParamDirectObject,typeEventHotKeyID,NULL,sizeof(hkCom),NULL,&hkCom);
+
+ RemoteControlEventIdentifier identifier = [keyboardDevice remoteControlEventIdentifierForID:hkCom.id];
+ if (identifier == 0) return noErr;
+
+ BOOL pressedDown = YES;
+ if (identifier != lastEvent) {
+ lastEvent = identifier;
+ } else {
+ lastEvent = 0;
+ pressedDown = NO;
+ }
+ [keyboardDevice sendRemoteButtonEvent: identifier pressedDown: pressedDown];
+
+ return noErr;
+}
+
+@end
64 HIDRemoteControlDevice.h
@@ -0,0 +1,64 @@
+/*****************************************************************************
+ * HIDRemoteControlDevice.h
+ * RemoteControlWrapper
+ *
+ * Created by Martin Kahr on 11.03.06 under a MIT-style license.
+ * Copyright (c) 2006 martinkahr.com. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ *****************************************************************************/
+
+#import <Cocoa/Cocoa.h>
+#import <IOKit/hid/IOHIDLib.h>
+
+#import "RemoteControl.h"
+
+/*
+ Base class for HID based remote control devices
+ */
+@interface HIDRemoteControlDevice : RemoteControl {
+ IOHIDDeviceInterface** hidDeviceInterface;
+ IOHIDQueueInterface** queue;
+ NSMutableArray* allCookies;
+ NSMutableDictionary* cookieToButtonMapping;
+ CFRunLoopSourceRef eventSource;
+
+ BOOL fixSecureEventInputBug;
+ BOOL openInExclusiveMode;
+ BOOL processesBacklog;
+
+ int supportedButtonEvents;
+}
+
+// When your application needs to much time on the main thread when processing an event other events
+// may already be received which are put on a backlog. As soon as your main thread
+// has some spare time this backlog is processed and may flood your delegate with calls.
+// Backlog processing is turned off by default.
+- (BOOL) processesBacklog;
+- (void) setProcessesBacklog: (BOOL) value;
+
+// methods that should be overwritten by subclasses
+- (void) setCookieMappingInDictionary: (NSMutableDictionary*) cookieToButtonMapping;
+
+- (void) sendRemoteButtonEvent: (RemoteControlEventIdentifier) event pressedDown: (BOOL) pressedDown;
+
++ (BOOL) isRemoteAvailable;
+
+@end
514 HIDRemoteControlDevice.m
@@ -0,0 +1,514 @@
+/*****************************************************************************
+ * HIDRemoteControlDevice.m
+ * RemoteControlWrapper
+ *
+ * Created by Martin Kahr on 11.03.06 under a MIT-style license.
+ * Copyright (c) 2006 martinkahr.com. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ *****************************************************************************/
+
+#import "HIDRemoteControlDevice.h"
+
+#import <mach/mach.h>
+#import <mach/mach_error.h>
+#import <IOKit/IOKitLib.h>
+#import <IOKit/IOCFPlugIn.h>
+#import <IOKit/hid/IOHIDKeys.h>
+#import <Carbon/Carbon.h>
+
+@interface HIDRemoteControlDevice (PrivateMethods)
+- (NSDictionary*) cookieToButtonMapping;
+- (IOHIDQueueInterface**) queue;
+- (IOHIDDeviceInterface**) hidDeviceInterface;
+- (void) handleEventWithCookieString: (NSString*) cookieString sumOfValues: (SInt32) sumOfValues;
+- (void) removeNotifcationObserver;
+- (void) remoteControlAvailable:(NSNotification *)notification;
+
+@end
+
+@interface HIDRemoteControlDevice (IOKitMethods)
++ (io_object_t) findRemoteDevice;
+- (IOHIDDeviceInterface**) createInterfaceForDevice: (io_object_t) hidDevice;
+- (BOOL) initializeCookies;
+- (BOOL) openDevice;
+@end
+
+@implementation HIDRemoteControlDevice
+
++ (const char*) remoteControlDeviceName {
+ return "";
+}
+
++ (BOOL) isRemoteAvailable {
+ io_object_t hidDevice = [self findRemoteDevice];
+ if (hidDevice != 0) {
+ IOObjectRelease(hidDevice);
+ return YES;
+ } else {
+ return NO;
+ }
+}
+
+- (id) initWithDelegate: (id) _remoteControlDelegate {
+ if ([[self class] isRemoteAvailable] == NO) return nil;
+
+ if ( self = [super initWithDelegate: _remoteControlDelegate] ) {
+ openInExclusiveMode = YES;
+ queue = NULL;
+ hidDeviceInterface = NULL;
+ cookieToButtonMapping = [[NSMutableDictionary alloc] init];
+
+ [self setCookieMappingInDictionary: cookieToButtonMapping];
+
+ NSEnumerator* enumerator = [cookieToButtonMapping objectEnumerator];
+ NSNumber* identifier;
+ supportedButtonEvents = 0;
+ while(identifier = [enumerator nextObject]) {
+ supportedButtonEvents |= [identifier intValue];
+ }
+
+ fixSecureEventInputBug = [[NSUserDefaults standardUserDefaults] boolForKey: @"remoteControlWrapperFixSecureEventInputBug"];
+ }
+
+ return self;
+}
+
+- (void) dealloc {
+ [self removeNotifcationObserver];
+ [self stopListening:self];
+ [cookieToButtonMapping release];
+ [super dealloc];
+}
+
+- (void) sendRemoteButtonEvent: (RemoteControlEventIdentifier) event pressedDown: (BOOL) pressedDown {
+ [delegate sendRemoteButtonEvent: event pressedDown: pressedDown remoteControl:self];
+}
+
+- (void) setCookieMappingInDictionary: (NSMutableDictionary*) cookieToButtonMapping {
+}
+- (int) remoteIdSwitchCookie {
+ return 0;
+}
+
+- (BOOL) sendsEventForButtonIdentifier: (RemoteControlEventIdentifier) identifier {
+ return (supportedButtonEvents & identifier) == identifier;
+}
+
+- (BOOL) isListeningToRemote {
+ return (hidDeviceInterface != NULL && allCookies != NULL && queue != NULL);
+}
+
+- (void) setListeningToRemote: (BOOL) value {
+ if (value == NO) {
+ [self stopListening:self];
+ } else {
+ [self startListening:self];
+ }
+}
+
+- (BOOL) isOpenInExclusiveMode {
+ return openInExclusiveMode;
+}
+- (void) setOpenInExclusiveMode: (BOOL) value {
+ openInExclusiveMode = value;
+}
+
+- (BOOL) processesBacklog {
+ return processesBacklog;
+}
+- (void) setProcessesBacklog: (BOOL) value {
+ processesBacklog = value;
+}
+
+- (IBAction) startListening: (id) sender {
+ if ([self isListeningToRemote]) return;
+
+ // 4th July 2007
+ //
+ // A security update in february of 2007 introduced an odd behavior.
+ // Whenever SecureEventInput is activated or deactivated the exclusive access
+ // to the remote control device is lost. This leads to very strange behavior where
+ // a press on the Menu button activates FrontRow while your app still gets the event.
+ // A great number of people have complained about this.
+ //
+ // Enabling the SecureEventInput and keeping it enabled does the trick.
+ //
+ // I'm pretty sure this is a kind of bug at Apple and I'm in contact with the responsible
+ // Apple Engineer. This solution is not a perfect one - I know.
+ // One of the side effects is that applications that listen for special global keyboard shortcuts (like Quicksilver)
+ // may get into problems as they no longer get the events.
+ // As there is no official Apple Remote API from Apple I also failed to open a technical incident on this.
+ //
+ // Note that there is a corresponding DisableSecureEventInput in the stopListening method below.
+ //
+ if ([self isOpenInExclusiveMode] && fixSecureEventInputBug) EnableSecureEventInput();
+
+ [self removeNotifcationObserver];
+
+ io_object_t hidDevice = [[self class] findRemoteDevice];
+ if (hidDevice == 0) return;
+
+ if ([self createInterfaceForDevice:hidDevice] == NULL) {
+ goto error;
+ }
+
+ if ([self initializeCookies]==NO) {
+ goto error;
+ }
+
+ if ([self openDevice]==NO) {
+ goto error;
+ }
+ // be KVO friendly
+ [self willChangeValueForKey:@"listeningToRemote"];
+ [self didChangeValueForKey:@"listeningToRemote"];
+ goto cleanup;
+
+error:
+ [self stopListening:self];
+ DisableSecureEventInput();
+
+cleanup:
+ IOObjectRelease(hidDevice);
+}
+
+- (IBAction) stopListening: (id) sender {
+ if ([self isListeningToRemote]==NO) return;
+
+ BOOL sendNotification = NO;
+
+ if (eventSource != NULL) {
+ CFRunLoopRemoveSource(CFRunLoopGetCurrent(), eventSource, kCFRunLoopDefaultMode);
+ CFRelease(eventSource);
+ eventSource = NULL;
+ }
+ if (queue != NULL) {
+ (*queue)->stop(queue);
+
+ //dispose of queue
+ (*queue)->dispose(queue);
+
+ //release the queue we allocated
+ (*queue)->Release(queue);
+
+ queue = NULL;
+
+ sendNotification = YES;
+ }
+
+ if (allCookies != nil) {
+ [allCookies autorelease];
+ allCookies = nil;
+ }
+
+ if (hidDeviceInterface != NULL) {
+ //close the device
+ (*hidDeviceInterface)->close(hidDeviceInterface);
+
+ //release the interface
+ (*hidDeviceInterface)->Release(hidDeviceInterface);
+
+ hidDeviceInterface = NULL;
+ }
+
+ if ([self isOpenInExclusiveMode] && fixSecureEventInputBug) DisableSecureEventInput();
+
+ if ([self isOpenInExclusiveMode] && sendNotification) {
+ [[self class] sendFinishedNotifcationForAppIdentifier: nil];
+ }
+ // be KVO friendly
+ [self willChangeValueForKey:@"listeningToRemote"];
+ [self didChangeValueForKey:@"listeningToRemote"];
+}
+
+@end
+
+@implementation HIDRemoteControlDevice (PrivateMethods)
+
+- (IOHIDQueueInterface**) queue {
+ return queue;
+}
+
+- (IOHIDDeviceInterface**) hidDeviceInterface {
+ return hidDeviceInterface;
+}
+
+
+- (NSDictionary*) cookieToButtonMapping {
+ return cookieToButtonMapping;
+}
+
+- (NSString*) validCookieSubstring: (NSString*) cookieString {
+ if (cookieString == nil || [cookieString length] == 0) return nil;
+ NSEnumerator* keyEnum = [[self cookieToButtonMapping] keyEnumerator];
+ NSString* key;
+ while(key = [keyEnum nextObject]) {
+ NSRange range = [cookieString rangeOfString:key];
+ if (range.location == 0) return key;
+ }
+ return nil;
+}
+
+- (void) handleEventWithCookieString: (NSString*) cookieString sumOfValues: (SInt32) sumOfValues {
+ /*
+ if (previousRemainingCookieString) {
+ cookieString = [previousRemainingCookieString stringByAppendingString: cookieString];
+ NSLog(@"New cookie string is %@", cookieString);
+ [previousRemainingCookieString release], previousRemainingCookieString=nil;
+ }*/
+ if (cookieString == nil || [cookieString length] == 0) return;
+
+ NSNumber* buttonId = [[self cookieToButtonMapping] objectForKey: cookieString];
+ if (buttonId != nil) {
+ [self sendRemoteButtonEvent: [buttonId intValue] pressedDown: (sumOfValues>0)];
+ } else {
+ // let's see if a number of events are stored in the cookie string. this does
+ // happen when the main thread is too busy to handle all incoming events in time.
+ NSString* subCookieString;
+ NSString* lastSubCookieString=nil;
+ while(subCookieString = [self validCookieSubstring: cookieString]) {
+ cookieString = [cookieString substringFromIndex: [subCookieString length]];
+ lastSubCookieString = subCookieString;
+ if (processesBacklog) [self handleEventWithCookieString: subCookieString sumOfValues:sumOfValues];
+ }
+ if (processesBacklog == NO && lastSubCookieString != nil) {
+ // process the last event of the backlog and assume that the button is not pressed down any longer.
+ // The events in the backlog do not seem to be in order and therefore (in rare cases) the last event might be
+ // a button pressed down event while in reality the user has released it.
+ // NSLog(@"processing last event of backlog");
+ [self handleEventWithCookieString: lastSubCookieString sumOfValues:0];
+ }
+ if ([cookieString length] > 0) {
+ NSLog(@"Unknown button for cookiestring %@", cookieString);
+ }
+ }
+}
+
+- (void) removeNotifcationObserver {
+ [[NSDistributedNotificationCenter defaultCenter] removeObserver:self name:FINISHED_USING_REMOTE_CONTROL_NOTIFICATION object:nil];
+}
+
+- (void) remoteControlAvailable:(NSNotification *)notification {
+ [self removeNotifcationObserver];
+ [self startListening: self];
+}
+
+@end
+
+/* Callback method for the device queue
+Will be called for any event of any type (cookie) to which we subscribe
+*/
+static void QueueCallbackFunction(void* target, IOReturn result, void* refcon, void* sender) {
+ if (target < 0) {
+ NSLog(@"QueueCallbackFunction called with invalid target!");
+ return;
+ }
+ NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
+
+ HIDRemoteControlDevice* remote = (HIDRemoteControlDevice*)target;
+ IOHIDEventStruct event;
+ AbsoluteTime zeroTime = {0,0};
+ NSMutableString* cookieString = [NSMutableString string];
+ SInt32 sumOfValues = 0;
+ while (result == kIOReturnSuccess)
+ {
+ result = (*[remote queue])->getNextEvent([remote queue], &event, zeroTime, 0);
+ if ( result != kIOReturnSuccess )
+ continue;
+
+ //printf("%d %d %d\n", event.elementCookie, event.value, event.longValue);
+
+ if (((int)event.elementCookie)!=5) {
+ sumOfValues+=event.value;
+ [cookieString appendString:[NSString stringWithFormat:@"%d_", event.elementCookie]];
+ }
+ }
+ [remote handleEventWithCookieString: cookieString sumOfValues: sumOfValues];
+
+ [pool release];
+}
+
+@implementation HIDRemoteControlDevice (IOKitMethods)
+
+- (IOHIDDeviceInterface**) createInterfaceForDevice: (io_object_t) hidDevice {
+ io_name_t className;
+ IOCFPlugInInterface** plugInInterface = NULL;
+ HRESULT plugInResult = S_OK;
+ SInt32 score = 0;
+ IOReturn ioReturnValue = kIOReturnSuccess;
+
+ hidDeviceInterface = NULL;
+
+ ioReturnValue = IOObjectGetClass(hidDevice, className);
+
+ if (ioReturnValue != kIOReturnSuccess) {
+ NSLog(@"Error: Failed to get class name.");
+ return NULL;
+ }
+
+ ioReturnValue = IOCreatePlugInInterfaceForService(hidDevice,
+ kIOHIDDeviceUserClientTypeID,
+ kIOCFPlugInInterfaceID,
+ &plugInInterface,
+ &score);
+ if (ioReturnValue == kIOReturnSuccess)
+ {
+ //Call a method of the intermediate plug-in to create the device interface
+ plugInResult = (*plugInInterface)->QueryInterface(plugInInterface, CFUUIDGetUUIDBytes(kIOHIDDeviceInterfaceID), (LPVOID) &hidDeviceInterface);
+
+ if (plugInResult != S_OK) {
+ NSLog(@"Error: Couldn't create HID class device interface");
+ }
+ // Release
+ if (plugInInterface) (*plugInInterface)->Release(plugInInterface);
+ }
+ return hidDeviceInterface;
+}
+
+- (BOOL) initializeCookies {
+ IOHIDDeviceInterface122** handle = (IOHIDDeviceInterface122**)hidDeviceInterface;
+ IOHIDElementCookie cookie;
+ long usage;
+ long usagePage;
+ id object;
+ NSArray* elements = nil;
+ NSDictionary* element;
+ IOReturn success;
+
+ if (!handle || !(*handle)) return NO;
+
+ // Copy all elements, since we're grabbing most of the elements
+ // for this device anyway, and thus, it's faster to iterate them
+ // ourselves. When grabbing only one or two elements, a matching
+ // dictionary should be passed in here instead of NULL.
+ success = (*handle)->copyMatchingElements(handle, NULL, (CFArrayRef*)&elements);
+
+ if (success == kIOReturnSuccess) {
+
+ [elements autorelease];
+ /*
+ cookies = calloc(NUMBER_OF_APPLE_REMOTE_ACTIONS, sizeof(IOHIDElementCookie));
+ memset(cookies, 0, sizeof(IOHIDElementCookie) * NUMBER_OF_APPLE_REMOTE_ACTIONS);
+ */
+ allCookies = [[NSMutableArray alloc] init];
+
+
+ for (element in elements) {
+ //Get cookie
+ object = [element valueForKey: (NSString*)CFSTR(kIOHIDElementCookieKey) ];
+ if (object == nil || ![object isKindOfClass:[NSNumber class]]) continue;
+ if (object == 0 || CFGetTypeID(object) != CFNumberGetTypeID()) continue;
+ cookie = (IOHIDElementCookie) [object longValue];
+
+ //Get usage
+ object = [element valueForKey: (NSString*)CFSTR(kIOHIDElementUsageKey) ];
+ if (object == nil || ![object isKindOfClass:[NSNumber class]]) continue;
+ usage = [object longValue];
+
+ //Get usage page
+ object = [element valueForKey: (NSString*)CFSTR(kIOHIDElementUsagePageKey) ];
+ if (object == nil || ![object isKindOfClass:[NSNumber class]]) continue;
+ usagePage = [object longValue];
+
+ [allCookies addObject: [NSNumber numberWithInt:(int)cookie]];
+ }
+ } else {
+ return NO;
+ }
+
+ return YES;
+}
+
+- (BOOL) openDevice {
+ HRESULT result;
+
+ IOHIDOptionsType openMode = kIOHIDOptionsTypeNone;
+ if ([self isOpenInExclusiveMode]) openMode = kIOHIDOptionsTypeSeizeDevice;
+ IOReturn ioReturnValue = (*hidDeviceInterface)->open(hidDeviceInterface, openMode);
+
+ if (ioReturnValue == KERN_SUCCESS) {
+ queue = (*hidDeviceInterface)->allocQueue(hidDeviceInterface);
+ if (queue) {
+ result = (*queue)->create(queue, 0, 12); //depth: maximum number of elements in queue before oldest elements in queue begin to be lost.
+
+ IOHIDElementCookie cookie;
+ NSEnumerator *allCookiesEnumerator = [allCookies objectEnumerator];
+
+ while (cookie = (IOHIDElementCookie)[[allCookiesEnumerator nextObject] intValue]) {
+ (*queue)->addElement(queue, cookie, 0);
+ }
+
+ // add callback for async events
+ ioReturnValue = (*queue)->createAsyncEventSource(queue, &eventSource);
+ if (ioReturnValue == KERN_SUCCESS) {
+ ioReturnValue = (*queue)->setEventCallout(queue,QueueCallbackFunction, self, NULL);
+ if (ioReturnValue == KERN_SUCCESS) {
+ CFRunLoopAddSource(CFRunLoopGetCurrent(), eventSource, kCFRunLoopDefaultMode);
+
+ //start data delivery to queue
+ (*queue)->start(queue);
+ return YES;
+ } else {
+ NSLog(@"Error when setting event callback");
+ }
+ } else {
+ NSLog(@"Error when creating async event source");
+ }
+ } else {
+ NSLog(@"Error when opening device");
+ }
+ } else if (ioReturnValue == kIOReturnExclusiveAccess) {
+ // the device is used exclusive by another application
+
+ // 1. we register for the FINISHED_USING_REMOTE_CONTROL_NOTIFICATION notification
+ [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(remoteControlAvailable:) name:FINISHED_USING_REMOTE_CONTROL_NOTIFICATION object:nil];
+
+ // 2. send a distributed notification that we wanted to use the remote control
+ [[self class] sendRequestForRemoteControlNotification];
+ }
+ return NO;
+}
+
++ (io_object_t) findRemoteDevice {
+ CFMutableDictionaryRef hidMatchDictionary = NULL;
+ IOReturn ioReturnValue = kIOReturnSuccess;
+ io_iterator_t hidObjectIterator = 0;
+ io_object_t hidDevice = 0;
+
+ // Set up a matching dictionary to search the I/O Registry by class
+ // name for all HID class devices
+ hidMatchDictionary = IOServiceMatching([self remoteControlDeviceName]);
+
+ // Now search I/O Registry for matching devices.
+ ioReturnValue = IOServiceGetMatchingServices(kIOMasterPortDefault, hidMatchDictionary, &hidObjectIterator);
+
+ if ((ioReturnValue == kIOReturnSuccess) && (hidObjectIterator != 0)) {
+ hidDevice = IOIteratorNext(hidObjectIterator);
+ }
+
+ // release the iterator
+ IOObjectRelease(hidObjectIterator);
+
+ return hidDevice;
+}
+
+@end
+
28 Info.plist
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>English</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIconFile</key>
+ <string></string>
+ <key>CFBundleIdentifier</key>
+ <string>com.yourcompany.AppleRemote</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>NSMainNibFile</key>
+ <string>MainMenu</string>
+ <key>NSPrincipalClass</key>
+ <string>NSApplication</string>
+</dict>
+</plist>
BIN  Japanese.lproj/InfoPlist.strings
Binary file not shown
4 Japanese.lproj/MainMenu.nib/classes.nib
@@ -0,0 +1,4 @@
+{
+ IBClasses = ({CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; });
+ IBVersion = 1;
+}
17 Japanese.lproj/MainMenu.nib/info.nib
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IBDocumentLocation</key>
+ <string>94 103 356 240 0 0 1280 1002 </string>
+ <key>IBEditorPositions</key>
+ <dict>
+ <key>29</key>
+ <string>93 343 318 44 0 0 1280 1002 </string>
+ </dict>
+ <key>IBFramework Version</key>
+ <string>403.0</string>
+ <key>IBSystem Version</key>
+ <string>8A278</string>
+</dict>
+</plist>
BIN  Japanese.lproj/MainMenu.nib/keyedobjects.nib
Binary file not shown
39 KeyspanFrontRowControl.h
@@ -0,0 +1,39 @@
+/*****************************************************************************
+ * KeyspanFrontRowControl.h
+ * RemoteControlWrapper
+ *
+ * Created by Martin Kahr on 11.03.06 under a MIT-style license.
+ * Copyright (c) 2006 martinkahr.com. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ *****************************************************************************/
+
+
+#import <Cocoa/Cocoa.h>
+#import "HIDRemoteControlDevice.h"
+
+/* Interacts with the Keyspan FrontRow Remote Control HID device
+ The class is not thread safe
+*/
+@interface KeyspanFrontRowControl : HIDRemoteControlDevice {
+
+}
+
+@end
87 KeyspanFrontRowControl.m
@@ -0,0 +1,87 @@
+/*****************************************************************************
+ * KeyspanFrontRowControl.m
+ * RemoteControlWrapper
+ *
+ * Created by Martin Kahr on 11.03.06 under a MIT-style license.
+ * Copyright (c) 2006 martinkahr.com. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ *****************************************************************************/
+
+#import "KeyspanFrontRowControl.h"
+#import <mach/mach.h>
+#import <mach/mach_error.h>
+#import <IOKit/IOKitLib.h>
+#import <IOKit/IOCFPlugIn.h>
+#import <IOKit/hid/IOHIDKeys.h>
+
+@implementation KeyspanFrontRowControl
+
+- (void) setCookieMappingInDictionary: (NSMutableDictionary*) _cookieToButtonMapping {
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlus] forKey:@"11_18_99_10_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMinus] forKey:@"11_18_98_10_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMenu] forKey:@"11_18_58_10_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlay] forKey:@"11_18_61_10_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonRight] forKey:@"11_18_96_10_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonLeft] forKey:@"11_18_97_10_"];
+ /* hold events are not being send by this device
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonRight_Hold] forKey:@"14_6_4_2_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonLeft_Hold] forKey:@"14_6_3_2_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMenu_Hold] forKey:@"14_6_14_6_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlay_Sleep] forKey:@"18_14_6_18_14_6_"];
+ [_cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteControl_Switched] forKey:@"19_"];
+ */
+}
+
++ (io_object_t) findRemoteDevice {
+ CFMutableDictionaryRef hidMatchDictionary = NULL;
+ IOReturn ioReturnValue = kIOReturnSuccess;
+ io_iterator_t hidObjectIterator = 0;
+ io_object_t hidDevice = 0;
+ SInt32 idVendor = 1741;
+ SInt32 idProduct = 0x420;
+
+ // Set up a matching dictionary to search the I/O Registry by class
+ // name for all HID class devices
+ hidMatchDictionary = IOServiceMatching(kIOHIDDeviceKey);
+
+ CFNumberRef numberRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &idVendor);
+ CFDictionaryAddValue(hidMatchDictionary, CFSTR(kIOHIDVendorIDKey), numberRef);
+ CFRelease(numberRef);
+
+ numberRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &idProduct);
+ CFDictionaryAddValue(hidMatchDictionary, CFSTR(kIOHIDProductIDKey), numberRef);
+ CFRelease(numberRef);
+
+ // Now search I/O Registry for matching devices.
+ ioReturnValue = IOServiceGetMatchingServices(kIOMasterPortDefault, hidMatchDictionary, &hidObjectIterator);
+
+ if ((ioReturnValue == kIOReturnSuccess) && (hidObjectIterator != 0)) {
+ hidDevice = IOIteratorNext(hidObjectIterator);
+ }
+
+ // release the iterator
+ IOObjectRelease(hidObjectIterator);
+
+ return hidDevice;
+
+}
+
+@end
24 MainController.h
@@ -0,0 +1,24 @@
+//
+// MainController.h
+// RemoteControlWrapper
+//
+// Created by Martin Kahr on 16.03.06.
+// Copyright 2006 martinkahr.com. All rights reserved.
+//
+
+#import <Cocoa/Cocoa.h>
+
+@class RemoteControl;
+@class MultiClickRemoteBehavior;
+
+@interface MainController : NSObject {
+ RemoteControl* remoteControl;
+ MultiClickRemoteBehavior* remoteControlBehavior;
+
+ IBOutlet NSView* feedbackView;
+ IBOutlet NSTextField* feedbackText;
+}
+
+- (RemoteControl*) remoteControl;
+
+@end
204 MainController.m
@@ -0,0 +1,204 @@
+//
+// MainController.m
+// RemoteControlWrapper
+//
+// Created by Martin Kahr on 16.03.06.
+// Copyright 2006 martinkahr.com. All rights reserved.
+//
+
+#import "MainController.h"
+#import "AppleRemote.h"
+#import "KeyspanFrontRowControl.h"
+#import "GlobalKeyboardDevice.h"
+#import "RemoteControlContainer.h"
+#import "MultiClickRemoteBehavior.h"
+
+// -------------------------------------------------------------------------------------------
+// Below you'll see different usages of the remote control wrapper. Uncomment the macro
+// definition of the one you want to try out and learn from.
+// -------------------------------------------------------------------------------------------
+
+#define SAMPLE1
+//#define SAMPLE2
+//#define SAMPLE3
+
+
+// -------------------------------------------------------------------------------------------
+// Sample Code 1: Get events from the Apple Remote Control
+// -------------------------------------------------------------------------------------------
+#ifdef SAMPLE1
+@implementation MainController(SampleCode1)
+
+- (void) awakeFromNib {
+ remoteControl = [[[AppleRemote alloc] initWithDelegate: self] retain];
+ [remoteControl startListening: self];
+}
+
+- (void) sendRemoteButtonEvent: (RemoteControlEventIdentifier) event pressedDown: (BOOL) pressedDown remoteControl: (RemoteControl*) remoteControl {
+ NSLog(@"Button %d pressed down %d", event, pressedDown);
+}
+
+@end
+#endif
+
+
+
+
+// -------------------------------------------------------------------------------------------
+// Sample Code 2: Using the Remote Control Container
+//
+// To test press COMMAND-SHIFT-CONTROL-F1 to F7 which is the Global Keyboard virtual remote
+// -------------------------------------------------------------------------------------------
+#ifdef SAMPLE2
+@implementation MainController(SampleCode3)
+
+- (void) awakeFromNib {
+
+
+ // A Remote Control Container manages a number of devices and conforms to the RemoteControl interface
+ // Therefore you can enable or disable all the devices of the container with a single "startListening:" call.
+ RemoteControlContainer* container = [[RemoteControlContainer alloc] initWithDelegate: self];
+ [container instantiateAndAddRemoteControlDeviceWithClass: [AppleRemote class]];
+ [container instantiateAndAddRemoteControlDeviceWithClass: [KeyspanFrontRowControl class]];
+ [container instantiateAndAddRemoteControlDeviceWithClass: [GlobalKeyboardDevice class]];
+
+ [container startListening: self];
+
+ remoteControl = container;
+}
+
+- (void) sendRemoteButtonEvent: (RemoteControlEventIdentifier) event pressedDown: (BOOL) pressedDown remoteControl: (RemoteControl*) remoteControl {
+ NSLog(@"Button %d pressed down %d", event, pressedDown);
+}
+@end
+#endif
+
+
+
+
+// -------------------------------------------------------------------------------------------
+// Sample Code 3: Multi Click Behavior and Hold Event Simulation
+// -------------------------------------------------------------------------------------------
+#ifdef SAMPLE3
+@implementation MainController(SampleCode3)
+
+- (void) awakeFromNib {
+ // 1. instantiate the desired behavior for the remote control device
+ remoteControlBehavior = [[MultiClickRemoteBehavior alloc] init];
+
+ // 2. configure the behavior
+ [remoteControlBehavior setDelegate: self];
+
+ // 3. a Remote Control Container manages a number of devices and conforms to the RemoteControl interface
+ // Therefore you can enable or disable all the devices of the container with a single "startListening:" call.
+ RemoteControlContainer* container = [[RemoteControlContainer alloc] initWithDelegate: remoteControlBehavior];
+ [container instantiateAndAddRemoteControlDeviceWithClass: [AppleRemote class]];
+ [container instantiateAndAddRemoteControlDeviceWithClass: [KeyspanFrontRowControl class]];
+ [container instantiateAndAddRemoteControlDeviceWithClass: [GlobalKeyboardDevice class]];
+
+ // to give the binding mechanism a chance to see the change of the attribute
+ [self setValue: container forKey: @"remoteControl"];
+}
+
+// delegate method for the MultiClickRemoteBehavior
+- (void) remoteButton: (RemoteControlEventIdentifier)buttonIdentifier pressedDown: (BOOL) pressedDown clickCount: (unsigned int)clickCount
+{
+ NSString* buttonName=nil;
+ NSString* pressed=@"";
+
+ if (pressedDown) pressed = @"(pressed)"; else pressed = @"(released)";
+
+ switch(buttonIdentifier) {
+ case kRemoteButtonPlus:
+ buttonName = @"Volume up";
+ break;
+ case kRemoteButtonMinus:
+ buttonName = @"Volume down";
+ break;
+ case kRemoteButtonMenu:
+ buttonName = @"Menu";
+ break;
+ case kRemoteButtonPlay:
+ buttonName = @"Play";
+ break;
+ case kRemoteButtonRight:
+ buttonName = @"Right";
+ break;
+ case kRemoteButtonLeft:
+ buttonName = @"Left";
+ break;
+ case kRemoteButtonRight_Hold:
+ buttonName = @"Right holding";
+ break;
+ case kRemoteButtonLeft_Hold:
+ buttonName = @"Left holding";
+ break;
+ case kRemoteButtonPlus_Hold:
+ buttonName = @"Volume up holding";
+ break;
+ case kRemoteButtonMinus_Hold:
+ buttonName = @"Volume down holding";
+ break;
+ case kRemoteButtonPlay_Hold:
+ buttonName = @"Play (sleep mode)";
+ break;
+ case kRemoteButtonMenu_Hold:
+ buttonName = @"Menu (long)";
+ break;
+ case kRemoteControl_Switched:
+ buttonName = @"Remote Control Switched";
+ break;
+ default:
+ NSLog(@"Unmapped event for button %d", buttonIdentifier);
+ break;
+ }
+ //NSLog(@"Button %@ pressed %@", buttonName, pressed);
+ NSString* clickCountString = @"";
+ if (clickCount > 1) clickCountString = [NSString stringWithFormat: @"%d clicks", clickCount];
+ NSString* feedbackString = [NSString stringWithFormat:@"%@ %@ %@", buttonName, pressed, clickCountString];
+ [feedbackText setStringValue:feedbackString];
+
+ // delegate to view
+ [feedbackView remoteButton:buttonIdentifier pressedDown:pressedDown clickCount: clickCount];
+
+ // print out events
+ NSLog(@"%@", feedbackString);
+ if (pressedDown == NO) printf("\n");
+
+ // simulate slow processing of events
+ // [NSThread sleepUntilDate: [NSDate dateWithTimeIntervalSinceNow: 0.5]];
+}
+@end
+#endif
+
+@implementation MainController
+
+- (void) dealloc {
+ [remoteControl autorelease];
+ [remoteControlBehavior autorelease];
+ [super dealloc];
+}
+
+// for bindings access
+- (RemoteControl*) remoteControl {
+ return remoteControl;
+}
+
+- (MultiClickRemoteBehavior*) remoteBehavior {
+ return remoteControlBehavior;
+}
+
+#ifndef SAMPLE3
+#pragma mark -
+#pragma mark NSApplication Delegates
+- (void)applicationWillBecomeActive:(NSNotification *)aNotification {
+ NSLog(@"Application will become active - Using remote controls");
+ [remoteControl startListening: self];
+}
+- (void)applicationWillResignActive:(NSNotification *)aNotification {
+ NSLog(@"Application will resign active - Releasing remote controls");
+ [remoteControl stopListening: self];
+}
+#endif
+
+@end
90 MultiClickRemoteBehavior.h
@@ -0,0 +1,90 @@
+/*****************************************************************************
+ * MultiClickRemoteBehavior.h
+ * RemoteControlWrapper
+ *
+ * Created by Martin Kahr on 11.03.06 under a MIT-style license.
+ * Copyright (c) 2006 martinkahr.com. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ *****************************************************************************/
+
+
+#import <Cocoa/Cocoa.h>
+#import "RemoteControl.h"
+
+/**
+ A behavior that adds multiclick and hold events on top of a device.
+ Events are generated and send to a delegate
+ */
+@interface MultiClickRemoteBehavior : NSObject {
+ id delegate;
+
+ // state for simulating plus/minus hold
+ BOOL simulateHoldEvents;
+ BOOL lastEventSimulatedHold;
+ RemoteControlEventIdentifier lastHoldEvent;
+ NSTimeInterval lastHoldEventTime;
+
+ // state for multi click
+ unsigned int clickCountEnabledButtons;
+ NSTimeInterval maxClickTimeDifference;
+ NSTimeInterval lastClickCountEventTime;
+ RemoteControlEventIdentifier lastClickCountEvent;
+ unsigned int eventClickCount;
+}
+
+- (id) init;
+
+// Delegates are not retained
+- (void) setDelegate: (id) delegate;
+- (id) delegate;
+
+// Simulating hold events does deactivate sending of individual requests for pressed down/released.
+// Instead special hold events are being triggered when the user is pressing and holding a button for a small period.
+// Simulation is activated only for those buttons and remote control that do not have a seperate event already
+- (BOOL) simulateHoldEvent;
+- (void) setSimulateHoldEvent: (BOOL) value;
+
+// click counting makes it possible to recognize if the user has pressed a button repeatedly
+// click counting does delay each event as it has to wait if there is another event (second click)
+// therefore there is a slight time difference (maximumClickCountTimeDifference) between a single click
+// of the user and the call of your delegate method
+// click counting can be enabled individually for specific buttons. Use the property clickCountEnableButtons to
+// set the buttons for which click counting shall be enabled
+- (BOOL) clickCountingEnabled;
+- (void) setClickCountingEnabled: (BOOL) value;
+
+- (unsigned int) clickCountEnabledButtons;
+- (void) setClickCountEnabledButtons: (unsigned int)value;
+
+// the maximum time difference till which clicks are recognized as multi clicks
+- (NSTimeInterval) maximumClickCountTimeDifference;
+- (void) setMaximumClickCountTimeDifference: (NSTimeInterval) timeDiff;
+
+@end
+
+/*
+ * Method definitions for the delegate of the MultiClickRemoteBehavior class
+ */
+@interface NSObject(MultiClickRemoteBehaviorDelegate)
+
+- (void) remoteButton: (RemoteControlEventIdentifier)buttonIdentifier pressedDown: (BOOL) pressedDown clickCount: (unsigned int) count;
+
+@end
210 MultiClickRemoteBehavior.m
@@ -0,0 +1,210 @@
+/*****************************************************************************
+ * MultiClickRemoteBehavior.m
+ * RemoteControlWrapper
+ *
+ * Created by Martin Kahr on 11.03.06 under a MIT-style license.
+ * Copyright (c) 2006 martinkahr.com. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ *****************************************************************************/
+
+#import "MultiClickRemoteBehavior.h"
+
+const NSTimeInterval DEFAULT_MAXIMUM_CLICK_TIME_DIFFERENCE=0.35;
+const NSTimeInterval HOLD_RECOGNITION_TIME_INTERVAL=0.4;
+
+@implementation MultiClickRemoteBehavior
+
+- (id) init {
+ if (self = [super init]) {
+ maxClickTimeDifference = DEFAULT_MAXIMUM_CLICK_TIME_DIFFERENCE;
+ }
+ return self;
+}
+
+// Delegates are not retained!
+// http://developer.apple.com/documentation/Cocoa/Conceptual/CocoaFundamentals/CommunicatingWithObjects/chapter_6_section_4.html
+// Delegating objects do not (and should not) retain their delegates.
+// However, clients of delegating objects (applications, usually) are responsible for ensuring that their delegates are around
+// to receive delegation messages. To do this, they may have to retain the delegate.
+- (void) setDelegate: (id) _delegate {
+ if (_delegate && [_delegate respondsToSelector:@selector(remoteButton:pressedDown:clickCount:)]==NO) return;
+
+ delegate = _delegate;
+}
+- (id) delegate {
+ return delegate;
+}
+
+- (BOOL) simulateHoldEvent {
+ return simulateHoldEvents;
+}
+- (void) setSimulateHoldEvent: (BOOL) value {
+ simulateHoldEvents = value;
+}
+
+- (BOOL) simulatesHoldForButtonIdentifier: (RemoteControlEventIdentifier) identifier remoteControl: (RemoteControl*) remoteControl {
+ // we do that check only for the normal button identifiers as we would check for hold support for hold events instead
+ if (identifier > (1 << EVENT_TO_HOLD_EVENT_OFFSET)) return NO;
+
+ return [self simulateHoldEvent] && [remoteControl sendsEventForButtonIdentifier: (identifier << EVENT_TO_HOLD_EVENT_OFFSET)]==NO;
+}
+
+- (BOOL) clickCountingEnabled {
+ return clickCountEnabledButtons != 0;
+}
+- (void) setClickCountingEnabled: (BOOL) value {
+ if (value) {
+ [self setClickCountEnabledButtons: kRemoteButtonPlus | kRemoteButtonMinus | kRemoteButtonPlay | kRemoteButtonLeft | kRemoteButtonRight | kRemoteButtonMenu];
+ } else {
+ [self setClickCountEnabledButtons: 0];
+ }
+}
+
+- (unsigned int) clickCountEnabledButtons {
+ return clickCountEnabledButtons;
+}
+- (void) setClickCountEnabledButtons: (unsigned int)value {
+ clickCountEnabledButtons = value;
+}
+
+- (NSTimeInterval) maximumClickCountTimeDifference {
+ return maxClickTimeDifference;
+}
+- (void) setMaximumClickCountTimeDifference: (NSTimeInterval) timeDiff {
+ maxClickTimeDifference = timeDiff;
+}
+
+- (void) sendSimulatedHoldEvent: (id) time {
+ BOOL startSimulateHold = NO;
+ RemoteControlEventIdentifier event = lastHoldEvent;
+ @synchronized(self) {
+ startSimulateHold = (lastHoldEvent>0 && lastHoldEventTime == [time doubleValue]);
+ }
+ if (startSimulateHold) {
+ lastEventSimulatedHold = YES;
+ event = (event << EVENT_TO_HOLD_EVENT_OFFSET);
+ [delegate remoteButton:event pressedDown: YES clickCount: 1];
+ }
+}
+
+- (void) executeClickCountEvent: (NSArray*) values {
+ RemoteControlEventIdentifier event = [[values objectAtIndex: 0] unsignedIntValue];
+ NSTimeInterval eventTimePoint = [[values objectAtIndex: 1] doubleValue];
+
+ BOOL finishedClicking = NO;
+ int finalClickCount = eventClickCount;
+
+ @synchronized(self) {
+ finishedClicking = (event != lastClickCountEvent || eventTimePoint == lastClickCountEventTime);
+ if (finishedClicking) {
+ eventClickCount = 0;
+ lastClickCountEvent = 0;
+ lastClickCountEventTime = 0;
+ }
+ }
+
+ if (finishedClicking) {
+ [delegate remoteButton:event pressedDown: YES clickCount:finalClickCount];
+ // trigger a button release event, too
+ [NSThread sleepUntilDate: [NSDate dateWithTimeIntervalSinceNow:0.1]];
+ [delegate remoteButton:event pressedDown: NO clickCount:finalClickCount];
+ }
+}
+
+- (void) sendRemoteButtonEvent: (RemoteControlEventIdentifier) event pressedDown: (BOOL) pressedDown remoteControl: (RemoteControl*) remoteControl {
+ if (!delegate) return;
+
+ BOOL clickCountingForEvent = ([self clickCountEnabledButtons] & event) == event;
+
+ if ([self simulatesHoldForButtonIdentifier: event remoteControl: remoteControl] && lastClickCountEvent==0) {
+ if (pressedDown) {
+ // wait to see if it is a hold
+ lastHoldEvent = event;
+ lastHoldEventTime = [NSDate timeIntervalSinceReferenceDate];
+ [self performSelector:@selector(sendSimulatedHoldEvent:)
+ withObject:[NSNumber numberWithDouble:lastHoldEventTime]
+ afterDelay:HOLD_RECOGNITION_TIME_INTERVAL];
+ return;
+ } else {
+ if (lastEventSimulatedHold) {
+ // it was a hold
+ // send an event for "hold release"
+ event = (event << EVENT_TO_HOLD_EVENT_OFFSET);
+ lastHoldEvent = 0;
+ lastEventSimulatedHold = NO;
+
+ [delegate remoteButton:event pressedDown: pressedDown clickCount:1];
+ return;
+ } else {
+ RemoteControlEventIdentifier previousEvent = lastHoldEvent;
+ @synchronized(self) {
+ lastHoldEvent = 0;
+ }
+
+ // in case click counting is enabled we have to setup the state for that, too
+ if (clickCountingForEvent) {
+ lastClickCountEvent = previousEvent;
+ lastClickCountEventTime = lastHoldEventTime;
+ NSNumber* eventNumber;
+ NSNumber* timeNumber;
+ eventClickCount = 1;
+ timeNumber = [NSNumber numberWithDouble:lastClickCountEventTime];
+ eventNumber= [NSNumber numberWithUnsignedInt:previousEvent];
+ NSTimeInterval diffTime = maxClickTimeDifference-([NSDate timeIntervalSinceReferenceDate]-lastHoldEventTime);
+ [self performSelector: @selector(executeClickCountEvent:)
+ withObject: [NSArray arrayWithObjects:eventNumber, timeNumber, nil]
+ afterDelay: diffTime];
+ // we do not return here because we are still in the press-release event
+ // that will be consumed below
+ } else {
+ // trigger the pressed down event that we consumed first
+ [delegate remoteButton:event pressedDown: YES clickCount:1];
+ }
+ }
+ }
+ }
+
+ if (clickCountingForEvent) {
+ if (pressedDown == NO) return;
+
+ NSNumber* eventNumber;
+ NSNumber* timeNumber;
+ @synchronized(self) {
+ lastClickCountEventTime = [NSDate timeIntervalSinceReferenceDate];
+ if (lastClickCountEvent == event) {
+ eventClickCount = eventClickCount + 1;
+ } else {
+ eventClickCount = 1;
+ }
+ lastClickCountEvent = event;
+ timeNumber = [NSNumber numberWithDouble:lastClickCountEventTime];
+ eventNumber= [NSNumber numberWithUnsignedInt:event];
+ }
+ [self performSelector: @selector(executeClickCountEvent:)
+ withObject: [NSArray arrayWithObjects:eventNumber, timeNumber, nil]
+ afterDelay: maxClickTimeDifference];
+ } else {
+ [delegate remoteButton:event pressedDown: pressedDown clickCount:1];
+ }
+
+}
+
+@end
102 RemoteControl.h
@@ -0,0 +1,102 @@
+/*****************************************************************************
+ * RemoteControl.h
+ * RemoteControlWrapper
+ *
+ * Created by Martin Kahr on 11.03.06 under a MIT-style license.
+ * Copyright (c) 2006 martinkahr.com. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ *****************************************************************************/
+
+#import <Cocoa/Cocoa.h>
+
+// notifaction names that are being used to signal that an application wants to
+// have access to the remote control device or if the application has finished
+// using the remote control device
+extern NSString* REQUEST_FOR_REMOTE_CONTROL_NOTIFCATION;
+extern NSString* FINISHED_USING_REMOTE_CONTROL_NOTIFICATION;
+
+// keys used in user objects for distributed notifications
+extern NSString* kRemoteControlDeviceName;
+extern NSString* kApplicationIdentifier;
+extern NSString* kTargetApplicationIdentifier;
+
+// we have a 6 bit offset to make a hold event out of a normal event
+#define EVENT_TO_HOLD_EVENT_OFFSET 6
+
+@class RemoteControl;
+
+typedef enum _RemoteControlEventIdentifier {
+ // normal events
+ kRemoteButtonPlus =1<<1,
+ kRemoteButtonMinus =1<<2,
+ kRemoteButtonMenu =1<<3,
+ kRemoteButtonPlay =1<<4,
+ kRemoteButtonRight =1<<5,
+ kRemoteButtonLeft =1<<6,
+
+ // hold events
+ kRemoteButtonPlus_Hold =1<<7,
+ kRemoteButtonMinus_Hold =1<<8,
+ kRemoteButtonMenu_Hold =1<<9,
+ kRemoteButtonPlay_Hold =1<<10,
+ kRemoteButtonRight_Hold =1<<11,
+ kRemoteButtonLeft_Hold =1<<12,
+
+ // special events (not supported by all devices)
+ kRemoteControl_Switched =1<<13,
+} RemoteControlEventIdentifier;
+
+@interface NSObject(RemoteControlDelegate)
+
+- (void) sendRemoteButtonEvent: (RemoteControlEventIdentifier) event pressedDown: (BOOL) pressedDown remoteControl: (RemoteControl*) remoteControl;
+
+@end
+
+/*
+ Base Interface for Remote Control devices
+*/
+@interface RemoteControl : NSObject {
+ id delegate;
+}
+
+// returns nil if the remote control device is not available
+- (id) initWithDelegate: (id) remoteControlDelegate;
+
+- (void) setListeningToRemote: (BOOL) value;
+- (BOOL) isListeningToRemote;
+
+- (BOOL) isOpenInExclusiveMode;
+- (void) setOpenInExclusiveMode: (BOOL) value;
+
+- (IBAction) startListening: (id) sender;
+- (IBAction) stopListening: (id) sender;
+
+// is this remote control sending the given event?
+- (BOOL) sendsEventForButtonIdentifier: (RemoteControlEventIdentifier) identifier;
+
+// sending of notifications between applications
++ (void) sendFinishedNotifcationForAppIdentifier: (NSString*) identifier;
++ (void) sendRequestForRemoteControlNotification;
+
+// name of the device
++ (const char*) remoteControlDeviceName;
+
+@end
103 RemoteControl.m
@@ -0,0 +1,103 @@
+/*****************************************************************************
+ * RemoteControl.m
+ * RemoteControlWrapper
+ *
+ * Created by Martin Kahr on 11.03.06 under a MIT-style license.
+ * Copyright (c) 2006 martinkahr.com. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ *****************************************************************************/
+
+#import "RemoteControl.h"
+
+// notifaction names that are being used to signal that an application wants to
+// have access to the remote control device or if the application has finished
+// using the remote control device
+NSString* REQUEST_FOR_REMOTE_CONTROL_NOTIFCATION = @"mac.remotecontrols.RequestForRemoteControl";
+NSString* FINISHED_USING_REMOTE_CONTROL_NOTIFICATION = @"mac.remotecontrols.FinishedUsingRemoteControl";
+
+// keys used in user objects for distributed notifications
+NSString* kRemoteControlDeviceName = @"RemoteControlDeviceName";
+NSString* kApplicationIdentifier = @"CFBundleIdentifier";
+// bundle identifier of the application that should get access to the remote control
+// this key is being used in the FINISHED notification only
+NSString* kTargetApplicationIdentifier = @"TargetBundleIdentifier";
+
+
+@implementation RemoteControl
+
+// returns nil if the remote control device is not available
+- (id) initWithDelegate: (id) _remoteControlDelegate {
+ if (self = [super init]) {
+ delegate = [_remoteControlDelegate retain];
+ }
+ return self;
+}
+
+- (void) dealloc {
+ [delegate release];
+ [super dealloc];
+}
+
+- (void) setListeningToRemote: (BOOL) value {
+}
+- (BOOL) isListeningToRemote {
+ return NO;
+}
+
+- (IBAction) startListening: (id) sender {
+}
+- (IBAction) stopListening: (id) sender {
+
+}
+
+- (BOOL) isOpenInExclusiveMode {
+ return YES;
+}
+- (void) setOpenInExclusiveMode: (BOOL) value {
+}
+
+- (BOOL) sendsEventForButtonIdentifier: (RemoteControlEventIdentifier) identifier {
+ return YES;
+}
+
++ (void) sendDistributedNotification: (NSString*) notificationName targetBundleIdentifier: (NSString*) targetIdentifier {
+ NSDictionary* userInfo = [NSDictionary dictionaryWithObjectsAndKeys: [NSString stringWithCString:[self remoteControlDeviceName] encoding:NSASCIIStringEncoding],
+ kRemoteControlDeviceName, [[NSBundle mainBundle] bundleIdentifier], kApplicationIdentifier,
+ targetIdentifier, kTargetApplicationIdentifier, nil];
+
+ [[NSDistributedNotificationCenter defaultCenter] postNotificationName:notificationName
+ object:nil
+ userInfo:userInfo
+ deliverImmediately:YES];
+}
+
++ (void) sendFinishedNotifcationForAppIdentifier: (NSString*) identifier {
+ [self sendDistributedNotification:FINISHED_USING_REMOTE_CONTROL_NOTIFICATION targetBundleIdentifier:identifier];
+}
++ (void) sendRequestForRemoteControlNotification {
+ [self sendDistributedNotification:REQUEST_FOR_REMOTE_CONTROL_NOTIFCATION targetBundleIdentifier:nil];
+}
+
++ (const char*) remoteControlDeviceName {
+ return NULL;
+}
+
+@end
954 RemoteControlClasses.graffle
@@ -0,0 +1,954 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>ActiveLayerIndex</key>
+ <integer>0</integer>
+ <key>AutoAdjust</key>
+ <true/>
+ <key>CanvasColor</key>
+ <dict>
+ <key>w</key>
+ <string>1</string>
+ </dict>
+ <key>CanvasOrigin</key>
+ <string>{0, 0}</string>
+ <key>CanvasScale</key>
+ <real>1</real>
+ <key>ColumnAlign</key>
+ <integer>1</integer>
+ <key>ColumnSpacing</key>
+ <real>36</real>
+ <key>CreationDate</key>
+ <string>2007-07-04 18:42:14 +0200</string>
+ <key>Creator</key>
+ <string>Martin Kahr</string>
+ <key>DisplayScale</key>
+ <string>1 cm = 1 cm</string>
+ <key>GraphDocumentVersion</key>
+ <integer>5</integer>
+ <key>GraphicsList</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{307, 81.9732}, {50, 14}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>YES</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>w</key>
+ <string>0</string>
+ </dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>ID</key>
+ <integer>64</integer>
+ <key>Line</key>
+ <dict>
+ <key>ID</key>
+ <integer>63</integer>
+ <key>Position</key>
+ <real>0.43165460228919983</real>
+ <key>RotationType</key>
+ <integer>0</integer>
+ </dict>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf909\cocoasubrtf800
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
+
+\f0\fs24 \cf0 informs}</string>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>36</integer>