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 
swizzlenull19   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     UIKeyCommand(input: "d", modifierFlags: .control, action: #selector(UIResponder.EXDevMenu_toggleDevMenu(_:)))
30   ]
31 }
32 
33 /**
34  Extend `UIResponder` so we can put our key commands to all responders.
35  */
36 extension UIResponder: DevMenuUIResponderExtensionProtocol {
37   // NOTE: throttle the key handler because on iOS the handleKeyCommand:
38   // method gets called repeatedly if the command key is held down.
39   static private var lastKeyCommandExecutionTime: TimeInterval = 0
40   static private var lastKeyCommand: UIKeyCommand?
41 
42   @objc
43   var EXDevMenu_keyCommands: [UIKeyCommand] {
44     if self is UITextField || self is UITextView || String(describing: type(of: self)) == "WKContentView" {
45       return []
46     }
47     let actions = DevMenuManager.shared.devMenuCallable.filter { $0 is DevMenuExportedAction } as! [DevMenuExportedAction]
48     let actionsWithKeyCommands = actions.filter { $0.keyCommand != nil }
49     var keyCommands = actionsWithKeyCommands.map { $0.keyCommand! }
50     keyCommands.insert(contentsOf: DevMenuKeyCommandsInterceptor.globalKeyCommands, at: 0)
51     keyCommands.append(contentsOf: self.EXDevMenu_keyCommands)
52     return keyCommands
53   }
54 
55   @objc
EXDevMenu_handleKeyCommandnull56   public func EXDevMenu_handleKeyCommand(_ key: UIKeyCommand) {
57     tryHandleKeyCommand(key) {
58       let actions = DevMenuManager.shared.devMenuCallable.filter { $0 is DevMenuExportedAction } as! [DevMenuExportedAction]
59       guard let action = actions.first(where: { $0.keyCommand == key }) else {
60         return
61       }
62 
63       if action.isAvailable() {
64         action.call()
65         DevMenuManager.shared.closeMenu()
66       }
67     }
68   }
69 
70   @objc
EXDevMenu_toggleDevMenunull71   func EXDevMenu_toggleDevMenu(_ key: UIKeyCommand) {
72     tryHandleKeyCommand(key) {
73       DevMenuManager.shared.toggleMenu()
74     }
75   }
76 
shouldTriggerActionnull77   private func shouldTriggerAction(_ key: UIKeyCommand) -> Bool {
78     return UIResponder.lastKeyCommand !== key || CACurrentMediaTime() - UIResponder.lastKeyCommandExecutionTime > 0.1
79   }
80 
81   private func tryHandleKeyCommand(_ key: UIKeyCommand, handler: () -> Void ) {
82     if shouldTriggerAction(key) {
83       handler()
84       UIResponder.lastKeyCommand = key
85       UIResponder.lastKeyCommandExecutionTime = CACurrentMediaTime()
86     }
87   }
88 }
89