1 // Copyright 2015-present 650 Industries. All rights reserved.
2 
3 import UIKit
4 import EXDevMenuInterface
5 
6 class DevMenuKeyCommandsInterceptor {
7   /**
8    Returns bool value whether the dev menu key commands are being intercepted.
9    */
10   static var isInstalled: Bool = false {
11     willSet {
12       if isInstalled != newValue {
13         // Capture touch gesture from any window by swizzling default implementation from UIWindow.
14         swizzle()
15       }
16     }
17   }
18 
19   static private func swizzle() {
20     DevMenuUtils.swizzle(
21       selector: #selector(getter: UIResponder.keyCommands),
22       withSelector: #selector(getter: UIResponder.EXDevMenu_keyCommands),
23       forClass: UIResponder.self
24     )
25   }
26 
27   static let globalKeyCommands: [UIKeyCommand] = [
28     UIKeyCommand(input: "d", modifierFlags: .command, action: #selector(UIResponder.EXDevMenu_toggleDevMenu(_:)))
29   ]
30 }
31 
32 /**
33  Extend `UIResponder` so we can put our key commands to all responders.
34  */
35 extension UIResponder: DevMenuUIResponderExtensionProtocol {
36   // NOTE: throttle the key handler because on iOS the handleKeyCommand:
37   // method gets called repeatedly if the command key is held down.
38   static private var lastKeyCommandExecutionTime: TimeInterval = 0
39   static private var lastKeyCommand: UIKeyCommand?
40 
41   @objc
42   var EXDevMenu_keyCommands: [UIKeyCommand] {
43     let actions = DevMenuManager.shared.devMenuCallable.filter { $0 is DevMenuExportedAction } as! [DevMenuExportedAction]
44     let actionsWithKeyCommands = actions.filter { $0.keyCommand != nil }
45     var keyCommands = actionsWithKeyCommands.map { $0.keyCommand! }
46     keyCommands.insert(contentsOf: DevMenuKeyCommandsInterceptor.globalKeyCommands, at: 0)
47     keyCommands.append(contentsOf: self.EXDevMenu_keyCommands)
48     return keyCommands
49   }
50 
51   @objc
52   public func EXDevMenu_handleKeyCommand(_ key: UIKeyCommand) {
53     tryHandleKeyCommand(key) {
54       let actions = DevMenuManager.shared.devMenuCallable.filter { $0 is DevMenuExportedAction } as! [DevMenuExportedAction]
55       guard let action = actions.first(where: { $0.keyCommand == key }) else {
56         return
57       }
58 
59       if action.isAvailable() {
60         action.call()
61         DevMenuManager.shared.closeMenu()
62       }
63     }
64   }
65 
66   @objc
67   func EXDevMenu_toggleDevMenu(_ key: UIKeyCommand) {
68     tryHandleKeyCommand(key) {
69       DevMenuManager.shared.toggleMenu()
70     }
71   }
72 
73   private func shouldTriggerAction(_ key: UIKeyCommand) -> Bool {
74     return UIResponder.lastKeyCommand !== key || CACurrentMediaTime() - UIResponder.lastKeyCommandExecutionTime > 0.5
75   }
76 
77   private func tryHandleKeyCommand(_ key: UIKeyCommand, handler: () -> Void ) {
78     if shouldTriggerAction(key) {
79       handler()
80       UIResponder.lastKeyCommand = key
81       UIResponder.lastKeyCommandExecutionTime = CACurrentMediaTime()
82     }
83   }
84 }
85