Skip to content

Commit

Permalink
Add new toast UI and operation hint message (PlayCover#82)
Browse files Browse the repository at this point in the history
* add new hint toast

* limit toast number to four
  • Loading branch information
XuYicong authored and IsaacMarovitz committed May 8, 2023
1 parent dd1518f commit b70a41c
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 93 deletions.
12 changes: 12 additions & 0 deletions PlayTools/Controls/ControlMode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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()
}
Expand All @@ -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")
}
141 changes: 50 additions & 91 deletions PlayTools/Controls/PlayInput.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -86,42 +50,35 @@ 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)
}
}
}
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 {
Expand All @@ -145,29 +102,36 @@ 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
}
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)
}
}
}

Expand All @@ -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()
}
Expand All @@ -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()
}
}
7 changes: 5 additions & 2 deletions PlayTools/Keymap/EditorController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
95 changes: 95 additions & 0 deletions PlayTools/Utils/Toast.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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..<hintView.count {
if index < id {
UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseOut, animations: {
hintView[index].layer.position.y -= hint.frame.size.height + gap
})
} else if index > 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

Expand Down

0 comments on commit b70a41c

Please sign in to comment.