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     if self is UITextField || self is UITextView || String(describing: type(of: self)) == "WKContentView" {
44       return []
45     }
46     let actions = DevMenuManager.shared.devMenuCallable.filter { $0 is DevMenuExportedAction } as! [DevMenuExportedAction]
47     let actionsWithKeyCommands = actions.filter { $0.keyCommand != nil }
48     var keyCommands = actionsWithKeyCommands.map { $0.keyCommand! }
49     keyCommands.insert(contentsOf: DevMenuKeyCommandsInterceptor.globalKeyCommands, at: 0)
50     keyCommands.append(contentsOf: self.EXDevMenu_keyCommands)
51     return keyCommands
52   }
53 
54   @objc
55   public func EXDevMenu_handleKeyCommand(_ key: UIKeyCommand) {
56     tryHandleKeyCommand(key) {
57       let actions = DevMenuManager.shared.devMenuCallable.filter { $0 is DevMenuExportedAction } as! [DevMenuExportedAction]
58       guard let action = actions.first(where: { $0.keyCommand == key }) else {
59         return
60       }
61 
62       if action.isAvailable() {
63         action.call()
64         DevMenuManager.shared.closeMenu()
65       }
66     }
67   }
68 
69   @objc
70   func EXDevMenu_toggleDevMenu(_ key: UIKeyCommand) {
71     tryHandleKeyCommand(key) {
72       DevMenuManager.shared.toggleMenu()
73     }
74   }
75 
76   private func shouldTriggerAction(_ key: UIKeyCommand) -> Bool {
77     return UIResponder.lastKeyCommand !== key || CACurrentMediaTime() - UIResponder.lastKeyCommandExecutionTime > 0.5
78   }
79 
80   private func tryHandleKeyCommand(_ key: UIKeyCommand, handler: () -> Void ) {
81     if shouldTriggerAction(key) {
82       handler()
83       UIResponder.lastKeyCommand = key
84       UIResponder.lastKeyCommandExecutionTime = CACurrentMediaTime()
85     }
86   }
87 }
88