Skip to content

Commit

Permalink
Add support for keyboard shortcuts in the simulator
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanolsonk committed Sep 21, 2015
1 parent b453086 commit b22d2b5
Show file tree
Hide file tree
Showing 6 changed files with 341 additions and 0 deletions.
17 changes: 17 additions & 0 deletions Classes/ExplorerToolbar/FLEXManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
43 changes: 43 additions & 0 deletions Classes/ExplorerToolbar/FLEXManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#import "FLEXObjectExplorerViewController.h"
#import "FLEXNetworkObserver.h"
#import "FLEXNetworkRecorder.h"
#import "FLEXKeyboardShortcutManager.h"

@interface FLEXManager () <FLEXWindowEventDelegate, FLEXExplorerViewControllerDelegate>

Expand Down Expand Up @@ -125,6 +126,48 @@ - (void)explorerViewControllerDidFinish:(FLEXExplorerViewController *)explorerVi
[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

Expand Down
24 changes: 24 additions & 0 deletions Classes/Utility/FLEXKeyboardShortcutManager.h
Original file line number Diff line number Diff line change
@@ -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 changes: 243 additions & 0 deletions Classes/Utility/FLEXKeyboardShortcutManager.m
Original file line number Diff line number Diff line change
@@ -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 changes: 6 additions & 0 deletions Example/UICatalog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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>"; };
Expand Down Expand Up @@ -544,6 +547,8 @@
944F7443197B458C009AB039 /* FLEXUtility.m */,
94CB48371A8EC6000054A905 /* FLEXMultilineTableViewCell.h */,
94CB48381A8EC6000054A905 /* FLEXMultilineTableViewCell.m */,
942DCD881BAE131500DB5DC2 /* FLEXKeyboardShortcutManager.h */,
942DCD891BAE131500DB5DC2 /* FLEXKeyboardShortcutManager.m */,
);
name = Utility;
path = ../Classes/Utility;
Expand Down Expand Up @@ -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 */,
Expand Down
Loading

0 comments on commit b22d2b5

Please sign in to comment.