Przeglądaj źródła

Add support for keyboard shortcuts in the simulator

Ryan Olson 10 lat temu
rodzic
commit
b22d2b57a2

+ 17 - 0
Classes/ExplorerToolbar/FLEXManager.h

@@ -29,6 +29,23 @@
 /// The response cache uses an NSCache, so it may purge prior to hitting the limit when the app is under memory pressure.
 @property (nonatomic, assign) NSUInteger networkResponseCacheByteLimit;
 
+#pragma mark - Keyboard Shortcuts
+
+/// Simulator keyboard shortcuts are enabled by default.
+/// The shortcuts will not fire when there is an active text field, text view, or other responder accepting key input.
+/// You can disable keyboard shortcuts if you have existing keyboard shortcuts that conflict with FLEX, or if you like doing things the hard way ;)
+/// Keyboard shortcuts are always disabled (and support is compiled out) in non-simulator builds
+@property (nonatomic, assign) BOOL simulatorShortcutsEnabled;
+
+/// Adds an action to run when the specified key & modifier combination is pressed
+/// @param key A single character string matching a key on the keyboard
+/// @param modifiers Modifier keys such as shift, command, or alt/option
+/// @param action The block to run on the main thread when the key & modifier combination is recognized.
+/// @param description Shown the the keyboard shortcut help menu, which is accessed via the '?' key.
+/// @note The action block will be retained for the duration of the application. You may want to use weak references.
+/// @note FLEX registers several default keyboard shortcuts. Use the '?' key to see a list of shortcuts.
+- (void)registerSimulatorShortcutWithKey:(NSString *)key modifiers:(UIKeyModifierFlags)modifiers action:(dispatch_block_t)action description:(NSString *)description;
+
 #pragma mark - Extensions
 
 /// Adds an entry at the bottom of the list of Global State items. Call this method before this view controller is displayed.

+ 43 - 0
Classes/ExplorerToolbar/FLEXManager.m

@@ -14,6 +14,7 @@
 #import "FLEXObjectExplorerViewController.h"
 #import "FLEXNetworkObserver.h"
 #import "FLEXNetworkRecorder.h"
+#import "FLEXKeyboardShortcutManager.h"
 
 @interface FLEXManager () <FLEXWindowEventDelegate, FLEXExplorerViewControllerDelegate>
 
@@ -125,6 +126,48 @@
     [self hideExplorer];
 }
 
+#pragma mark - Simulator Shortcuts
+
+- (void)registerSimulatorShortcutWithKey:(NSString *)key modifiers:(UIKeyModifierFlags)modifiers action:(dispatch_block_t)action description:(NSString *)description
+{
+# if TARGET_OS_SIMULATOR
+    [[FLEXKeyboardShortcutManager sharedManager] registerSimulatorShortcutWithKey:key modifiers:modifiers action:action description:description];
+#endif
+}
+
+- (void)setSimulatorShortcutsEnabled:(BOOL)simulatorShortcutsEnabled
+{
+# if TARGET_OS_SIMULATOR
+    [[FLEXKeyboardShortcutManager sharedManager] setEnabled:simulatorShortcutsEnabled];
+#endif
+}
+
+- (BOOL)simulatorShortcutsEnabled
+{
+# if TARGET_OS_SIMULATOR
+    return [[FLEXKeyboardShortcutManager sharedManager] isEnabled];
+#else
+    return NO;
+#endif
+}
+
+- (void)registerDefaultSimulatorShortcuts
+{
+    [self registerSimulatorShortcutWithKey:@"f" modifiers:0 action:^{
+        if ([self isHidden]) {
+            [self showExplorer];
+        } else {
+            [self hideExplorer];
+        }
+    } description:@"Toggle FLEX toolbar"];
+}
+
++ (void)load
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        [[[self class] sharedManager] registerDefaultSimulatorShortcuts];
+    });
+}
 
 #pragma mark - Extensions
 

+ 24 - 0
Classes/Utility/FLEXKeyboardShortcutManager.h

