| /* |
| * 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 |