Commit 92c36210 authored by Felix Paul Kühne's avatar Felix Paul Kühne

* various improvements to the AppleRemote support by Martin Kahr <martin...

* various improvements to the AppleRemote support by Martin Kahr <martin -attt-> martinkahr com> feat. the following changes:
    - holding +/- continuously increases/decreases volume
    - pressing Play twice toggles fullscreen mode
    -  a press on Menu shows the "Position" overlay (like pressing the key
't')

This updates our copy of Martin's unofficial framework to the latest version released.
parent b8e3c1ea
......@@ -61,38 +61,51 @@
enum AppleRemoteEventIdentifier
{
kRemoteButtonVolume_Plus=0,
kRemoteButtonVolume_Minus,
kRemoteButtonMenu,
kRemoteButtonPlay,
kRemoteButtonRight,
kRemoteButtonLeft,
kRemoteButtonRight_Hold,
kRemoteButtonLeft_Hold,
kRemoteButtonMenu_Hold,
kRemoteButtonPlay_Sleep,
kRemoteControl_Switched
kRemoteButtonVolume_Plus =1<<1,
kRemoteButtonVolume_Minus =1<<2,
kRemoteButtonMenu =1<<3,
kRemoteButtonPlay =1<<4,
kRemoteButtonRight =1<<5,
kRemoteButtonLeft =1<<6,
kRemoteButtonRight_Hold =1<<7,
kRemoteButtonLeft_Hold =1<<8,
kRemoteButtonMenu_Hold =1<<9,
kRemoteButtonPlay_Sleep =1<<10,
kRemoteControl_Switched =1<<11,
kRemoteButtonVolume_Plus_Hold =1<<12,
kRemoteButtonVolume_Minus_Hold =1<<13
};
typedef enum AppleRemoteEventIdentifier AppleRemoteEventIdentifier;
/* Encapsulates usage of the apple remote control
This class is implemented as a singleton as there is exactly one remote per machine (until now)
The class is not thread safe
/* Encapsulates usage of the apple remote control
This class is implemented as a singleton as there is exactly one remote per machine (until now)
The class is not thread safe
*/
@interface AppleRemote : NSObject {
IOHIDDeviceInterface** hidDeviceInterface;
IOHIDQueueInterface** queue;
NSMutableArray* allCookies;
NSMutableDictionary* cookieToButtonMapping;
BOOL openInExclusiveMode;
int remoteId;
IBOutlet id delegate;
IOHIDDeviceInterface** hidDeviceInterface;
IOHIDQueueInterface** queue;
NSMutableArray* allCookies;
NSMutableDictionary* cookieToButtonMapping;
BOOL openInExclusiveMode;
BOOL simulatePlusMinusHold;
BOOL processesBacklog;
/* state for simulating plus/minus hold */
BOOL lastEventSimulatedHold;
AppleRemoteEventIdentifier lastPlusMinusEvent;
NSTimeInterval lastPlusMinusEventTime;
int remoteId;
unsigned int clickCountEnabledButtons;
NSTimeInterval maxClickTimeDifference;
NSTimeInterval lastClickCountEventTime;
AppleRemoteEventIdentifier lastClickCountEvent;
unsigned int eventClickCount;
IBOutlet id delegate;
}
- (void) setRemoteId: (int) aValue;
- (int) remoteId;
- (BOOL) isRemoteAvailable;
......@@ -103,6 +116,42 @@ typedef enum AppleRemoteEventIdentifier AppleRemoteEventIdentifier;
- (BOOL) isOpenInExclusiveMode;
- (void) setOpenInExclusiveMode: (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;
/* 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;
/* Sets an NSApplication delegate which starts listening when application is becoming active
* and stops listening when application resigns being active.
* If an NSApplication delegate has been already set all method calls will be forwarded to this delegate, too. */
- (BOOL) listeningOnAppActivate;
- (void) setListeningOnAppActivate: (BOOL) value;
/* Simulating plus/minus hold does deactivate sending of individual requests for plus/minus pressed down/released.
* Instead special hold events are being triggered when the user is pressing and holding plus/minus for a small period.
* With simulating enabled the plus/minus buttons do behave as the left/right buttons */
- (BOOL) simulatesPlusMinusHold;
- (void) setSimulatesPlusMinusHold: (BOOL) value;
/* Delegates are not retained */
- (void) setDelegate: (id) delegate;
- (id) delegate;
......@@ -116,15 +165,15 @@ typedef enum AppleRemoteEventIdentifier AppleRemoteEventIdentifier;
@end
/* Method definitions for the delegate of the AppleRemote class
*/
/* Method definitions for the delegate of the AppleRemote class */
@interface NSObject(NSAppleRemoteDelegate)
- (void) appleRemoteButton: (AppleRemoteEventIdentifier)buttonIdentifier pressedDown: (BOOL) pressedDown;
- (void) appleRemoteButton: (AppleRemoteEventIdentifier)buttonIdentifier pressedDown: (BOOL) pressedDown clickCount: (unsigned int) count;
@end
@interface AppleRemote (PrivateMethods)
- (void) setRemoteId: (int) aValue;
- (NSDictionary*) cookieToButtonMapping;
- (IOHIDQueueInterface**) queue;
- (IOHIDDeviceInterface**) hidDeviceInterface;
......@@ -136,4 +185,15 @@ typedef enum AppleRemoteEventIdentifier AppleRemoteEventIdentifier;
- (IOHIDDeviceInterface**) createInterfaceForDevice: (io_object_t) hidDevice;
- (BOOL) initializeCookies;
- (BOOL) openDevice;
@end
/* A NSApplication delegate which is used to activate and deactivate listening to the remote control
* dependent on the activation state of your application.
* All events are delegated to the original NSApplication delegate if necessary */
@interface AppleRemoteApplicationDelegate : NSObject {
id applicationDelegate;
}
- (id) initWithApplicationDelegate: (id) delegate;
- (id) applicationDelegate;
@end
\ No newline at end of file
......@@ -55,141 +55,204 @@
const char* AppleRemoteDeviceName = "AppleIRController";
const int REMOTE_SWITCH_COOKIE=19;
const NSTimeInterval DEFAULT_MAXIMUM_CLICK_TIME_DIFFERENCE=0.35;
const NSTimeInterval HOLD_RECOGNITION_TIME_INTERVAL=0.4;
@implementation AppleRemote
- (id) init
{
self = [super init];
if ( self == [super init] ) {
openInExclusiveMode = YES;
queue = NULL;
hidDeviceInterface = NULL;
cookieToButtonMapping = [[NSMutableDictionary alloc] init];
[cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonVolume_Plus] forKey:@"14_12_11_6_5_"];
[cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonVolume_Minus] forKey:@"14_13_11_6_5_"];
[cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMenu] forKey:@"14_7_6_5_14_7_6_5_"];
[cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlay] forKey:@"14_8_6_5_14_8_6_5_"];
[cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonRight] forKey:@"14_9_6_5_14_9_6_5_"];
[cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonLeft] forKey:@"14_10_6_5_14_10_6_5_"];
[cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonRight_Hold] forKey:@"14_6_5_4_2_"];
[cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonLeft_Hold] forKey:@"14_6_5_3_2_"];
[cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMenu_Hold] forKey:@"14_6_5_14_6_5_"];
[cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlay_Sleep] forKey:@"18_14_6_5_18_14_6_5_"];
[cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteControl_Switched] forKey:@"19_"];
}
return self;
#pragma public interface
- (id) init {
if ( self = [super init] ) {
openInExclusiveMode = YES;
queue = NULL;
hidDeviceInterface = NULL;
cookieToButtonMapping = [[NSMutableDictionary alloc] init];
[cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonVolume_Plus] forKey:@"14_12_11_6_"];
[cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonVolume_Minus] 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_Sleep] forKey:@"18_14_6_18_14_6_"];
[cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteControl_Switched] forKey:@"19_"];
/* defaults */
[self setSimulatesPlusMinusHold: YES];
maxClickTimeDifference = DEFAULT_MAXIMUM_CLICK_TIME_DIFFERENCE;
}
return self;
}
- (void) dealloc {
[self stopListening:self];
[cookieToButtonMapping release];
[super dealloc];
[self stopListening:self];
[cookieToButtonMapping release];
[super dealloc];
}
- (void) setRemoteId: (int) value {
remoteId = value;
}
- (int) remoteId {
return remoteId;
return remoteId;
}
- (BOOL) isRemoteAvailable {
io_object_t hidDevice = [self findAppleRemoteDevice];
if (hidDevice != 0) {
IOObjectRelease(hidDevice);
return YES;
} else {
return NO;
}
- (BOOL) isRemoteAvailable {
io_object_t hidDevice = [self findAppleRemoteDevice];
if (hidDevice != 0) {
IOObjectRelease(hidDevice);
return YES;
} else {
return NO;
}
}
- (BOOL) isListeningToRemote {
return (hidDeviceInterface != NULL && allCookies != NULL && queue != NULL);
return (hidDeviceInterface != NULL && allCookies != NULL && queue != NULL);
}
- (void) setListeningToRemote: (BOOL) value {
if (value == NO) {
[self stopListening:self];
} else {
[self startListening:self];
}
if (value == NO) {
[self stopListening:self];
} else {
[self startListening: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 respondsToSelector:@selector(appleRemoteButton:pressedDown:)]==NO) return;
[_delegate retain];
[delegate release];
delegate = _delegate;
if (_delegate && [_delegate respondsToSelector:@selector(appleRemoteButton:pressedDown:clickCount:)]==NO) return;
delegate = _delegate;
}
- (id) delegate {
return delegate;
return delegate;
}
- (BOOL) isOpenInExclusiveMode {
return openInExclusiveMode;
return openInExclusiveMode;
}
- (void) setOpenInExclusiveMode: (BOOL) value {
openInExclusiveMode = value;
}
- (IBAction) startListening: (id) sender {
if ([self isListeningToRemote]) return;
io_object_t hidDevice = [self findAppleRemoteDevice];
if (hidDevice == 0) return;
if ([self createInterfaceForDevice:hidDevice] == NULL) {
goto error;
}
if ([self initializeCookies]==NO) {
goto error;
}
if ([self openDevice]==NO) {
goto error;
}
goto cleanup;
openInExclusiveMode = value;
}
- (BOOL) clickCountingEnabled {
return clickCountEnabledButtons != 0;
}
- (void) setClickCountingEnabled: (BOOL) value {
if (value) {
[self setClickCountEnabledButtons: kRemoteButtonVolume_Plus | kRemoteButtonVolume_Minus | 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;
}
- (BOOL) processesBacklog {
return processesBacklog;
}
- (void) setProcessesBacklog: (BOOL) value {
processesBacklog = value;
}
- (BOOL) listeningOnAppActivate {
id appDelegate = [NSApp delegate];
return (appDelegate!=nil && [appDelegate isKindOfClass: [AppleRemoteApplicationDelegate class]]);
}
- (void) setListeningOnAppActivate: (BOOL) value {
if (value) {
if ([self listeningOnAppActivate]) return;
AppleRemoteApplicationDelegate* appDelegate = [[AppleRemoteApplicationDelegate alloc] initWithApplicationDelegate: [NSApp delegate]];
/* NSApp does not retain its delegate therefore we keep retain count on 1 */
[NSApp setDelegate: appDelegate];
} else {
if ([self listeningOnAppActivate]==NO) return;
AppleRemoteApplicationDelegate* appDelegate = (AppleRemoteApplicationDelegate*)[NSApp delegate];
id previousAppDelegate = [appDelegate applicationDelegate];
[NSApp setDelegate: previousAppDelegate];
[appDelegate release];
}
}
- (BOOL) simulatesPlusMinusHold {
return simulatePlusMinusHold;
}
- (void) setSimulatesPlusMinusHold: (BOOL) value {
simulatePlusMinusHold = value;
}
- (IBAction) startListening: (id) sender {
if ([self isListeningToRemote]) return;
io_object_t hidDevice = [self findAppleRemoteDevice];
if (hidDevice == 0) return;
if ([self createInterfaceForDevice:hidDevice] == NULL) {
goto error;
}
if ([self initializeCookies]==NO) {
goto error;
}
if ([self openDevice]==NO) {
goto error;
}
goto cleanup;
error:
[self stopListening:self];
cleanup:
IOObjectRelease(hidDevice);
[self stopListening:self];
cleanup:
IOObjectRelease(hidDevice);
}
- (IBAction) stopListening: (id) sender {
if (queue != NULL) {
(*queue)->stop(queue);
//dispose of queue
(*queue)->dispose(queue);
//release the queue we allocated
(*queue)->Release(queue);
queue = NULL;
}
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 (queue != NULL) {
(*queue)->stop(queue);
//dispose of queue
(*queue)->dispose(queue);
//release the queue we allocated
(*queue)->Release(queue);
queue = NULL;
}
if (allCookies != nil) {
[allCookies autorelease];
allCookies = nil;
}
if (hidDeviceInterface != NULL) {
//close the device
(*hidDeviceInterface)->close(hidDeviceInterface);
//release the interface
(*hidDeviceInterface)->Release(hidDeviceInterface);
hidDeviceInterface = NULL;
}
}
@end
......@@ -198,20 +261,20 @@ cleanup:
static AppleRemote* sharedInstance=nil;
+ (AppleRemote*) sharedRemote {
@synchronized(self) {
+ (AppleRemote*) sharedRemote {
@synchronized(self) {
if (sharedInstance == nil) {
sharedInstance = [[self alloc] init];
}
}
return sharedInstance;
return sharedInstance;
}
+ (id)allocWithZone:(NSZone *)zone {
@synchronized(self) {
if (sharedInstance == nil) {
return [super allocWithZone:zone];
}
}
}
return sharedInstance;
}
- (id)copyWithZone:(NSZone *)zone {
......@@ -234,217 +297,408 @@ static AppleRemote* sharedInstance=nil;
@implementation AppleRemote (PrivateMethods)
- (void) setRemoteId: (int) value {
remoteId = value;
}
- (IOHIDQueueInterface**) queue {
return queue;
return queue;
}
- (IOHIDDeviceInterface**) hidDeviceInterface {
return hidDeviceInterface;
return hidDeviceInterface;
}
- (NSDictionary*) cookieToButtonMapping {
return 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) sendSimulatedPlusMinusEvent: (id) time {
BOOL startSimulateHold = NO;
AppleRemoteEventIdentifier event = lastPlusMinusEvent;
@synchronized(self) {
startSimulateHold = (lastPlusMinusEvent>0 && lastPlusMinusEventTime == [time doubleValue]);
}
if (startSimulateHold) {
lastEventSimulatedHold = YES;
event = (event==kRemoteButtonVolume_Plus) ? kRemoteButtonVolume_Plus_Hold : kRemoteButtonVolume_Minus_Hold;
[delegate appleRemoteButton:event pressedDown: YES clickCount: 1];
}
}
- (void) sendRemoteButtonEvent: (AppleRemoteEventIdentifier) event pressedDown: (BOOL) pressedDown {
if (delegate) {
if (simulatePlusMinusHold) {
if (event == kRemoteButtonVolume_Plus || event == kRemoteButtonVolume_Minus) {
if (pressedDown) {
lastPlusMinusEvent = event;
lastPlusMinusEventTime = [NSDate timeIntervalSinceReferenceDate];
[self performSelector:@selector(sendSimulatedPlusMinusEvent:)
withObject:[NSNumber numberWithDouble:lastPlusMinusEventTime]
afterDelay:HOLD_RECOGNITION_TIME_INTERVAL];
return;
} else {
if (lastEventSimulatedHold) {
event = (event==kRemoteButtonVolume_Plus) ? kRemoteButtonVolume_Plus_Hold : kRemoteButtonVolume_Minus_Hold;
lastPlusMinusEvent = 0;
lastEventSimulatedHold = NO;
} else {
@synchronized(self) {
lastPlusMinusEvent = 0;
}
pressedDown = YES;
}
}
}
}
if (([self clickCountEnabledButtons] & event) == event) {
if (pressedDown==NO && (event == kRemoteButtonVolume_Minus || event == kRemoteButtonVolume_Plus)) {
return; // this one is triggered automatically by the handler
}
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 appleRemoteButton:event pressedDown: pressedDown clickCount:1];
}
}
}
- (void) executeClickCountEvent: (NSArray*) values {
AppleRemoteEventIdentifier 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;
}
if (finishedClicking) {
[delegate appleRemoteButton:event pressedDown: YES clickCount:finalClickCount];
if ([self simulatesPlusMinusHold]==NO && (event == kRemoteButtonVolume_Minus || event == kRemoteButtonVolume_Plus)) {
// trigger a button release event, too
[NSThread sleepUntilDate: [NSDate dateWithTimeIntervalSinceNow:0.1]];
[delegate appleRemoteButton:event pressedDown: NO clickCount:finalClickCount];