FLEXKeyboardShortcutManager.m 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  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. @end
  19. @interface FLEXKeyInput : NSObject <NSCopying>
  20. @property (nonatomic, copy, readonly) NSString *key;
  21. @property (nonatomic, assign, readonly) UIKeyModifierFlags flags;
  22. @property (nonatomic, copy, readonly) NSString *helpDescription;
  23. @end
  24. @implementation FLEXKeyInput
  25. - (BOOL)isEqual:(id)object
  26. {
  27. BOOL isEqual = NO;
  28. if ([object isKindOfClass:[FLEXKeyInput class]]) {
  29. FLEXKeyInput *keyCommand = (FLEXKeyInput *)object;
  30. BOOL equalKeys = self.key == keyCommand.key || [self.key isEqual:keyCommand.key];
  31. BOOL equalFlags = self.flags == keyCommand.flags;
  32. isEqual = equalKeys && equalFlags;
  33. }
  34. return isEqual;
  35. }
  36. - (NSUInteger)hash
  37. {
  38. return [self.key hash] ^ self.flags;
  39. }
  40. - (id)copyWithZone:(NSZone *)zone
  41. {
  42. return [[self class] keyInputForKey:self.key flags:self.flags helpDescription:self.helpDescription];
  43. }
  44. - (NSString *)description
  45. {
  46. NSDictionary *keyMappings = @{ UIKeyInputUpArrow : @"↑",
  47. UIKeyInputDownArrow : @"↓",
  48. UIKeyInputLeftArrow : @"←",
  49. UIKeyInputRightArrow : @"→",
  50. UIKeyInputEscape : @"␛",
  51. @" " : @"␠"};
  52. NSString *prettyKey = nil;
  53. if (self.key && [keyMappings objectForKey:self.key]) {
  54. prettyKey = [keyMappings objectForKey:self.key];
  55. } else {
  56. prettyKey = [self.key uppercaseString];
  57. }
  58. NSString *prettyFlags = @"";
  59. if (self.flags & UIKeyModifierControl) {
  60. prettyFlags = [prettyFlags stringByAppendingString:@"⌃"];
  61. }
  62. if (self.flags & UIKeyModifierAlternate) {
  63. prettyFlags = [prettyFlags stringByAppendingString:@"⌥"];
  64. }
  65. if (self.flags & UIKeyModifierShift) {
  66. prettyFlags = [prettyFlags stringByAppendingString:@"⇧"];
  67. }
  68. if (self.flags & UIKeyModifierCommand) {
  69. prettyFlags = [prettyFlags stringByAppendingString:@"⌘"];
  70. }
  71. // Fudging to get easy columns with tabs
  72. if ([prettyFlags length] < 2) {
  73. prettyKey = [prettyKey stringByAppendingString:@"\t"];
  74. }
  75. return [NSString stringWithFormat:@"%@%@\t%@", prettyFlags, prettyKey, self.helpDescription];
  76. }
  77. + (instancetype)keyInputForKey:(NSString *)key flags:(UIKeyModifierFlags)flags
  78. {
  79. return [self keyInputForKey:key flags:flags helpDescription:nil];
  80. }
  81. + (instancetype)keyInputForKey:(NSString *)key flags:(UIKeyModifierFlags)flags helpDescription:(NSString *)helpDescription
  82. {
  83. FLEXKeyInput *keyInput = [[self alloc] init];
  84. if (keyInput) {
  85. keyInput->_key = key;
  86. keyInput->_flags = flags;
  87. keyInput->_helpDescription = helpDescription;
  88. }
  89. return keyInput;
  90. }
  91. @end
  92. @interface FLEXKeyboardShortcutManager ()
  93. @property (nonatomic, strong) NSMutableDictionary *actionsForKeyInputs;
  94. @end
  95. @implementation FLEXKeyboardShortcutManager
  96. + (instancetype)sharedManager
  97. {
  98. static FLEXKeyboardShortcutManager *sharedManager = nil;
  99. static dispatch_once_t onceToken;
  100. dispatch_once(&onceToken, ^{
  101. sharedManager = [[[self class] alloc] init];
  102. });
  103. return sharedManager;
  104. }
  105. + (void)load
  106. {
  107. SEL originalKeyEventSelector = NSSelectorFromString(@"handleKeyUIEvent:");
  108. SEL swizzledKeyEventSelector = [FLEXUtility swizzledSelectorForSelector:originalKeyEventSelector];
  109. void (^sendEventSwizzleBlock)(UIApplication *, UIEvent *) = ^(UIApplication *slf, UIEvent *event) {
  110. [[[self class] sharedManager] handleKeyboardEvent:event];
  111. ((void(*)(id, SEL, id))objc_msgSend)(slf, swizzledKeyEventSelector, event);
  112. };
  113. [FLEXUtility replaceImplementationOfKnownSelector:originalKeyEventSelector onClass:[UIApplication class] withBlock:sendEventSwizzleBlock swizzledSelector:swizzledKeyEventSelector];
  114. }
  115. - (instancetype)init
  116. {
  117. self = [super init];
  118. if (self) {
  119. _actionsForKeyInputs = [NSMutableDictionary dictionary];
  120. _enabled = YES;
  121. }
  122. return self;
  123. }
  124. - (void)registerSimulatorShortcutWithKey:(NSString *)key modifiers:(UIKeyModifierFlags)modifiers action:(dispatch_block_t)action description:(NSString *)description
  125. {
  126. FLEXKeyInput *keyInput = [FLEXKeyInput keyInputForKey:key flags:modifiers helpDescription:description];
  127. [self.actionsForKeyInputs setObject:action forKey:keyInput];
  128. }
  129. - (void)handleKeyboardEvent:(UIEvent *)event
  130. {
  131. if (!self.enabled) {
  132. return;
  133. }
  134. NSString *modifiedInput = nil;
  135. NSString *unmodifiedInput = nil;
  136. UIKeyModifierFlags flags = 0;
  137. BOOL isKeyDown = NO;
  138. if ([event respondsToSelector:@selector(_modifiedInput)]) {
  139. modifiedInput = [event _modifiedInput];
  140. }
  141. if ([event respondsToSelector:@selector(_unmodifiedInput)]) {
  142. unmodifiedInput = [event _unmodifiedInput];
  143. }
  144. if ([event respondsToSelector:@selector(_modifierFlags)]) {
  145. flags = [event _modifierFlags];
  146. }
  147. if ([event respondsToSelector:@selector(_isKeyDown)]) {
  148. isKeyDown = [event _isKeyDown];
  149. }
  150. BOOL interactionEnabled = ![[UIApplication sharedApplication] isIgnoringInteractionEvents];
  151. if (isKeyDown && [modifiedInput length] > 0 && interactionEnabled) {
  152. UIResponder *firstResponder = nil;
  153. for (UIWindow *window in [[UIApplication sharedApplication] windows]) {
  154. firstResponder = [window valueForKey:@"firstResponder"];
  155. if (firstResponder) {
  156. break;
  157. }
  158. }
  159. // Ignore key commands (except escape) when there's an active responder
  160. if (firstResponder) {
  161. if ([unmodifiedInput isEqual:UIKeyInputEscape]) {
  162. [firstResponder resignFirstResponder];
  163. }
  164. } else {
  165. FLEXKeyInput *exactMatch = [FLEXKeyInput keyInputForKey:unmodifiedInput flags:flags];
  166. dispatch_block_t actionBlock = [self.actionsForKeyInputs objectForKey:exactMatch];
  167. if (!actionBlock) {
  168. FLEXKeyInput *shiftMatch = [FLEXKeyInput keyInputForKey:modifiedInput flags:flags&(!UIKeyModifierShift)];
  169. actionBlock = [self.actionsForKeyInputs objectForKey:shiftMatch];
  170. }
  171. if (!actionBlock) {
  172. FLEXKeyInput *capitalMatch = [FLEXKeyInput keyInputForKey:[unmodifiedInput uppercaseString] flags:flags];
  173. actionBlock = [self.actionsForKeyInputs objectForKey:capitalMatch];
  174. }
  175. if (actionBlock) {
  176. actionBlock();
  177. }
  178. }
  179. }
  180. }
  181. - (NSString *)keyboardShortcutsDescription
  182. {
  183. NSMutableString *description = [NSMutableString string];
  184. NSArray *keyInputs = [[self.actionsForKeyInputs allKeys] sortedArrayUsingComparator:^NSComparisonResult(FLEXKeyInput *_Nonnull input1, FLEXKeyInput *_Nonnull input2) {
  185. return [input1.key caseInsensitiveCompare:input2.key];
  186. }];
  187. for (FLEXKeyInput *keyInput in keyInputs) {
  188. [description appendFormat:@"%@\n", keyInput];
  189. }
  190. return [description copy];
  191. }
  192. @end
  193. #endif