FLEXKeyboardShortcutManager.m 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. //
  2. // FLEXKeyboardShortcutManager.m
  3. // FLEX
  4. //
  5. // Created by Ryan Olson on 9/19/15.
  6. // Copyright © 2015 Flipboard. All rights reserved.
  7. //
  8. #import "FLEXKeyboardShortcutManager.h"
  9. #import "FLEXUtility.h"
  10. #import <objc/runtime.h>
  11. #import <objc/message.h>
  12. #if TARGET_OS_SIMULATOR
  13. @interface UIEvent (UIPhysicalKeyboardEvent)
  14. @property (nonatomic, strong) NSString *_modifiedInput;
  15. @property (nonatomic, strong) NSString *_unmodifiedInput;
  16. @property (nonatomic, assign) UIKeyModifierFlags _modifierFlags;
  17. @property (nonatomic, assign) BOOL _isKeyDown;
  18. @property (nonatomic, assign) long _keyCode;
  19. @end
  20. @interface FLEXKeyInput : NSObject <NSCopying>
  21. @property (nonatomic, copy, readonly) NSString *key;
  22. @property (nonatomic, assign, readonly) UIKeyModifierFlags flags;
  23. @property (nonatomic, copy, readonly) NSString *helpDescription;
  24. @end
  25. @implementation FLEXKeyInput
  26. - (BOOL)isEqual:(id)object
  27. {
  28. BOOL isEqual = NO;
  29. if ([object isKindOfClass:[FLEXKeyInput class]]) {
  30. FLEXKeyInput *keyCommand = (FLEXKeyInput *)object;
  31. BOOL equalKeys = self.key == keyCommand.key || [self.key isEqual:keyCommand.key];
  32. BOOL equalFlags = self.flags == keyCommand.flags;
  33. isEqual = equalKeys && equalFlags;
  34. }
  35. return isEqual;
  36. }
  37. - (NSUInteger)hash
  38. {
  39. return [self.key hash] ^ self.flags;
  40. }
  41. - (id)copyWithZone:(NSZone *)zone
  42. {
  43. return [[self class] keyInputForKey:self.key flags:self.flags helpDescription:self.helpDescription];
  44. }
  45. - (NSString *)description
  46. {
  47. NSDictionary *keyMappings = @{ UIKeyInputUpArrow : @"↑",
  48. UIKeyInputDownArrow : @"↓",
  49. UIKeyInputLeftArrow : @"←",
  50. UIKeyInputRightArrow : @"→",
  51. UIKeyInputEscape : @"␛",
  52. @" " : @"␠"};
  53. NSString *prettyKey = nil;
  54. if (self.key && keyMappings[self.key]) {
  55. prettyKey = keyMappings[self.key];
  56. } else {
  57. prettyKey = [self.key uppercaseString];
  58. }
  59. NSString *prettyFlags = @"";
  60. if (self.flags & UIKeyModifierControl) {
  61. prettyFlags = [prettyFlags stringByAppendingString:@"⌃"];
  62. }
  63. if (self.flags & UIKeyModifierAlternate) {
  64. prettyFlags = [prettyFlags stringByAppendingString:@"⌥"];
  65. }
  66. if (self.flags & UIKeyModifierShift) {
  67. prettyFlags = [prettyFlags stringByAppendingString:@"⇧"];
  68. }
  69. if (self.flags & UIKeyModifierCommand) {
  70. prettyFlags = [prettyFlags stringByAppendingString:@"⌘"];
  71. }
  72. // Fudging to get easy columns with tabs
  73. if ([prettyFlags length] < 2) {
  74. prettyKey = [prettyKey stringByAppendingString:@"\t"];
  75. }
  76. return [NSString stringWithFormat:@"%@%@\t%@", prettyFlags, prettyKey, self.helpDescription];
  77. }
  78. + (instancetype)keyInputForKey:(NSString *)key flags:(UIKeyModifierFlags)flags
  79. {
  80. return [self keyInputForKey:key flags:flags helpDescription:nil];
  81. }
  82. + (instancetype)keyInputForKey:(NSString *)key flags:(UIKeyModifierFlags)flags helpDescription:(NSString *)helpDescription
  83. {
  84. FLEXKeyInput *keyInput = [[self alloc] init];
  85. if (keyInput) {
  86. keyInput->_key = key;
  87. keyInput->_flags = flags;
  88. keyInput->_helpDescription = helpDescription;
  89. }
  90. return keyInput;
  91. }
  92. @end
  93. @interface FLEXKeyboardShortcutManager ()
  94. @property (nonatomic, strong) NSMutableDictionary *actionsForKeyInputs;
  95. @property (nonatomic, assign, getter=isPressingShift) BOOL pressingShift;
  96. @property (nonatomic, assign, getter=isPressingCommand) BOOL pressingCommand;
  97. @property (nonatomic, assign, getter=isPressingControl) BOOL pressingControl;
  98. @end
  99. @implementation FLEXKeyboardShortcutManager
  100. + (instancetype)sharedManager
  101. {
  102. static FLEXKeyboardShortcutManager *sharedManager = nil;
  103. static dispatch_once_t onceToken;
  104. dispatch_once(&onceToken, ^{
  105. sharedManager = [[[self class] alloc] init];
  106. });
  107. return sharedManager;
  108. }
  109. + (void)load
  110. {
  111. SEL originalKeyEventSelector = NSSelectorFromString(@"handleKeyUIEvent:");
  112. SEL swizzledKeyEventSelector = [FLEXUtility swizzledSelectorForSelector:originalKeyEventSelector];
  113. void (^handleKeyUIEventSwizzleBlock)(UIApplication *, UIEvent *) = ^(UIApplication *slf, UIEvent *event) {
  114. [[[self class] sharedManager] handleKeyboardEvent:event];
  115. ((void(*)(id, SEL, id))objc_msgSend)(slf, swizzledKeyEventSelector, event);
  116. };
  117. [FLEXUtility replaceImplementationOfKnownSelector:originalKeyEventSelector onClass:[UIApplication class] withBlock:handleKeyUIEventSwizzleBlock swizzledSelector:swizzledKeyEventSelector];
  118. if ([[UITouch class] instancesRespondToSelector:@selector(maximumPossibleForce)]) {
  119. SEL originalSendEventSelector = NSSelectorFromString(@"sendEvent:");
  120. SEL swizzledSendEventSelector = [FLEXUtility swizzledSelectorForSelector:originalSendEventSelector];
  121. void (^sendEventSwizzleBlock)(UIApplication *, UIEvent *) = ^(UIApplication *slf, UIEvent *event) {
  122. if (event.type == UIEventTypeTouches) {
  123. FLEXKeyboardShortcutManager *keyboardManager = [FLEXKeyboardShortcutManager sharedManager];
  124. NSInteger pressureLevel = 0;
  125. if (keyboardManager.isPressingShift) {
  126. pressureLevel++;
  127. }
  128. if (keyboardManager.isPressingCommand) {
  129. pressureLevel++;
  130. }
  131. if (keyboardManager.isPressingControl) {
  132. pressureLevel++;
  133. }
  134. if (pressureLevel > 0) {
  135. for (UITouch *touch in [event allTouches]) {
  136. double adjustedPressureLevel = pressureLevel * 20 * touch.maximumPossibleForce;
  137. [touch setValue:@(adjustedPressureLevel) forKey:@"_pressure"];
  138. }
  139. }
  140. }
  141. ((void(*)(id, SEL, id))objc_msgSend)(slf, swizzledSendEventSelector, event);
  142. };
  143. [FLEXUtility replaceImplementationOfKnownSelector:originalSendEventSelector onClass:[UIApplication class] withBlock:sendEventSwizzleBlock swizzledSelector:swizzledSendEventSelector];
  144. SEL originalSupportsTouchPressureSelector = NSSelectorFromString(@"_supportsForceTouch");
  145. SEL swizzledSupportsTouchPressureSelector = [FLEXUtility swizzledSelectorForSelector:originalSupportsTouchPressureSelector];
  146. BOOL (^supportsTouchPressureSwizzleBlock)(UIDevice *) = ^BOOL(UIDevice *slf) {
  147. return YES;
  148. };
  149. [FLEXUtility replaceImplementationOfKnownSelector:originalSupportsTouchPressureSelector onClass:[UIDevice class] withBlock:supportsTouchPressureSwizzleBlock swizzledSelector:swizzledSupportsTouchPressureSelector];
  150. }
  151. }
  152. - (instancetype)init
  153. {
  154. self = [super init];
  155. if (self) {
  156. _actionsForKeyInputs = [NSMutableDictionary dictionary];
  157. _enabled = YES;
  158. }
  159. return self;
  160. }
  161. - (void)registerSimulatorShortcutWithKey:(NSString *)key modifiers:(UIKeyModifierFlags)modifiers action:(dispatch_block_t)action description:(NSString *)description
  162. {
  163. FLEXKeyInput *keyInput = [FLEXKeyInput keyInputForKey:key flags:modifiers helpDescription:description];
  164. [self.actionsForKeyInputs setObject:action forKey:keyInput];
  165. }
  166. static const long kFLEXControlKeyCode = 0xe0;
  167. static const long kFLEXShiftKeyCode = 0xe1;
  168. static const long kFLEXCommandKeyCode = 0xe3;
  169. - (void)handleKeyboardEvent:(UIEvent *)event
  170. {
  171. if (!self.enabled) {
  172. return;
  173. }
  174. NSString *modifiedInput = nil;
  175. NSString *unmodifiedInput = nil;
  176. UIKeyModifierFlags flags = 0;
  177. BOOL isKeyDown = NO;
  178. if ([event respondsToSelector:@selector(_modifiedInput)]) {
  179. modifiedInput = [event _modifiedInput];
  180. }
  181. if ([event respondsToSelector:@selector(_unmodifiedInput)]) {
  182. unmodifiedInput = [event _unmodifiedInput];
  183. }
  184. if ([event respondsToSelector:@selector(_modifierFlags)]) {
  185. flags = [event _modifierFlags];
  186. }
  187. if ([event respondsToSelector:@selector(_isKeyDown)]) {
  188. isKeyDown = [event _isKeyDown];
  189. }
  190. BOOL interactionEnabled = ![[UIApplication sharedApplication] isIgnoringInteractionEvents];
  191. BOOL hasFirstResponder = NO;
  192. if (isKeyDown && [modifiedInput length] > 0 && interactionEnabled) {
  193. UIResponder *firstResponder = nil;
  194. for (UIWindow *window in [FLEXUtility allWindows]) {
  195. firstResponder = [window valueForKey:@"firstResponder"];
  196. if (firstResponder) {
  197. hasFirstResponder = YES;
  198. break;
  199. }
  200. }
  201. // Ignore key commands (except escape) when there's an active responder
  202. if (firstResponder) {
  203. if ([unmodifiedInput isEqual:UIKeyInputEscape]) {
  204. [firstResponder resignFirstResponder];
  205. }
  206. } else {
  207. FLEXKeyInput *exactMatch = [FLEXKeyInput keyInputForKey:unmodifiedInput flags:flags];
  208. dispatch_block_t actionBlock = self.actionsForKeyInputs[exactMatch];
  209. if (!actionBlock) {
  210. FLEXKeyInput *shiftMatch = [FLEXKeyInput keyInputForKey:modifiedInput flags:flags&(!UIKeyModifierShift)];
  211. actionBlock = self.actionsForKeyInputs[shiftMatch];
  212. }
  213. if (!actionBlock) {
  214. FLEXKeyInput *capitalMatch = [FLEXKeyInput keyInputForKey:[unmodifiedInput uppercaseString] flags:flags];
  215. actionBlock = self.actionsForKeyInputs[capitalMatch];
  216. }
  217. if (actionBlock) {
  218. actionBlock();
  219. }
  220. }
  221. }
  222. // Calling _keyCode on events from the simulator keyboard will crash.
  223. // It is only safe to call _keyCode when there's not an active responder.
  224. if (!hasFirstResponder && [event respondsToSelector:@selector(_keyCode)]) {
  225. long keyCode = [event _keyCode];
  226. if (keyCode == kFLEXControlKeyCode) {
  227. self.pressingControl = isKeyDown;
  228. } else if (keyCode == kFLEXCommandKeyCode) {
  229. self.pressingCommand = isKeyDown;
  230. } else if (keyCode == kFLEXShiftKeyCode) {
  231. self.pressingShift = isKeyDown;
  232. }
  233. }
  234. }
  235. - (NSString *)keyboardShortcutsDescription
  236. {
  237. NSMutableString *description = [NSMutableString string];
  238. NSArray *keyInputs = [[self.actionsForKeyInputs allKeys] sortedArrayUsingComparator:^NSComparisonResult(FLEXKeyInput *_Nonnull input1, FLEXKeyInput *_Nonnull input2) {
  239. return [input1.key caseInsensitiveCompare:input2.key];
  240. }];
  241. for (FLEXKeyInput *keyInput in keyInputs) {
  242. [description appendFormat:@"%@\n", keyInput];
  243. }
  244. return [description copy];
  245. }
  246. @end
  247. #endif