@@ -0,0 +1,24 @@
+//
+//  FLEXKeyboardShortcutManager.h
+//  FLEX
+//
+//  Created by Ryan Olson on 9/19/15.
+//  Copyright © 2015 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+#if TARGET_OS_SIMULATOR
+
+@interface FLEXKeyboardShortcutManager : NSObject
+
++ (instancetype)sharedManager;
+
+- (void)registerSimulatorShortcutWithKey:(NSString *)key modifiers:(UIKeyModifierFlags)modifiers action:(dispatch_block_t)action description:(NSString *)description;
+- (NSString *)keyboardShortcutsDescription;
+
+@property (nonatomic, assign, getter=isEnabled) BOOL enabled;
+
+@end
+
+#endif

+ 243 - 0
Classes/Utility/FLEXKeyboardShortcutManager.m

@@ -0,0 +1,243 @@
+//
+//  FLEXKeyboardShortcutManager.m
+//  FLEX
+//
+//  Created by Ryan Olson on 9/19/15.
+//  Copyright © 2015 Flipboard. All rights reserved.
+//
+
+#import "FLEXKeyboardShortcutManager.h"
+#import "FLEXUtility.h"
+#import <objc/runtime.h>
+#import <objc/message.h>
+
+#if TARGET_OS_SIMULATOR
+
+@interface UIEvent (UIPhysicalKeyboardEvent)
+
+@property (nonatomic, strong) NSString *_modifiedInput;
+@property (nonatomic, strong) NSString *_unmodifiedInput;
+@property (nonatomic, assign) UIKeyModifierFlags _modifierFlags;
+@property (nonatomic, assign) BOOL _isKeyDown;
+
+@end
+
+@interface FLEXKeyInput : NSObject <NSCopying>
+
+@property (nonatomic, copy, readonly) NSString *key;
+@property (nonatomic, assign, readonly) UIKeyModifierFlags flags;
+@property (nonatomic, copy, readonly) NSString *helpDescription;
+
+@end
+
+@implementation FLEXKeyInput
+
+- (BOOL)isEqual:(id)object
+{
+    BOOL isEqual = NO;
+    if ([object isKindOfClass:[FLEXKeyInput class]]) {
+        FLEXKeyInput *keyCommand = (FLEXKeyInput *)object;
+        BOOL equalKeys = self.key == keyCommand.key || [self.key isEqual:keyCommand.key];
+        BOOL equalFlags = self.flags == keyCommand.flags;
+        isEqual = equalKeys && equalFlags;
+    }
+    return isEqual;
+}
+
+- (NSUInteger)hash
+{
+    return [self.key hash] ^ self.flags;
+}
+
+- (id)copyWithZone:(NSZone *)zone
+{
+    return [[self class] keyInputForKey:self.key flags:self.flags helpDescription:self.helpDescription];
+}
+
+- (NSString *)description
+{
+    NSDictionary *keyMappings = @{ UIKeyInputUpArrow : @"↑",
+                                   UIKeyInputDownArrow : @"↓",
+                                   UIKeyInputLeftArrow : @"←",
+                                   UIKeyInputRightArrow : @"→",
+                                   UIKeyInputEscape : @"␛",
+                                   @" " : @"␠"};
+    
+    NSString *prettyKey = nil;
+    if (self.key && [keyMappings objectForKey:self.key]) {
+        prettyKey = [keyMappings objectForKey:self.key];
+    } else {
+        prettyKey = [self.key uppercaseString];
+    }
+    
+    NSString *prettyFlags = @"";
+    if (self.flags & UIKeyModifierControl) {
+        prettyFlags = [prettyFlags stringByAppendingString:@"⌃"];
+    }
+    if (self.flags & UIKeyModifierAlternate) {
+        prettyFlags = [prettyFlags stringByAppendingString:@"⌥"];
+    }
+    if (self.flags & UIKeyModifierShift) {
+        prettyFlags = [prettyFlags stringByAppendingString:@"⇧"];
+    }
+    if (self.flags & UIKeyModifierCommand) {
+        prettyFlags = [prettyFlags stringByAppendingString:@"⌘"];
+    }
+    
+    // Fudging to get easy columns with tabs
+    if ([prettyFlags length] < 2) {
+        prettyKey = [prettyKey stringByAppendingString:@"\t"];
+    }
+    
+    return [NSString stringWithFormat:@"%@%@\t%@", prettyFlags, prettyKey, self.helpDescription];
+}
+
++ (instancetype)keyInputForKey:(NSString *)key flags:(UIKeyModifierFlags)flags
+{
+    return [self keyInputForKey:key flags:flags helpDescription:nil];
+}
+
++ (instancetype)keyInputForKey:(NSString *)key flags:(UIKeyModifierFlags)flags helpDescription:(NSString *)helpDescription
+{
+    FLEXKeyInput *keyInput = [[self alloc] init];
+    if (keyInput) {
+        keyInput->_key = key;
+        keyInput->_flags = flags;
+        keyInput->_helpDescription = helpDescription;
+    }
+    return keyInput;
+}
+
+@end
+
+@interface FLEXKeyboardShortcutManager ()
+
+@property (nonatomic, strong) NSMutableDictionary *actionsForKeyInputs;
+
+@end
+
+@implementation FLEXKeyboardShortcutManager
+
++ (instancetype)sharedManager
+{
+    static FLEXKeyboardShortcutManager *sharedManager = nil;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        sharedManager = [[[self class] alloc] init];
+    });
+    return sharedManager;
+}
+
++ (void)load
+{
+    SEL originalKeyEventSelector = NSSelectorFromString(@"handleKeyUIEvent:");
+    SEL swizzledKeyEventSelector = [FLEXUtility swizzledSelectorForSelector:originalKeyEventSelector];
+    
+    void (^sendEventSwizzleBlock)(UIApplication *, UIEvent *) = ^(UIApplication *slf, UIEvent *event) {
+        
+        [[[self class] sharedManager] handleKeyboardEvent:event];
+        
+        ((void(*)(id, SEL, id))objc_msgSend)(slf, swizzledKeyEventSelector, event);
+    };
+    
+    [FLEXUtility replaceImplementationOfKnownSelector:originalKeyEventSelector onClass:[UIApplication class] withBlock:sendEventSwizzleBlock swizzledSelector:swizzledKeyEventSelector];
+}
+
+- (instancetype)init
+{
+    self = [super init];
+    
+    if (self) {
+        _actionsForKeyInputs = [NSMutableDictionary dictionary];
+        _enabled = YES;
+    }
+    
+    return self;
+}
+
+- (void)registerSimulatorShortcutWithKey:(NSString *)key modifiers:(UIKeyModifierFlags)modifiers action:(dispatch_block_t)action description:(NSString *)description
+{
+    FLEXKeyInput *keyInput = [FLEXKeyInput keyInputForKey:key flags:modifiers helpDescription:description];
+    [self.actionsForKeyInputs setObject:action forKey:keyInput];
+}
+
+- (void)handleKeyboardEvent:(UIEvent *)event
+{
+    if (!self.enabled) {
+        return;
+    }
+    
+    NSString *modifiedInput = nil;
+    NSString *unmodifiedInput = nil;
+    UIKeyModifierFlags flags = 0;
+    BOOL isKeyDown = NO;
+    
+    if ([event respondsToSelector:@selector(_modifiedInput)]) {
+        modifiedInput = [event _modifiedInput];
+    }
+    
+    if ([event respondsToSelector:@selector(_unmodifiedInput)]) {
+        unmodifiedInput = [event _unmodifiedInput];
+    }
+    
+    if ([event respondsToSelector:@selector(_modifierFlags)]) {
+        flags = [event _modifierFlags];
+    }
+    
+    if ([event respondsToSelector:@selector(_isKeyDown)]) {
+        isKeyDown = [event _isKeyDown];
+    }
+    
+    BOOL interactionEnabled = ![[UIApplication sharedApplication] isIgnoringInteractionEvents];
+    
+    if (isKeyDown && [modifiedInput length] > 0 && interactionEnabled) {
+        UIResponder *firstResponder = nil;
+        for (UIWindow *window in [[UIApplication sharedApplication] windows]) {
+            firstResponder = [window valueForKey:@"firstResponder"];
+            if (firstResponder) {
+                break;
+            }
+        }
+        
+        // Ignore key commands (except escape) when there's an active responder
+        if (firstResponder) {
+            if ([unmodifiedInput isEqual:UIKeyInputEscape]) {
+                [firstResponder resignFirstResponder];
+            }
+        } else {
+            FLEXKeyInput *exactMatch = [FLEXKeyInput keyInputForKey:unmodifiedInput flags:flags];
+            
+            dispatch_block_t actionBlock = [self.actionsForKeyInputs objectForKey:exactMatch];
+            
+            if (!actionBlock) {
+                FLEXKeyInput *shiftMatch = [FLEXKeyInput keyInputForKey:modifiedInput flags:flags&(!UIKeyModifierShift)];
+                actionBlock = [self.actionsForKeyInputs objectForKey:shiftMatch];
+            }
+            
+            if (!actionBlock) {
+                FLEXKeyInput *capitalMatch = [FLEXKeyInput keyInputForKey:[unmodifiedInput uppercaseString] flags:flags];
+                actionBlock = [self.actionsForKeyInputs objectForKey:capitalMatch];
+            }
+            
+            if (actionBlock) {
+                actionBlock();
+            }
+        }
+    }
+}
+
+- (NSString *)keyboardShortcutsDescription
+{
+    NSMutableString *description = [NSMutableString string];
+    NSArray *keyInputs = [[self.actionsForKeyInputs allKeys] sortedArrayUsingComparator:^NSComparisonResult(FLEXKeyInput *_Nonnull input1, FLEXKeyInput *_Nonnull input2) {
+        return [input1.key caseInsensitiveCompare:input2.key];
+    }];
+    for (FLEXKeyInput *keyInput in keyInputs) {
+        [description appendFormat:@"%@\n", keyInput];
+    }
+    return [description copy];
+}
+
+@end
+
+#endif

