diff --git a/PlayTools/Controls/ControlMode.swift b/PlayTools/Controls/ControlMode.swift index 35661efb..df298807 100644 --- a/PlayTools/Controls/ControlMode.swift +++ b/PlayTools/Controls/ControlMode.swift @@ -16,6 +16,8 @@ public class ControlMode { if !editor.editorMode { if show { if !visible { + NotificationCenter.default.post(name: NSNotification.Name.playtoolsKeymappingWillDisable, + object: nil, userInfo: [:]) if screen.fullscreen { screen.switchDock(true) } @@ -26,6 +28,8 @@ public class ControlMode { } } else { if visible { + NotificationCenter.default.post(name: NSNotification.Name.playtoolsKeymappingWillEnable, + object: nil, userInfo: [:]) if PlaySettings.shared.mouseMapping { AKInterface.shared!.hideCursor() } @@ -40,3 +44,11 @@ public class ControlMode { } } } + +extension NSNotification.Name { + public static let playtoolsKeymappingWillEnable: NSNotification.Name + = NSNotification.Name("playtools.keymappingWillEnable") + + public static let playtoolsKeymappingWillDisable: NSNotification.Name + = NSNotification.Name("playtools.keymappingWillDisable") +} diff --git a/PlayTools/Controls/PlayInput.swift b/PlayTools/Controls/PlayInput.swift index e541c285..6b031afd 100644 --- a/PlayTools/Controls/PlayInput.swift +++ b/PlayTools/Controls/PlayInput.swift @@ -10,47 +10,11 @@ class PlayInput { static private var lCmdPressed = false static private var rCmdPressed = false - static public var buttonHandlers: [String: [(Bool) -> Void]] = [:] - func invalidate() { + PlayMice.shared.stop() for action in self.actions { action.invalidate() } - PlayInput.buttonHandlers.removeAll(keepingCapacity: true) - GCController.current?.extendedGamepad?.valueChangedHandler = nil - } - - static public func registerButton(key: String, handler: @escaping (Bool) -> Void) { - if PlayInput.buttonHandlers[key] == nil { - PlayInput.buttonHandlers[key] = [] - } - PlayInput.buttonHandlers[key]!.append(handler) - } - - func keyboardHandler(_ keyCode: UInt16, _ pressed: Bool) { - let name = KeyCodeNames.virtualCodes[keyCode] ?? "Btn" - guard let handlers = PlayInput.buttonHandlers[name] else { - return - } - for handler in handlers { - handler(pressed) - } - } - - func controllerButtonHandler(_ profile: GCExtendedGamepad, _ element: GCControllerElement) { - let name: String = element.aliases.first! - if let buttonElement = element as? GCControllerButtonInput { -// Toast.showOver(msg: "recognised controller button: \(name)") - guard let handlers = PlayInput.buttonHandlers[name] else { return } - Toast.showOver(msg: name + ": \(buttonElement.isPressed)") - for handler in handlers { - handler(buttonElement.isPressed) - } - } else if let dpadElement = element as? GCControllerDirectionPad { - PlayMice.shared.handleControllerDirectionPad(profile, dpadElement) - } else { - Toast.showOver(msg: "unrecognised controller element input happens") - } } func parseKeymap() { @@ -86,8 +50,7 @@ class PlayInput { keyboard.keyChangedHandler = { _, _, keyCode, _ in if !PlayInput.cmdPressed() && !PlayInput.FORBIDDEN.contains(keyCode) - && self.isSafeToBind(keyboard) - && KeyCodeNames.keyCodes[keyCode.rawValue] != nil { + && self.isSafeToBind(keyboard) { EditorController.shared.setKey(keyCode.rawValue) } } @@ -95,33 +58,27 @@ class PlayInput { if let controller = GCController.current?.extendedGamepad { controller.valueChangedHandler = { _, element in // This is the index of controller buttons, which is String, not Int - var alias: String = element.aliases.first! - if alias == "Direction Pad" { - guard let dpadElement = element as? GCControllerDirectionPad else { - Toast.showOver(msg: "cannot map direction pad: element type not recognizable") - return - } - if dpadElement.xAxis.value > 0 { - alias = dpadElement.right.aliases.first! - } else if dpadElement.xAxis.value < 0 { - alias = dpadElement.left.aliases.first! - } - if dpadElement.yAxis.value > 0 { - alias = dpadElement.down.aliases.first! - } else if dpadElement.yAxis.value < 0 { - alias = dpadElement.up.aliases.first! - } - } + let alias: String! = element.aliases.first EditorController.shared.setKey(alias) } } + } else { + GCKeyboard.coalesced!.keyboardInput!.keyChangedHandler = nil + GCController.current?.extendedGamepad?.valueChangedHandler = nil } } func setup() { parseKeymap() - GCKeyboard.coalesced?.keyboardInput?.keyChangedHandler = nil - GCController.current?.extendedGamepad?.valueChangedHandler = controllerButtonHandler + + for mouse in GCMouse.mice() { + if settings.mouseMapping { + mouse.mouseInput?.mouseMovedHandler = PlayMice.shared.handleMouseMoved + } else { + mouse.mouseInput?.mouseMovedHandler = PlayMice.shared.handleFakeMouseMoved + } + } + } static public func cmdPressed() -> Bool { @@ -145,21 +102,23 @@ class PlayInput { .printScreen ] - private func swapMode() { + private func swapMode(_ pressed: Bool) { if !settings.mouseMapping { return } - if !mode.visible { - self.invalidate() + if pressed { + if !mode.visible { + self.invalidate() + } + mode.show(!mode.visible) } - mode.show(!mode.visible) } var root: UIViewController? { return screen.window?.rootViewController } - func setupHotkeys() { + func setupShortcuts() { if let keyboard = GCKeyboard.coalesced?.keyboardInput { keyboard.button(forKeyCode: .leftGUI)?.pressedChangedHandler = { _, _, pressed in PlayInput.lCmdPressed = pressed @@ -167,7 +126,12 @@ class PlayInput { keyboard.button(forKeyCode: .rightGUI)?.pressedChangedHandler = { _, _, pressed in PlayInput.rCmdPressed = pressed } - // TODO: set a timeout to display usage guide of Option and Keymapping menu in turn + keyboard.button(forKeyCode: .leftAlt)?.pressedChangedHandler = { _, _, pressed in + self.swapMode(pressed) + } + keyboard.button(forKeyCode: .rightAlt)?.pressedChangedHandler = { _, _, pressed in + self.swapMode(pressed) + } } } @@ -180,7 +144,7 @@ class PlayInput { let main = OperationQueue.main centre.addObserver(forName: NSNotification.Name.GCKeyboardDidConnect, object: nil, queue: main) { _ in - self.setupHotkeys() + self.setupShortcuts() if !mode.visible { self.setup() } @@ -196,36 +160,31 @@ class PlayInput { if !mode.visible { self.setup() } - if EditorController.shared.editorMode { - self.toggleEditor(show: true) - } } - centre.addObserver(forName: NSNotification.Name(rawValue: "NSWindowDidBecomeKeyNotification"), object: nil, - queue: main) { _ in - if !mode.visible && settings.mouseMapping { - AKInterface.shared!.warpCursor() + setupShortcuts() + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) { + if !settings.mouseMapping || !mode.visible { + return + } + Toast.showHint(title: "Keymapping Disabled", text: ["Press ", "option ⌥", " to enable keymapping"], + notification: NSNotification.Name.playtoolsKeymappingWillEnable) + let center = NotificationCenter.default + var token: NSObjectProtocol? + token = center.addObserver(forName: NSNotification.Name.playtoolsKeymappingWillEnable, + object: nil, queue: OperationQueue.main) { _ in + center.removeObserver(token!) + Toast.showHint(title: "Keymapping Enabled", text: ["Press ", "option ⌥", " to disable keymapping"], + notification: NSNotification.Name.playtoolsKeymappingWillDisable) } } - setupHotkeys() - AKInterface.shared!.initialize(keyboard: {keycode, pressed in - let consumed = !mode.visible && !PlayInput.cmdPressed() - if !consumed { - return false - } - self.keyboardHandler(keycode, pressed) - return consumed - }, mouseMoved: {deltaX, deltaY in - if mode.visible { - return false - } - if settings.mouseMapping { - PlayMice.shared.handleMouseMoved(deltaX: deltaX, deltaY: deltaY) - } else { - PlayMice.shared.handleFakeMouseMoved(deltaX: deltaX, deltaY: deltaY) - } - return true - }, swapMode: self.swapMode) + // Fix beep sound + AKInterface.shared! + .eliminateRedundantKeyPressEvents(self.dontIgnore) + } + + func dontIgnore() -> Bool { + (mode.visible && !EditorController.shared.editorMode) || PlayInput.cmdPressed() } } diff --git a/PlayTools/Keymap/EditorController.swift b/PlayTools/Keymap/EditorController.swift index 1fe47c66..c6f7075e 100644 --- a/PlayTools/Keymap/EditorController.swift +++ b/PlayTools/Keymap/EditorController.swift @@ -60,14 +60,17 @@ class EditorController { previousWindow?.makeKeyAndVisible() PlayInput.shared.toggleEditor(show: false) focusedControl = nil - Toast.showOver(msg: "Keymapping saved") + Toast.showHint(title: "Keymap Saved") } else { PlayInput.shared.toggleEditor(show: true) previousWindow = screen.keyWindow editorWindow = initWindow() editorWindow?.makeKeyAndVisible() showButtons() - Toast.showOver(msg: "Click to start keymmaping edit") + Toast.showHint(title: "Keymapping Editor", + text: ["Click a button to edit its position or key bind\n" + + "Click an empty area to open input menu"], + notification: NSNotification.Name.playtoolsKeymappingWillEnable) } // Toast.showOver(msg: "\(UIApplication.shared.windows.count)") lock.unlock() diff --git a/PlayTools/Utils/Toast.swift b/PlayTools/Utils/Toast.swift index 4bf90244..44fba46a 100644 --- a/PlayTools/Utils/Toast.swift +++ b/PlayTools/Utils/Toast.swift @@ -12,6 +12,101 @@ class Toast { Toast.show(message: msg, parent: parent) } } + static var hintView: [UIView] = [] + + private static let gap: CGFloat = 40 + + public static func hideHint(hint: UIView) { + guard let id = hintView.firstIndex(of: hint) else {return} + for index in 0.. id { + hintView[index-1] = hintView[index] + } + } + hintView.removeLast() + UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseOut, animations: { + hint.alpha = 0.0 + }, completion: {_ in + hint.removeFromSuperview() + }) + } + + private static func getAttributedString(title: String, text: [String]) -> NSMutableAttributedString { + var heading = title + if !text.isEmpty { + heading += "\n" + } + let txt = NSMutableAttributedString(string: text.reduce(into: heading, { result, string in + result += string + })) + var messageLength = 0 + var highlight = false + for msg in text { + txt.addAttribute(.foregroundColor, value: highlight ? UIColor.cyan: UIColor.white, + range: NSRange(location: heading.count + messageLength, length: msg.count)) + highlight = !highlight + messageLength += msg.count + } + let style = NSMutableParagraphStyle() + style.alignment = .center + txt.addAttribute(.paragraphStyle, value: style, + range: NSRange(location: 0, length: heading.count + messageLength)) + txt.addAttribute(.font, value: UIFont.systemFont(ofSize: 28, weight: .bold), + range: NSRange(location: 0, length: heading.count)) + txt.addAttribute(.foregroundColor, value: UIColor.white, + range: NSRange(location: 0, length: heading.count)) + txt.addAttribute(.font, value: UIFont.systemFont(ofSize: 28), + range: NSRange(location: heading.count, length: messageLength)) + return txt + } + + public static func showHint(title: String, text: [String] = [], timeout: Double = 3, + notification: NSNotification.Name? = nil) { + let parent = screen.keyWindow! + + // Width and height here serve as an upper limit. + // Text would fill width first, then wrap, then fill height, then scroll + let messageLabel = UITextView(frame: CGRect(x: 0, y: 0, width: 800, height: 800)) + messageLabel.attributedText = getAttributedString(title: title, text: text) + messageLabel.backgroundColor = UIColor.black.withAlphaComponent(0.5) + messageLabel.alpha = 1.0 + messageLabel.clipsToBounds = true + messageLabel.isUserInteractionEnabled = false + messageLabel.frame.size = messageLabel.sizeThatFits(messageLabel.frame.size) + messageLabel.layer.cornerCurve = CALayerCornerCurve.continuous + messageLabel.layer.cornerRadius = messageLabel.frame.size.height / 4 + messageLabel.frame.size.width += messageLabel.layer.cornerRadius * 2 + messageLabel.center.x = parent.center.x + messageLabel.center.y = -messageLabel.frame.size.height / 2 + + hintView.append(messageLabel) + parent.addSubview(messageLabel) + + if hintView.count > 4 { + hideHint(hint: hintView.first!) + } + if let note = notification { + let center = NotificationCenter.default + var token: NSObjectProtocol? + token = center.addObserver(forName: note, object: nil, queue: OperationQueue.main) { _ in + center.removeObserver(token!) + hideHint(hint: messageLabel) + } + } else { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5 + timeout) { + hideHint(hint: messageLabel) + } + } + for view in hintView { + UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseIn, animations: { + view.layer.position.y += messageLabel.frame.size.height + gap + }) + } + } // swiftlint:disable function_body_length