1151 lines
43 KiB
Plaintext
1151 lines
43 KiB
Plaintext
/*
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
#import "config.h"
|
|
#import "HIDEventGenerator.h"
|
|
|
|
#import "GeneratedTouchesDebugWindow.h"
|
|
#import "UIKitSPI.h"
|
|
#import <mach/mach_time.h>
|
|
#import <pal/spi/cocoa/IOKitSPI.h>
|
|
#import <wtf/Assertions.h>
|
|
#import <wtf/BlockPtr.h>
|
|
#import <wtf/SoftLinking.h>
|
|
|
|
SOFT_LINK_PRIVATE_FRAMEWORK(BackBoardServices)
|
|
SOFT_LINK(BackBoardServices, BKSHIDEventSetDigitizerInfo, void, (IOHIDEventRef digitizerEvent, uint32_t contextID, uint8_t systemGestureisPossible, uint8_t isSystemGestureStateChangeEvent, CFStringRef displayUUID, CFTimeInterval initialTouchTimestamp, float maxForce), (digitizerEvent, contextID, systemGestureisPossible, isSystemGestureStateChangeEvent, displayUUID, initialTouchTimestamp, maxForce));
|
|
|
|
NSString* const TopLevelEventInfoKey = @"events";
|
|
NSString* const HIDEventInputType = @"inputType";
|
|
NSString* const HIDEventTimeOffsetKey = @"timeOffset";
|
|
NSString* const HIDEventTouchesKey = @"touches";
|
|
NSString* const HIDEventPhaseKey = @"phase";
|
|
NSString* const HIDEventInterpolateKey = @"interpolate";
|
|
NSString* const HIDEventTimestepKey = @"timestep";
|
|
NSString* const HIDEventCoordinateSpaceKey = @"coordinateSpace";
|
|
NSString* const HIDEventStartEventKey = @"startEvent";
|
|
NSString* const HIDEventEndEventKey = @"endEvent";
|
|
NSString* const HIDEventTouchIDKey = @"id";
|
|
NSString* const HIDEventPressureKey = @"pressure";
|
|
NSString* const HIDEventXKey = @"x";
|
|
NSString* const HIDEventYKey = @"y";
|
|
NSString* const HIDEventTwistKey = @"twist";
|
|
NSString* const HIDEventMajorRadiusKey = @"majorRadius";
|
|
NSString* const HIDEventMinorRadiusKey = @"minorRadius";
|
|
|
|
NSString* const HIDEventInputTypeHand = @"hand";
|
|
NSString* const HIDEventInputTypeFinger = @"finger";
|
|
NSString* const HIDEventInputTypeStylus = @"stylus";
|
|
|
|
NSString* const HIDEventCoordinateSpaceTypeGlobal = @"global";
|
|
NSString* const HIDEventCoordinateSpaceTypeContent = @"content";
|
|
|
|
NSString* const HIDEventInterpolationTypeLinear = @"linear";
|
|
NSString* const HIDEventInterpolationTypeSimpleCurve = @"simpleCurve";
|
|
|
|
NSString* const HIDEventPhaseBegan = @"began";
|
|
NSString* const HIDEventPhaseStationary = @"stationary";
|
|
NSString* const HIDEventPhaseMoved = @"moved";
|
|
NSString* const HIDEventPhaseEnded = @"ended";
|
|
NSString* const HIDEventPhaseCanceled = @"canceled";
|
|
|
|
static const NSTimeInterval fingerLiftDelay = 0.05;
|
|
static const NSTimeInterval multiTapInterval = 0.15;
|
|
static const NSTimeInterval fingerMoveInterval = 0.016;
|
|
static const NSTimeInterval longPressHoldDelay = 2.0;
|
|
static const IOHIDFloat defaultMajorRadius = 5;
|
|
static const IOHIDFloat defaultPathPressure = 0;
|
|
static const long nanosecondsPerSecond = 1e9;
|
|
|
|
NSUInteger const HIDMaxTouchCount = 5;
|
|
|
|
|
|
|
|
static int fingerIdentifiers[HIDMaxTouchCount] = { 2, 3, 4, 5, 1 };
|
|
|
|
typedef enum {
|
|
InterpolationTypeLinear,
|
|
InterpolationTypeSimpleCurve,
|
|
} InterpolationType;
|
|
|
|
typedef enum {
|
|
HandEventNull,
|
|
HandEventTouched,
|
|
HandEventMoved,
|
|
HandEventChordChanged,
|
|
HandEventLifted,
|
|
HandEventCanceled,
|
|
StylusEventTouched,
|
|
StylusEventMoved,
|
|
StylusEventLifted,
|
|
} HandEventType;
|
|
|
|
typedef struct {
|
|
int identifier;
|
|
CGPoint point;
|
|
IOHIDFloat pathMajorRadius;
|
|
IOHIDFloat pathPressure;
|
|
UInt8 pathProximity;
|
|
BOOL isStylus;
|
|
IOHIDFloat azimuthAngle;
|
|
IOHIDFloat altitudeAngle;
|
|
} SyntheticEventDigitizerInfo;
|
|
|
|
static CFTimeInterval secondsSinceAbsoluteTime(CFAbsoluteTime startTime)
|
|
{
|
|
return (CFAbsoluteTimeGetCurrent() - startTime);
|
|
}
|
|
|
|
static double linearInterpolation(double a, double b, double t)
|
|
{
|
|
return (a + (b - a) * t );
|
|
}
|
|
|
|
static double simpleCurveInterpolation(double a, double b, double t)
|
|
{
|
|
return (a + (b - a) * sin(sin(t * M_PI / 2) * t * M_PI / 2));
|
|
}
|
|
|
|
|
|
static CGPoint calculateNextCurveLocation(CGPoint a, CGPoint b, CFTimeInterval t)
|
|
{
|
|
return CGPointMake(simpleCurveInterpolation(a.x, b.x, t), simpleCurveInterpolation(a.y, b.y, t));
|
|
}
|
|
|
|
typedef double(*pressureInterpolationFunction)(double, double, CFTimeInterval);
|
|
static pressureInterpolationFunction interpolations[] = {
|
|
linearInterpolation,
|
|
simpleCurveInterpolation,
|
|
};
|
|
|
|
static void delayBetweenMove(int eventIndex, double elapsed)
|
|
{
|
|
// Delay next event until expected elapsed time.
|
|
double delay = (eventIndex * fingerMoveInterval) - elapsed;
|
|
if (delay > 0) {
|
|
struct timespec moveDelay = { 0, static_cast<long>(delay * nanosecondsPerSecond) };
|
|
nanosleep(&moveDelay, NULL);
|
|
}
|
|
}
|
|
|
|
@implementation HIDEventGenerator {
|
|
IOHIDEventSystemClientRef _ioSystemClient;
|
|
SyntheticEventDigitizerInfo _activePoints[HIDMaxTouchCount];
|
|
NSUInteger _activePointCount;
|
|
RetainPtr<NSMutableDictionary> _eventCallbacks;
|
|
}
|
|
|
|
+ (HIDEventGenerator *)sharedHIDEventGenerator
|
|
{
|
|
static NeverDestroyed<RetainPtr<HIDEventGenerator>> eventGenerator = adoptNS([[HIDEventGenerator alloc] init]);
|
|
return eventGenerator.get().get();
|
|
}
|
|
|
|
+ (CFIndex)nextEventCallbackID
|
|
{
|
|
static CFIndex callbackID = 0;
|
|
return ++callbackID;
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
self = [super init];
|
|
if (!self)
|
|
return nil;
|
|
|
|
for (NSUInteger i = 0; i < HIDMaxTouchCount; ++i)
|
|
_activePoints[i].identifier = fingerIdentifiers[i];
|
|
|
|
_eventCallbacks = adoptNS([[NSMutableDictionary alloc] init]);
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)_sendIOHIDKeyboardEvent:(uint64_t)timestamp usage:(uint32_t)usage isKeyDown:(bool)isKeyDown
|
|
{
|
|
auto eventRef = adoptCF(IOHIDEventCreateKeyboardEvent(kCFAllocatorDefault,
|
|
timestamp,
|
|
kHIDPage_KeyboardOrKeypad,
|
|
usage,
|
|
isKeyDown,
|
|
kIOHIDEventOptionNone));
|
|
[self _sendHIDEvent:eventRef.get()];
|
|
}
|
|
|
|
static IOHIDDigitizerTransducerType transducerTypeFromString(NSString * transducerTypeString)
|
|
{
|
|
if ([transducerTypeString isEqualToString:HIDEventInputTypeHand])
|
|
return kIOHIDDigitizerTransducerTypeHand;
|
|
|
|
if ([transducerTypeString isEqualToString:HIDEventInputTypeFinger])
|
|
return kIOHIDDigitizerTransducerTypeFinger;
|
|
|
|
if ([transducerTypeString isEqualToString:HIDEventInputTypeStylus])
|
|
return kIOHIDDigitizerTransducerTypeStylus;
|
|
|
|
ASSERT_NOT_REACHED();
|
|
return 0;
|
|
}
|
|
|
|
static UITouchPhase phaseFromString(NSString *string)
|
|
{
|
|
if ([string isEqualToString:HIDEventPhaseBegan])
|
|
return UITouchPhaseBegan;
|
|
|
|
if ([string isEqualToString:HIDEventPhaseStationary])
|
|
return UITouchPhaseStationary;
|
|
|
|
if ([string isEqualToString:HIDEventPhaseMoved])
|
|
return UITouchPhaseMoved;
|
|
|
|
if ([string isEqualToString:HIDEventPhaseEnded])
|
|
return UITouchPhaseEnded;
|
|
|
|
if ([string isEqualToString:HIDEventPhaseCanceled])
|
|
return UITouchPhaseCancelled;
|
|
|
|
return UITouchPhaseStationary;
|
|
}
|
|
|
|
static InterpolationType interpolationFromString(NSString *string)
|
|
{
|
|
if ([string isEqualToString:HIDEventInterpolationTypeLinear])
|
|
return InterpolationTypeLinear;
|
|
|
|
if ([string isEqualToString:HIDEventInterpolationTypeSimpleCurve])
|
|
return InterpolationTypeSimpleCurve;
|
|
|
|
return InterpolationTypeLinear;
|
|
}
|
|
|
|
- (IOHIDDigitizerEventMask)eventMaskFromEventInfo:(NSDictionary *)info
|
|
{
|
|
IOHIDDigitizerEventMask eventMask = 0;
|
|
NSArray *childEvents = info[HIDEventTouchesKey];
|
|
for (NSDictionary *touchInfo in childEvents) {
|
|
UITouchPhase phase = phaseFromString(touchInfo[HIDEventPhaseKey]);
|
|
// If there are any new or ended events, mask includes touch.
|
|
if (phase == UITouchPhaseBegan || phase == UITouchPhaseEnded || phase == UITouchPhaseCancelled)
|
|
eventMask |= kIOHIDDigitizerEventTouch;
|
|
// If there are any pressure readings, set mask must include attribute
|
|
if ([touchInfo[HIDEventPressureKey] floatValue])
|
|
eventMask |= kIOHIDDigitizerEventAttribute;
|
|
}
|
|
|
|
return eventMask;
|
|
}
|
|
|
|
// Returns 1 for all events where the fingers are on the glass (everything but ended and canceled).
|
|
- (CFIndex)touchFromEventInfo:(NSDictionary *)info
|
|
{
|
|
NSArray *childEvents = info[HIDEventTouchesKey];
|
|
for (NSDictionary *touchInfo in childEvents) {
|
|
UITouchPhase phase = phaseFromString(touchInfo[HIDEventPhaseKey]);
|
|
if (phase == UITouchPhaseBegan || phase == UITouchPhaseMoved || phase == UITouchPhaseStationary)
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// FIXME: callers of _createIOHIDEventType could switch to this.
|
|
- (IOHIDEventRef)_createIOHIDEventWithInfo:(NSDictionary *)info
|
|
{
|
|
uint64_t machTime = mach_absolute_time();
|
|
|
|
IOHIDDigitizerEventMask eventMask = [self eventMaskFromEventInfo:info];
|
|
|
|
CFIndex range = 0;
|
|
// touch is 1 if a finger is down.
|
|
CFIndex touch = [self touchFromEventInfo:info];
|
|
|
|
IOHIDEventRef eventRef = IOHIDEventCreateDigitizerEvent(kCFAllocatorDefault, machTime,
|
|
transducerTypeFromString(info[HIDEventInputType]), // transducerType
|
|
0, // index
|
|
0, // identifier
|
|
eventMask, // event mask
|
|
0, // button event
|
|
0, // x
|
|
0, // y
|
|
0, // z
|
|
0, // presure
|
|
0, // twist
|
|
range, // range
|
|
touch, // touch
|
|
kIOHIDEventOptionNone);
|
|
|
|
IOHIDEventSetIntegerValue(eventRef, kIOHIDEventFieldDigitizerIsDisplayIntegrated, 1);
|
|
|
|
NSArray *childEvents = info[HIDEventTouchesKey];
|
|
for (NSDictionary *touchInfo in childEvents) {
|
|
|
|
[[GeneratedTouchesDebugWindow sharedGeneratedTouchesDebugWindow] updateDebugIndicatorForTouch:[touchInfo[HIDEventTouchIDKey] intValue] withPointInWindowCoordinates:CGPointMake([touchInfo[HIDEventXKey] floatValue], [touchInfo[HIDEventYKey] floatValue]) isTouching:(BOOL)touch];
|
|
|
|
IOHIDDigitizerEventMask childEventMask = 0;
|
|
|
|
UITouchPhase phase = phaseFromString(touchInfo[HIDEventPhaseKey]);
|
|
if (phase != UITouchPhaseCancelled && phase != UITouchPhaseBegan && phase != UITouchPhaseEnded && phase != UITouchPhaseStationary)
|
|
childEventMask |= kIOHIDDigitizerEventPosition;
|
|
|
|
if (phase == UITouchPhaseBegan || phase == UITouchPhaseEnded || phase == UITouchPhaseCancelled)
|
|
childEventMask |= (kIOHIDDigitizerEventTouch | kIOHIDDigitizerEventRange);
|
|
|
|
if (phase == UITouchPhaseCancelled)
|
|
childEventMask |= kIOHIDDigitizerEventCancel;
|
|
|
|
if ([touchInfo[HIDEventPressureKey] floatValue])
|
|
childEventMask |= kIOHIDDigitizerEventAttribute;
|
|
|
|
auto subEvent = adoptCF(IOHIDEventCreateDigitizerFingerEvent(kCFAllocatorDefault, machTime,
|
|
[touchInfo[HIDEventTouchIDKey] intValue], // index
|
|
2, // identifier (which finger we think it is). FIXME: this should come from the data.
|
|
childEventMask,
|
|
[touchInfo[HIDEventXKey] floatValue],
|
|
[touchInfo[HIDEventYKey] floatValue],
|
|
0, // z
|
|
[touchInfo[HIDEventPressureKey] floatValue],
|
|
[touchInfo[HIDEventTwistKey] floatValue],
|
|
touch, // range
|
|
touch, // touch
|
|
kIOHIDEventOptionNone));
|
|
|
|
IOHIDEventSetFloatValue(subEvent.get(), kIOHIDEventFieldDigitizerMajorRadius, [touchInfo[HIDEventMajorRadiusKey] floatValue]);
|
|
IOHIDEventSetFloatValue(subEvent.get(), kIOHIDEventFieldDigitizerMinorRadius, [touchInfo[HIDEventMinorRadiusKey] floatValue]);
|
|
|
|
IOHIDEventAppendEvent(eventRef, subEvent.get(), 0);
|
|
}
|
|
|
|
return eventRef;
|
|
}
|
|
|
|
- (IOHIDEventRef)_createIOHIDEventType:(HandEventType)eventType
|
|
{
|
|
BOOL isTouching = (eventType == HandEventTouched || eventType == HandEventMoved || eventType == HandEventChordChanged || eventType == StylusEventTouched || eventType == StylusEventMoved);
|
|
|
|
IOHIDDigitizerEventMask eventMask = kIOHIDDigitizerEventTouch;
|
|
if (eventType == HandEventMoved) {
|
|
eventMask &= ~kIOHIDDigitizerEventTouch;
|
|
eventMask |= kIOHIDDigitizerEventPosition;
|
|
eventMask |= kIOHIDDigitizerEventAttribute;
|
|
} else if (eventType == HandEventChordChanged) {
|
|
eventMask |= kIOHIDDigitizerEventPosition;
|
|
eventMask |= kIOHIDDigitizerEventAttribute;
|
|
} else if (eventType == HandEventTouched || eventType == HandEventCanceled || eventType == HandEventLifted)
|
|
eventMask |= kIOHIDDigitizerEventIdentity;
|
|
|
|
uint64_t machTime = mach_absolute_time();
|
|
auto eventRef = adoptCF(IOHIDEventCreateDigitizerEvent(kCFAllocatorDefault, machTime,
|
|
kIOHIDDigitizerTransducerTypeHand,
|
|
0,
|
|
0,
|
|
eventMask,
|
|
0,
|
|
0, 0, 0,
|
|
0,
|
|
0,
|
|
0,
|
|
isTouching,
|
|
kIOHIDEventOptionNone));
|
|
|
|
IOHIDEventSetIntegerValue(eventRef.get(), kIOHIDEventFieldDigitizerIsDisplayIntegrated, 1);
|
|
|
|
for (NSUInteger i = 0; i < _activePointCount; ++i) {
|
|
SyntheticEventDigitizerInfo* pointInfo = &_activePoints[i];
|
|
if (eventType == HandEventTouched) {
|
|
if (!pointInfo->pathMajorRadius)
|
|
pointInfo->pathMajorRadius = defaultMajorRadius;
|
|
if (!pointInfo->pathPressure)
|
|
pointInfo->pathPressure = defaultPathPressure;
|
|
if (!pointInfo->pathProximity)
|
|
pointInfo->pathProximity = kGSEventPathInfoInTouch | kGSEventPathInfoInRange;
|
|
} else if (eventType == HandEventLifted || eventType == HandEventCanceled || eventType == StylusEventLifted) {
|
|
pointInfo->pathMajorRadius = 0;
|
|
pointInfo->pathPressure = 0;
|
|
pointInfo->pathProximity = 0;
|
|
}
|
|
|
|
CGPoint point = pointInfo->point;
|
|
point = CGPointMake(roundf(point.x), roundf(point.y));
|
|
|
|
[[GeneratedTouchesDebugWindow sharedGeneratedTouchesDebugWindow] updateDebugIndicatorForTouch:i withPointInWindowCoordinates:point isTouching:isTouching];
|
|
|
|
RetainPtr<IOHIDEventRef> subEvent;
|
|
if (pointInfo->isStylus) {
|
|
if (eventType == StylusEventTouched) {
|
|
eventMask |= kIOHIDDigitizerEventEstimatedAltitude;
|
|
eventMask |= kIOHIDDigitizerEventEstimatedAzimuth;
|
|
eventMask |= kIOHIDDigitizerEventEstimatedPressure;
|
|
} else if (eventType == StylusEventMoved)
|
|
eventMask = kIOHIDDigitizerEventPosition;
|
|
|
|
subEvent = adoptCF(IOHIDEventCreateDigitizerStylusEventWithPolarOrientation(kCFAllocatorDefault, machTime,
|
|
pointInfo->identifier,
|
|
pointInfo->identifier,
|
|
eventMask,
|
|
0,
|
|
point.x, point.y, 0,
|
|
pointInfo->pathPressure,
|
|
pointInfo->pathPressure,
|
|
0,
|
|
pointInfo->altitudeAngle,
|
|
pointInfo->azimuthAngle,
|
|
1,
|
|
0,
|
|
isTouching ? kIOHIDTransducerTouch : 0));
|
|
|
|
if (eventType == StylusEventTouched)
|
|
IOHIDEventSetIntegerValue(subEvent.get(), kIOHIDEventFieldDigitizerWillUpdateMask, 0x0400);
|
|
else if (eventType == StylusEventMoved)
|
|
IOHIDEventSetIntegerValue(subEvent.get(), kIOHIDEventFieldDigitizerDidUpdateMask, 0x0400);
|
|
|
|
} else {
|
|
subEvent = adoptCF(IOHIDEventCreateDigitizerFingerEvent(kCFAllocatorDefault, machTime,
|
|
pointInfo->identifier,
|
|
pointInfo->identifier,
|
|
eventMask,
|
|
point.x, point.y, 0,
|
|
pointInfo->pathPressure,
|
|
0,
|
|
pointInfo->pathProximity & kGSEventPathInfoInRange,
|
|
pointInfo->pathProximity & kGSEventPathInfoInTouch,
|
|
kIOHIDEventOptionNone));
|
|
}
|
|
|
|
IOHIDEventSetFloatValue(subEvent.get(), kIOHIDEventFieldDigitizerMajorRadius, pointInfo->pathMajorRadius);
|
|
IOHIDEventSetFloatValue(subEvent.get(), kIOHIDEventFieldDigitizerMinorRadius, pointInfo->pathMajorRadius);
|
|
|
|
IOHIDEventAppendEvent(eventRef.get(), subEvent.get(), 0);
|
|
}
|
|
|
|
return eventRef.leakRef();
|
|
}
|
|
|
|
- (BOOL)_sendHIDEvent:(IOHIDEventRef)eventRef
|
|
{
|
|
if (!_ioSystemClient)
|
|
_ioSystemClient = IOHIDEventSystemClientCreate(kCFAllocatorDefault);
|
|
|
|
if (eventRef) {
|
|
auto strongEvent = retainPtr(eventRef);
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
uint32_t contextID = [UIApplication sharedApplication].keyWindow._contextId;
|
|
ASSERT(contextID);
|
|
BKSHIDEventSetDigitizerInfo(strongEvent.get(), contextID, false, false, NULL, 0, 0);
|
|
[[UIApplication sharedApplication] _enqueueHIDEvent:strongEvent.get()];
|
|
});
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)sendMarkerHIDEventWithCompletionBlock:(void (^)(void))completionBlock
|
|
{
|
|
auto callbackID = [HIDEventGenerator nextEventCallbackID];
|
|
[_eventCallbacks setObject:Block_copy(completionBlock) forKey:@(callbackID)];
|
|
|
|
auto markerEvent = adoptCF(IOHIDEventCreateVendorDefinedEvent(kCFAllocatorDefault,
|
|
mach_absolute_time(),
|
|
kHIDPage_VendorDefinedStart + 100,
|
|
0,
|
|
1,
|
|
(uint8_t*)&callbackID,
|
|
sizeof(CFIndex),
|
|
kIOHIDEventOptionNone));
|
|
|
|
if (markerEvent) {
|
|
dispatch_async(dispatch_get_main_queue(), [markerEvent = WTFMove(markerEvent)] {
|
|
auto contextID = [UIApplication sharedApplication].keyWindow._contextId;
|
|
ASSERT(contextID);
|
|
BKSHIDEventSetDigitizerInfo(markerEvent.get(), contextID, false, false, NULL, 0, 0);
|
|
[[UIApplication sharedApplication] _enqueueHIDEvent:markerEvent.get()];
|
|
});
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (void)_updateTouchPoints:(CGPoint*)points count:(NSUInteger)count
|
|
{
|
|
HandEventType handEventType;
|
|
|
|
// The hand event type is based on previous state.
|
|
if (!_activePointCount)
|
|
handEventType = HandEventTouched;
|
|
else if (!count)
|
|
handEventType = HandEventLifted;
|
|
else if (count == _activePointCount)
|
|
handEventType = HandEventMoved;
|
|
else
|
|
handEventType = HandEventChordChanged;
|
|
|
|
// Update previous count for next event.
|
|
_activePointCount = count;
|
|
|
|
// Update point locations.
|
|
for (NSUInteger i = 0; i < count; ++i) {
|
|
_activePoints[i].point = points[i];
|
|
[[GeneratedTouchesDebugWindow sharedGeneratedTouchesDebugWindow] updateDebugIndicatorForTouch:i withPointInWindowCoordinates:points[i] isTouching:YES];
|
|
}
|
|
|
|
auto eventRef = adoptCF([self _createIOHIDEventType:handEventType]);
|
|
[self _sendHIDEvent:eventRef.get()];
|
|
}
|
|
|
|
- (void)touchDownAtPoints:(CGPoint*)locations touchCount:(NSUInteger)touchCount
|
|
{
|
|
touchCount = std::min(touchCount, HIDMaxTouchCount);
|
|
|
|
_activePointCount = touchCount;
|
|
|
|
for (NSUInteger index = 0; index < touchCount; ++index) {
|
|
_activePoints[index].point = locations[index];
|
|
_activePoints[index].isStylus = NO;
|
|
|
|
[[GeneratedTouchesDebugWindow sharedGeneratedTouchesDebugWindow] updateDebugIndicatorForTouch:index withPointInWindowCoordinates:locations[index] isTouching:YES];
|
|
}
|
|
|
|
auto eventRef = adoptCF([self _createIOHIDEventType:HandEventTouched]);
|
|
[self _sendHIDEvent:eventRef.get()];
|
|
}
|
|
|
|
- (void)touchDown:(CGPoint)location touchCount:(NSUInteger)touchCount
|
|
{
|
|
touchCount = std::min(touchCount, HIDMaxTouchCount);
|
|
|
|
CGPoint locations[touchCount];
|
|
|
|
for (NSUInteger index = 0; index < touchCount; ++index)
|
|
locations[index] = location;
|
|
|
|
[self touchDownAtPoints:locations touchCount:touchCount];
|
|
}
|
|
|
|
- (void)touchDown:(CGPoint)location
|
|
{
|
|
[self touchDownAtPoints:&location touchCount:1];
|
|
}
|
|
|
|
- (void)liftUpAtPoints:(CGPoint*)locations touchCount:(NSUInteger)touchCount
|
|
{
|
|
touchCount = std::min(touchCount, HIDMaxTouchCount);
|
|
touchCount = std::min(touchCount, _activePointCount);
|
|
|
|
NSUInteger newPointCount = _activePointCount - touchCount;
|
|
|
|
for (NSUInteger index = 0; index < touchCount; ++index) {
|
|
_activePoints[newPointCount + index].point = locations[index];
|
|
|
|
[[GeneratedTouchesDebugWindow sharedGeneratedTouchesDebugWindow] updateDebugIndicatorForTouch:index withPointInWindowCoordinates:CGPointZero isTouching:NO];
|
|
}
|
|
|
|
auto eventRef = adoptCF([self _createIOHIDEventType:HandEventLifted]);
|
|
[self _sendHIDEvent:eventRef.get()];
|
|
|
|
_activePointCount = newPointCount;
|
|
}
|
|
|
|
- (void)liftUp:(CGPoint)location touchCount:(NSUInteger)touchCount
|
|
{
|
|
touchCount = std::min(touchCount, HIDMaxTouchCount);
|
|
|
|
CGPoint locations[touchCount];
|
|
|
|
for (NSUInteger index = 0; index < touchCount; ++index)
|
|
locations[index] = location;
|
|
|
|
[self liftUpAtPoints:locations touchCount:touchCount];
|
|
}
|
|
|
|
- (void)liftUp:(CGPoint)location
|
|
{
|
|
[self liftUp:location touchCount:1];
|
|
}
|
|
|
|
- (void)moveToPoints:(CGPoint*)newLocations touchCount:(NSUInteger)touchCount duration:(NSTimeInterval)seconds
|
|
{
|
|
touchCount = std::min(touchCount, HIDMaxTouchCount);
|
|
|
|
CGPoint startLocations[touchCount];
|
|
CGPoint nextLocations[touchCount];
|
|
|
|
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
|
|
CFTimeInterval elapsed = 0;
|
|
|
|
int eventIndex = 0;
|
|
while (elapsed < (seconds - fingerMoveInterval)) {
|
|
elapsed = secondsSinceAbsoluteTime(startTime);
|
|
CFTimeInterval interval = elapsed / seconds;
|
|
|
|
for (NSUInteger i = 0; i < touchCount; ++i) {
|
|
if (!eventIndex)
|
|
startLocations[i] = _activePoints[i].point;
|
|
|
|
nextLocations[i] = calculateNextCurveLocation(startLocations[i], newLocations[i], interval);
|
|
}
|
|
[self _updateTouchPoints:nextLocations count:touchCount];
|
|
|
|
delayBetweenMove(eventIndex++, elapsed);
|
|
}
|
|
|
|
[self _updateTouchPoints:newLocations count:touchCount];
|
|
}
|
|
|
|
- (void)touchDown:(CGPoint)location touchCount:(NSUInteger)count completionBlock:(void (^)(void))completionBlock
|
|
{
|
|
[self touchDown:location touchCount:count];
|
|
[self sendMarkerHIDEventWithCompletionBlock:completionBlock];
|
|
}
|
|
|
|
- (void)liftUp:(CGPoint)location touchCount:(NSUInteger)count completionBlock:(void (^)(void))completionBlock
|
|
{
|
|
[self liftUp:location touchCount:count];
|
|
[self sendMarkerHIDEventWithCompletionBlock:completionBlock];
|
|
}
|
|
|
|
- (void)stylusDownAtPoint:(CGPoint)location azimuthAngle:(CGFloat)azimuthAngle altitudeAngle:(CGFloat)altitudeAngle pressure:(CGFloat)pressure
|
|
{
|
|
_activePointCount = 1;
|
|
_activePoints[0].point = location;
|
|
_activePoints[0].isStylus = YES;
|
|
|
|
// At the time of writing, the IOKit documentation isn't always correct. For example
|
|
// it says that pressure is a value [0,1], but in practice it is [0,500] for stylus
|
|
// data. It does not mention that the azimuth angle is offset from a full rotation.
|
|
// Also, UIKit and IOHID interpret the altitude as different adjacent angles.
|
|
_activePoints[0].pathPressure = pressure * 500;
|
|
_activePoints[0].azimuthAngle = M_PI * 2 - azimuthAngle;
|
|
_activePoints[0].altitudeAngle = M_PI_2 - altitudeAngle;
|
|
|
|
auto eventRef = adoptCF([self _createIOHIDEventType:StylusEventTouched]);
|
|
[self _sendHIDEvent:eventRef.get()];
|
|
}
|
|
|
|
- (void)stylusMoveToPoint:(CGPoint)location azimuthAngle:(CGFloat)azimuthAngle altitudeAngle:(CGFloat)altitudeAngle pressure:(CGFloat)pressure
|
|
{
|
|
_activePointCount = 1;
|
|
_activePoints[0].point = location;
|
|
_activePoints[0].isStylus = YES;
|
|
// See notes above for details on these calculations.
|
|
_activePoints[0].pathPressure = pressure * 500;
|
|
_activePoints[0].azimuthAngle = M_PI * 2 - azimuthAngle;
|
|
_activePoints[0].altitudeAngle = M_PI_2 - altitudeAngle;
|
|
|
|
auto eventRef = adoptCF([self _createIOHIDEventType:StylusEventMoved]);
|
|
[self _sendHIDEvent:eventRef.get()];
|
|
}
|
|
|
|
- (void)stylusUpAtPoint:(CGPoint)location
|
|
{
|
|
_activePointCount = 1;
|
|
_activePoints[0].point = location;
|
|
_activePoints[0].isStylus = YES;
|
|
_activePoints[0].pathPressure = 0;
|
|
_activePoints[0].azimuthAngle = 0;
|
|
_activePoints[0].altitudeAngle = 0;
|
|
|
|
auto eventRef = adoptCF([self _createIOHIDEventType:StylusEventLifted]);
|
|
[self _sendHIDEvent:eventRef.get()];
|
|
}
|
|
|
|
- (void)stylusDownAtPoint:(CGPoint)location azimuthAngle:(CGFloat)azimuthAngle altitudeAngle:(CGFloat)altitudeAngle pressure:(CGFloat)pressure completionBlock:(void (^)(void))completionBlock
|
|
{
|
|
[self stylusDownAtPoint:location azimuthAngle:azimuthAngle altitudeAngle:altitudeAngle pressure:pressure];
|
|
[self sendMarkerHIDEventWithCompletionBlock:completionBlock];
|
|
}
|
|
|
|
- (void)stylusMoveToPoint:(CGPoint)location azimuthAngle:(CGFloat)azimuthAngle altitudeAngle:(CGFloat)altitudeAngle pressure:(CGFloat)pressure completionBlock:(void (^)(void))completionBlock
|
|
{
|
|
[self stylusMoveToPoint:location azimuthAngle:azimuthAngle altitudeAngle:altitudeAngle pressure:pressure];
|
|
[self sendMarkerHIDEventWithCompletionBlock:completionBlock];
|
|
}
|
|
|
|
- (void)stylusUpAtPoint:(CGPoint)location completionBlock:(void (^)(void))completionBlock
|
|
{
|
|
[self stylusUpAtPoint:location];
|
|
[self sendMarkerHIDEventWithCompletionBlock:completionBlock];
|
|
}
|
|
|
|
- (void)stylusTapAtPoint:(CGPoint)location azimuthAngle:(CGFloat)azimuthAngle altitudeAngle:(CGFloat)altitudeAngle pressure:(CGFloat)pressure completionBlock:(void (^)(void))completionBlock
|
|
{
|
|
struct timespec pressDelay = { 0, static_cast<long>(fingerLiftDelay * nanosecondsPerSecond) };
|
|
|
|
[self stylusDownAtPoint:location azimuthAngle:azimuthAngle altitudeAngle:altitudeAngle pressure:pressure];
|
|
nanosleep(&pressDelay, 0);
|
|
[self stylusUpAtPoint:location];
|
|
|
|
[self sendMarkerHIDEventWithCompletionBlock:completionBlock];
|
|
}
|
|
|
|
- (void)_waitFor:(NSTimeInterval)delay
|
|
{
|
|
if (delay <= 0)
|
|
return;
|
|
|
|
bool doneWaitingForDelay = false;
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), [&doneWaitingForDelay] {
|
|
doneWaitingForDelay = true;
|
|
});
|
|
|
|
while (!doneWaitingForDelay)
|
|
[NSRunLoop.currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:NSDate.distantFuture];
|
|
}
|
|
|
|
- (void)sendTaps:(int)tapCount location:(CGPoint)location withNumberOfTouches:(int)touchCount delay:(NSTimeInterval)delay completionBlock:(void (^)(void))completionBlock
|
|
{
|
|
struct timespec doubleDelay = { 0, static_cast<long>(multiTapInterval * nanosecondsPerSecond) };
|
|
struct timespec pressDelay = { 0, static_cast<long>(fingerLiftDelay * nanosecondsPerSecond) };
|
|
|
|
for (int i = 0; i < tapCount; i++) {
|
|
[self touchDown:location touchCount:touchCount];
|
|
nanosleep(&pressDelay, 0);
|
|
[self liftUp:location touchCount:touchCount];
|
|
if (i + 1 != tapCount)
|
|
nanosleep(&doubleDelay, 0);
|
|
|
|
[self _waitFor:delay];
|
|
}
|
|
|
|
[self sendMarkerHIDEventWithCompletionBlock:completionBlock];
|
|
}
|
|
|
|
- (void)tap:(CGPoint)location completionBlock:(void (^)(void))completionBlock
|
|
{
|
|
[self sendTaps:1 location:location withNumberOfTouches:1 delay:0 completionBlock:completionBlock];
|
|
}
|
|
|
|
- (void)doubleTap:(CGPoint)location delay:(NSTimeInterval)delay completionBlock:(void (^)(void))completionBlock
|
|
{
|
|
[self sendTaps:2 location:location withNumberOfTouches:1 delay:delay completionBlock:completionBlock];
|
|
}
|
|
|
|
- (void)twoFingerTap:(CGPoint)location completionBlock:(void (^)(void))completionBlock
|
|
{
|
|
[self sendTaps:1 location:location withNumberOfTouches:2 delay:0 completionBlock:completionBlock];
|
|
}
|
|
|
|
- (void)longPress:(CGPoint)location completionBlock:(void (^)(void))completionBlock
|
|
{
|
|
[self touchDown:location touchCount:1];
|
|
auto completionBlockCopy = makeBlockPtr(completionBlock);
|
|
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, longPressHoldDelay * nanosecondsPerSecond), dispatch_get_main_queue(), ^ {
|
|
[self liftUp:location];
|
|
[self sendMarkerHIDEventWithCompletionBlock:completionBlockCopy.get()];
|
|
});
|
|
}
|
|
|
|
- (void)dragWithStartPoint:(CGPoint)startLocation endPoint:(CGPoint)endLocation duration:(double)seconds completionBlock:(void (^)(void))completionBlock
|
|
{
|
|
[self touchDown:startLocation touchCount:1];
|
|
[self moveToPoints:&endLocation touchCount:1 duration:seconds];
|
|
[self liftUp:endLocation];
|
|
[self sendMarkerHIDEventWithCompletionBlock:completionBlock];
|
|
}
|
|
|
|
- (void)pinchCloseWithStartPoint:(CGPoint)startLocation endPoint:(CGPoint)endLocation duration:(double)seconds completionBlock:(void (^)(void))completionBlock
|
|
{
|
|
}
|
|
|
|
- (void)pinchOpenWithStartPoint:(CGPoint)startLocation endPoint:(CGPoint)endLocation duration:(double)seconds completionBlock:(void (^)(void))completionBlock
|
|
{
|
|
}
|
|
|
|
- (void)markerEventReceived:(IOHIDEventRef)event
|
|
{
|
|
if (IOHIDEventGetType(event) != kIOHIDEventTypeVendorDefined)
|
|
return;
|
|
|
|
CFIndex callbackID = IOHIDEventGetIntegerValue(event, kIOHIDEventFieldVendorDefinedData);
|
|
void (^completionBlock)() = [_eventCallbacks objectForKey:@(callbackID)];
|
|
if (completionBlock) {
|
|
[_eventCallbacks removeObjectForKey:@(callbackID)];
|
|
completionBlock();
|
|
Block_release(completionBlock);
|
|
}
|
|
}
|
|
|
|
- (BOOL)hasOutstandingCallbacks
|
|
{
|
|
return [_eventCallbacks count];
|
|
}
|
|
|
|
static inline bool shouldWrapWithShiftKeyEventForCharacter(NSString *key)
|
|
{
|
|
if (key.length != 1)
|
|
return false;
|
|
int keyCode = [key characterAtIndex:0];
|
|
if (65 <= keyCode && keyCode <= 90)
|
|
return true;
|
|
switch (keyCode) {
|
|
case '!':
|
|
case '@':
|
|
case '#':
|
|
case '$':
|
|
case '%':
|
|
case '^':
|
|
case '&':
|
|
case '*':
|
|
case '(':
|
|
case ')':
|
|
case '_':
|
|
case '+':
|
|
case '{':
|
|
case '}':
|
|
case '|':
|
|
case ':':
|
|
case '"':
|
|
case '<':
|
|
case '>':
|
|
case '?':
|
|
case '~':
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static std::optional<uint32_t> keyCodeForDOMFunctionKey(NSString *key)
|
|
{
|
|
// Compare the input string with the function-key names defined by the DOM spec (i.e. "F1",...,"F24").
|
|
// If the input string is a function-key name, set its key code. On iOS the key codes for the first 12
|
|
// function keys are disjoint from the key codes of the last 12 function keys.
|
|
for (int i = 1; i <= 12; ++i) {
|
|
if ([key isEqualToString:[NSString stringWithFormat:@"F%d", i]])
|
|
return kHIDUsage_KeyboardF1 + i - 1;
|
|
}
|
|
for (int i = 13; i <= 24; ++i) {
|
|
if ([key isEqualToString:[NSString stringWithFormat:@"F%d", i]])
|
|
return kHIDUsage_KeyboardF13 + i - 13;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
static inline uint32_t hidUsageCodeForCharacter(NSString *key)
|
|
{
|
|
const int uppercaseAlphabeticOffset = 'A' - kHIDUsage_KeyboardA;
|
|
const int lowercaseAlphabeticOffset = 'a' - kHIDUsage_KeyboardA;
|
|
const int numericNonZeroOffset = '1' - kHIDUsage_Keyboard1;
|
|
if (key.length == 1) {
|
|
// Handle alphanumeric characters and basic symbols.
|
|
int keyCode = [key characterAtIndex:0];
|
|
if (97 <= keyCode && keyCode <= 122) // Handle a-z.
|
|
return keyCode - lowercaseAlphabeticOffset;
|
|
|
|
if (65 <= keyCode && keyCode <= 90) // Handle A-Z.
|
|
return keyCode - uppercaseAlphabeticOffset;
|
|
|
|
if (49 <= keyCode && keyCode <= 57) // Handle 1-9.
|
|
return keyCode - numericNonZeroOffset;
|
|
|
|
// Handle all other cases.
|
|
switch (keyCode) {
|
|
case '`':
|
|
case '~':
|
|
return kHIDUsage_KeyboardGraveAccentAndTilde;
|
|
case '!':
|
|
return kHIDUsage_Keyboard1;
|
|
case '@':
|
|
return kHIDUsage_Keyboard2;
|
|
case '#':
|
|
return kHIDUsage_Keyboard3;
|
|
case '$':
|
|
return kHIDUsage_Keyboard4;
|
|
case '%':
|
|
return kHIDUsage_Keyboard5;
|
|
case '^':
|
|
return kHIDUsage_Keyboard6;
|
|
case '&':
|
|
return kHIDUsage_Keyboard7;
|
|
case '*':
|
|
return kHIDUsage_Keyboard8;
|
|
case '(':
|
|
return kHIDUsage_Keyboard9;
|
|
case ')':
|
|
case '0':
|
|
return kHIDUsage_Keyboard0;
|
|
case '-':
|
|
case '_':
|
|
return kHIDUsage_KeyboardHyphen;
|
|
case '=':
|
|
case '+':
|
|
return kHIDUsage_KeyboardEqualSign;
|
|
case '\b':
|
|
return kHIDUsage_KeyboardDeleteOrBackspace;
|
|
case '\t':
|
|
return kHIDUsage_KeyboardTab;
|
|
case '[':
|
|
case '{':
|
|
return kHIDUsage_KeyboardOpenBracket;
|
|
case ']':
|
|
case '}':
|
|
return kHIDUsage_KeyboardCloseBracket;
|
|
case '\\':
|
|
case '|':
|
|
return kHIDUsage_KeyboardBackslash;
|
|
case ';':
|
|
case ':':
|
|
return kHIDUsage_KeyboardSemicolon;
|
|
case '\'':
|
|
case '"':
|
|
return kHIDUsage_KeyboardQuote;
|
|
case '\r':
|
|
case '\n':
|
|
return kHIDUsage_KeyboardReturnOrEnter;
|
|
case ',':
|
|
case '<':
|
|
return kHIDUsage_KeyboardComma;
|
|
case '.':
|
|
case '>':
|
|
return kHIDUsage_KeyboardPeriod;
|
|
case '/':
|
|
case '?':
|
|
return kHIDUsage_KeyboardSlash;
|
|
case ' ':
|
|
return kHIDUsage_KeyboardSpacebar;
|
|
}
|
|
}
|
|
|
|
if (auto keyCode = keyCodeForDOMFunctionKey(key))
|
|
return *keyCode;
|
|
|
|
if ([key isEqualToString:@"capsLock"] || [key isEqualToString:@"capsLockKey"])
|
|
return kHIDUsage_KeyboardCapsLock;
|
|
if ([key isEqualToString:@"pageUp"])
|
|
return kHIDUsage_KeyboardPageUp;
|
|
if ([key isEqualToString:@"pageDown"])
|
|
return kHIDUsage_KeyboardPageDown;
|
|
if ([key isEqualToString:@"home"])
|
|
return kHIDUsage_KeyboardHome;
|
|
if ([key isEqualToString:@"insert"])
|
|
return kHIDUsage_KeyboardInsert;
|
|
if ([key isEqualToString:@"end"])
|
|
return kHIDUsage_KeyboardEnd;
|
|
if ([key isEqualToString:@"escape"])
|
|
return kHIDUsage_KeyboardEscape;
|
|
if ([key isEqualToString:@"return"] || [key isEqualToString:@"enter"])
|
|
return kHIDUsage_KeyboardReturnOrEnter;
|
|
if ([key isEqualToString:@"leftArrow"])
|
|
return kHIDUsage_KeyboardLeftArrow;
|
|
if ([key isEqualToString:@"rightArrow"])
|
|
return kHIDUsage_KeyboardRightArrow;
|
|
if ([key isEqualToString:@"upArrow"])
|
|
return kHIDUsage_KeyboardUpArrow;
|
|
if ([key isEqualToString:@"downArrow"])
|
|
return kHIDUsage_KeyboardDownArrow;
|
|
if ([key isEqualToString:@"delete"])
|
|
return kHIDUsage_KeyboardDeleteOrBackspace;
|
|
if ([key isEqualToString:@"forwardDelete"])
|
|
return kHIDUsage_KeyboardDeleteForward;
|
|
if ([key isEqualToString:@"leftCommand"] || [key isEqualToString:@"metaKey"])
|
|
return kHIDUsage_KeyboardLeftGUI;
|
|
if ([key isEqualToString:@"rightCommand"])
|
|
return kHIDUsage_KeyboardRightGUI;
|
|
if ([key isEqualToString:@"clear"]) // Num Lock / Clear
|
|
return kHIDUsage_KeypadNumLock;
|
|
if ([key isEqualToString:@"leftControl"] || [key isEqualToString:@"ctrlKey"])
|
|
return kHIDUsage_KeyboardLeftControl;
|
|
if ([key isEqualToString:@"rightControl"])
|
|
return kHIDUsage_KeyboardRightControl;
|
|
if ([key isEqualToString:@"leftShift"] || [key isEqualToString:@"shiftKey"])
|
|
return kHIDUsage_KeyboardLeftShift;
|
|
if ([key isEqualToString:@"rightShift"])
|
|
return kHIDUsage_KeyboardRightShift;
|
|
if ([key isEqualToString:@"leftAlt"] || [key isEqualToString:@"altKey"])
|
|
return kHIDUsage_KeyboardLeftAlt;
|
|
if ([key isEqualToString:@"rightAlt"])
|
|
return kHIDUsage_KeyboardRightAlt;
|
|
if ([key isEqualToString:@"numpadComma"])
|
|
return kHIDUsage_KeypadComma;
|
|
|
|
return 0;
|
|
}
|
|
|
|
RetainPtr<IOHIDEventRef> createHIDKeyEvent(NSString *character, uint64_t timestamp, bool isKeyDown)
|
|
{
|
|
return adoptCF(IOHIDEventCreateKeyboardEvent(kCFAllocatorDefault, timestamp, kHIDPage_KeyboardOrKeypad, hidUsageCodeForCharacter(character), isKeyDown, kIOHIDEventOptionNone));
|
|
}
|
|
|
|
- (void)keyDown:(NSString *)character
|
|
{
|
|
[self _sendIOHIDKeyboardEvent:mach_absolute_time() usage:hidUsageCodeForCharacter(character) isKeyDown:true];
|
|
}
|
|
|
|
- (void)keyUp:(NSString *)character
|
|
{
|
|
[self _sendIOHIDKeyboardEvent:mach_absolute_time() usage:hidUsageCodeForCharacter(character) isKeyDown:false];
|
|
}
|
|
|
|
- (void)keyPress:(NSString *)character completionBlock:(void (^)(void))completionBlock
|
|
{
|
|
bool shouldWrapWithShift = shouldWrapWithShiftKeyEventForCharacter(character);
|
|
uint32_t usage = hidUsageCodeForCharacter(character);
|
|
uint64_t absoluteMachTime = mach_absolute_time();
|
|
|
|
if (shouldWrapWithShift)
|
|
[self _sendIOHIDKeyboardEvent:absoluteMachTime usage:kHIDUsage_KeyboardLeftShift isKeyDown:true];
|
|
|
|
[self _sendIOHIDKeyboardEvent:absoluteMachTime usage:usage isKeyDown:true];
|
|
[self _sendIOHIDKeyboardEvent:absoluteMachTime usage:usage isKeyDown:false];
|
|
|
|
if (shouldWrapWithShift)
|
|
[self _sendIOHIDKeyboardEvent:absoluteMachTime usage:kHIDUsage_KeyboardLeftShift isKeyDown:false];
|
|
|
|
[self sendMarkerHIDEventWithCompletionBlock:completionBlock];
|
|
}
|
|
|
|
- (void)dispatchEventWithInfo:(NSDictionary *)eventInfo
|
|
{
|
|
ASSERT([NSThread isMainThread]);
|
|
|
|
auto eventRef = adoptCF([self _createIOHIDEventWithInfo:eventInfo]);
|
|
[self _sendHIDEvent:eventRef.get()];
|
|
}
|
|
|
|
- (NSArray *)interpolatedEvents:(NSDictionary *)interpolationsDictionary
|
|
{
|
|
NSDictionary *startEvent = interpolationsDictionary[HIDEventStartEventKey];
|
|
NSDictionary *endEvent = interpolationsDictionary[HIDEventEndEventKey];
|
|
NSTimeInterval timeStep = [interpolationsDictionary[HIDEventTimestepKey] doubleValue];
|
|
InterpolationType interpolationType = interpolationFromString(interpolationsDictionary[HIDEventInterpolateKey]);
|
|
|
|
NSMutableArray *interpolatedEvents = [NSMutableArray arrayWithObject:startEvent];
|
|
|
|
NSTimeInterval startTime = [startEvent[HIDEventTimeOffsetKey] doubleValue];
|
|
NSTimeInterval endTime = [endEvent[HIDEventTimeOffsetKey] doubleValue];
|
|
NSTimeInterval time = startTime + timeStep;
|
|
|
|
NSArray *startTouches = startEvent[HIDEventTouchesKey];
|
|
NSArray *endTouches = endEvent[HIDEventTouchesKey];
|
|
|
|
while (time < endTime) {
|
|
auto newEvent = adoptNS([endEvent mutableCopy]);
|
|
double timeRatio = (time - startTime) / (endTime - startTime);
|
|
newEvent.get()[HIDEventTimeOffsetKey] = @(time);
|
|
|
|
NSEnumerator *startEnumerator = [startTouches objectEnumerator];
|
|
NSDictionary *startTouch;
|
|
NSMutableArray *newTouches = [NSMutableArray arrayWithCapacity:[endTouches count]];
|
|
while (startTouch = [startEnumerator nextObject]) {
|
|
NSEnumerator *endEnumerator = [endTouches objectEnumerator];
|
|
NSDictionary *endTouch = [endEnumerator nextObject];
|
|
NSInteger startTouchID = [startTouch[HIDEventTouchIDKey] integerValue];
|
|
|
|
while (endTouch && ([endTouch[HIDEventTouchIDKey] integerValue] != startTouchID))
|
|
endTouch = [endEnumerator nextObject];
|
|
|
|
if (endTouch) {
|
|
auto newTouch = adoptNS([endTouch mutableCopy]);
|
|
|
|
if (newTouch.get()[HIDEventXKey] != startTouch[HIDEventXKey])
|
|
newTouch.get()[HIDEventXKey] = @(interpolations[interpolationType]([startTouch[HIDEventXKey] doubleValue], [endTouch[HIDEventXKey] doubleValue], timeRatio));
|
|
|
|
if (newTouch.get()[HIDEventYKey] != startTouch[HIDEventYKey])
|
|
newTouch.get()[HIDEventYKey] = @(interpolations[interpolationType]([startTouch[HIDEventYKey] doubleValue], [endTouch[HIDEventYKey] doubleValue], timeRatio));
|
|
|
|
if (newTouch.get()[HIDEventPressureKey] != startTouch[HIDEventPressureKey])
|
|
newTouch.get()[HIDEventPressureKey] = @(interpolations[interpolationType]([startTouch[HIDEventPressureKey] doubleValue], [endTouch[HIDEventPressureKey] doubleValue], timeRatio));
|
|
|
|
[newTouches addObject:newTouch.get()];
|
|
} else
|
|
NSLog(@"Missing End Touch with ID: %ld", (long)startTouchID);
|
|
}
|
|
|
|
newEvent.get()[HIDEventTouchesKey] = newTouches;
|
|
|
|
[interpolatedEvents addObject:newEvent.get()];
|
|
time += timeStep;
|
|
}
|
|
|
|
[interpolatedEvents addObject:endEvent];
|
|
|
|
return interpolatedEvents;
|
|
}
|
|
|
|
- (NSArray *)expandEvents:(NSArray *)events withStartTime:(CFAbsoluteTime)startTime
|
|
{
|
|
NSMutableArray *expandedEvents = [NSMutableArray array];
|
|
for (NSDictionary *event in events) {
|
|
NSString *interpolate = event[HIDEventInterpolateKey];
|
|
// we have key events that we need to generate
|
|
if (interpolate) {
|
|
NSArray *newEvents = [self interpolatedEvents:event];
|
|
[expandedEvents addObjectsFromArray:[self expandEvents:newEvents withStartTime:startTime]];
|
|
} else
|
|
[expandedEvents addObject:event];
|
|
}
|
|
return expandedEvents;
|
|
}
|
|
|
|
- (void)eventDispatchThreadEntry:(NSDictionary *)threadData
|
|
{
|
|
NSDictionary *eventStream = threadData[@"eventInfo"];
|
|
void (^completionBlock)() = threadData[@"completionBlock"];
|
|
|
|
NSArray *events = eventStream[TopLevelEventInfoKey];
|
|
if (!events.count) {
|
|
NSLog(@"No events found in event stream");
|
|
return;
|
|
}
|
|
|
|
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
|
|
|
|
NSArray *expandedEvents = [self expandEvents:events withStartTime:startTime];
|
|
|
|
for (NSDictionary *eventInfo in expandedEvents) {
|
|
NSTimeInterval eventRelativeTime = [eventInfo[HIDEventTimeOffsetKey] doubleValue];
|
|
CFAbsoluteTime targetTime = startTime + eventRelativeTime;
|
|
|
|
CFTimeInterval waitTime = targetTime - CFAbsoluteTimeGetCurrent();
|
|
if (waitTime > 0)
|
|
[NSThread sleepForTimeInterval:waitTime];
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^ {
|
|
[self dispatchEventWithInfo:eventInfo];
|
|
});
|
|
}
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^ {
|
|
[self sendMarkerHIDEventWithCompletionBlock:completionBlock];
|
|
});
|
|
}
|
|
|
|
- (void)sendEventStream:(NSDictionary *)eventInfo completionBlock:(void (^)(void))completionBlock
|
|
{
|
|
if (!eventInfo) {
|
|
NSLog(@"eventInfo is nil");
|
|
if (completionBlock)
|
|
completionBlock();
|
|
return;
|
|
}
|
|
|
|
NSDictionary* threadData = @{
|
|
@"eventInfo": adoptNS([eventInfo copy]).get(),
|
|
@"completionBlock": adoptNS([completionBlock copy]).get()
|
|
};
|
|
|
|
auto eventDispatchThread = adoptNS([[NSThread alloc] initWithTarget:self selector:@selector(eventDispatchThreadEntry:) object:threadData]);
|
|
[eventDispatchThread setQualityOfService:NSQualityOfServiceUserInteractive];
|
|
[eventDispatchThread start];
|
|
}
|
|
|
|
@end
|