+ 6 - 0
Example/UICatalog.xcodeproj/project.pbxproj

@@ -52,6 +52,7 @@
 		9421B8911A8BBCB200BA3E46 /* FLEXNetworkTransactionTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 9421B8881A8BBCB200BA3E46 /* FLEXNetworkTransactionTableViewCell.m */; };
 		9421B8921A8BBCB200BA3E46 /* FLEXNetworkObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 9421B88B1A8BBCB200BA3E46 /* FLEXNetworkObserver.m */; };
 		9421B8931A8BBCB200BA3E46 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 9421B88C1A8BBCB200BA3E46 /* LICENSE */; };
+		942DCD8A1BAE131500DB5DC2 /* FLEXKeyboardShortcutManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 942DCD891BAE131500DB5DC2 /* FLEXKeyboardShortcutManager.m */; };
 		943203FE1978F42F00E24DB3 /* AAPLCatalogTableTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 943203FD1978F42F00E24DB3 /* AAPLCatalogTableTableViewController.m */; };
 		944F7489197B458C009AB039 /* FLEXArrayExplorerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F7426197B458C009AB039 /* FLEXArrayExplorerViewController.m */; };
 		944F748A197B458C009AB039 /* FLEXClassExplorerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F7428197B458C009AB039 /* FLEXClassExplorerViewController.m */; };
@@ -193,6 +194,8 @@
 		9421B88A1A8BBCB200BA3E46 /* FLEXNetworkObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXNetworkObserver.h; sourceTree = "<group>"; };
 		9421B88B1A8BBCB200BA3E46 /* FLEXNetworkObserver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXNetworkObserver.m; sourceTree = "<group>"; };
 		9421B88C1A8BBCB200BA3E46 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
+		942DCD881BAE131500DB5DC2 /* FLEXKeyboardShortcutManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXKeyboardShortcutManager.h; sourceTree = "<group>"; };
+		942DCD891BAE131500DB5DC2 /* FLEXKeyboardShortcutManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXKeyboardShortcutManager.m; sourceTree = "<group>"; };
 		943203FC1978F42F00E24DB3 /* AAPLCatalogTableTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AAPLCatalogTableTableViewController.h; sourceTree = "<group>"; };
 		943203FD1978F42F00E24DB3 /* AAPLCatalogTableTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AAPLCatalogTableTableViewController.m; sourceTree = "<group>"; };
 		944F7425197B458C009AB039 /* FLEXArrayExplorerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXArrayExplorerViewController.h; sourceTree = "<group>"; };
@@ -544,6 +547,8 @@
 				944F7443197B458C009AB039 /* FLEXUtility.m */,
 				94CB48371A8EC6000054A905 /* FLEXMultilineTableViewCell.h */,
 				94CB48381A8EC6000054A905 /* FLEXMultilineTableViewCell.m */,
+				942DCD881BAE131500DB5DC2 /* FLEXKeyboardShortcutManager.h */,
+				942DCD891BAE131500DB5DC2 /* FLEXKeyboardShortcutManager.m */,
 			);
 			name = Utility;
 			path = ../Classes/Utility;
@@ -772,6 +777,7 @@
 				944F74A2197B458C009AB039 /* FLEXArgumentInputViewFactory.m in Sources */,
 				944F74A1197B458C009AB039 /* FLEXArgumentInputView.m in Sources */,
 				535682B618F3670300BAAD62 /* AAPLSegmentedControlViewController.m in Sources */,
+				942DCD8A1BAE131500DB5DC2 /* FLEXKeyboardShortcutManager.m in Sources */,
 				944F74AE197B458C009AB039 /* FLEXClassesTableViewController.m in Sources */,
 				944F74B1197B458C009AB039 /* FLEXInstancesTableViewController.m in Sources */,
 				944F7497197B458C009AB039 /* FLEXUtility.m in Sources */,

+ 8 - 0
FLEX.xcodeproj/project.pbxproj

@@ -136,6 +136,8 @@
 		3A4C95421B5B21410088C3F2 /* FLEXNetworkObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A4C94C21B5B21410088C3F2 /* FLEXNetworkObserver.h */; settings = {ATTRIBUTES = (Private, ); }; };
 		3A4C95431B5B21410088C3F2 /* FLEXNetworkObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A4C94C31B5B21410088C3F2 /* FLEXNetworkObserver.m */; };
 		3A4C95471B5B217D0088C3F2 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A4C95461B5B217D0088C3F2 /* libz.dylib */; };
+		942DCD871BAE0CA300DB5DC2 /* FLEXKeyboardShortcutManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 942DCD831BAE0AD300DB5DC2 /* FLEXKeyboardShortcutManager.m */; };
+		94AAF03A1BAF2F0300DE8760 /* FLEXKeyboardShortcutManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 942DCD821BAE0AD300DB5DC2 /* FLEXKeyboardShortcutManager.h */; settings = {ATTRIBUTES = (Private, ); }; };
 /* End PBXBuildFile section */
 
 /* Begin PBXFileReference section */
@@ -271,6 +273,8 @@
 		3A4C94C31B5B21410088C3F2 /* FLEXNetworkObserver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXNetworkObserver.m; sourceTree = "<group>"; };
 		3A4C94C41B5B21410088C3F2 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
 		3A4C95461B5B217D0088C3F2 /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; };
+		942DCD821BAE0AD300DB5DC2 /* FLEXKeyboardShortcutManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXKeyboardShortcutManager.h; sourceTree = "<group>"; };
+		942DCD831BAE0AD300DB5DC2 /* FLEXKeyboardShortcutManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXKeyboardShortcutManager.m; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -371,6 +375,8 @@
 				3A4C945C1B5B21410088C3F2 /* FLEXRuntimeUtility.m */,
 				3A4C945D1B5B21410088C3F2 /* FLEXUtility.h */,
 				3A4C945E1B5B21410088C3F2 /* FLEXUtility.m */,
+				942DCD821BAE0AD300DB5DC2 /* FLEXKeyboardShortcutManager.h */,
+				942DCD831BAE0AD300DB5DC2 /* FLEXKeyboardShortcutManager.m */,
 			);
 			path = Utility;
 			sourceTree = "<group>";
@@ -607,6 +613,7 @@
 				3A4C95421B5B21410088C3F2 /* FLEXNetworkObserver.h in Headers */,
 				3A4C95401B5B21410088C3F2 /* FLEXNetworkTransactionTableViewCell.h in Headers */,
 				3A4C95241B5B21410088C3F2 /* FLEXFileBrowserTableViewController.h in Headers */,
+				94AAF03A1BAF2F0300DE8760 /* FLEXKeyboardShortcutManager.h in Headers */,
 				3A4C94E11B5B21410088C3F2 /* FLEXResources.h in Headers */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -679,6 +686,7 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				942DCD871BAE0CA300DB5DC2 /* FLEXKeyboardShortcutManager.m in Sources */,
 				3A4C95121B5B21410088C3F2 /* FLEXPropertyEditorViewController.m in Sources */,
 				3A4C95391B5B21410088C3F2 /* FLEXNetworkRecorder.m in Sources */,
 				3A4C950E1B5B21410088C3F2 /* FLEXIvarEditorViewController.m in Sources */,