Skip to content

Commit

Permalink
Merge pull request #133 from PlayCover/3.0.0-staging
Browse files Browse the repository at this point in the history
Adding staging changes for RC2
  • Loading branch information
JoseMoreville committed Dec 12, 2023
2 parents 7aff631 + 7cd418f commit 13d28b7
Show file tree
Hide file tree
Showing 35 changed files with 1,616 additions and 703 deletions.
24 changes: 14 additions & 10 deletions AKPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ class AKPlugin: NSObject, Plugin {
}

var cmdPressed: Bool = false

var cursorHideLevel = 0
func hideCursor() {
NSCursor.hide()
cursorHideLevel += 1
CGAssociateMouseAndMouseCursorPosition(0)
warpCursor()
}
Expand All @@ -54,7 +55,10 @@ class AKPlugin: NSObject, Plugin {

func unhideCursor() {
NSCursor.unhide()
CGAssociateMouseAndMouseCursorPosition(1)
cursorHideLevel -= 1
if cursorHideLevel <= 0 {
CGAssociateMouseAndMouseCursorPosition(1)
}
}

func terminateApplication() {
Expand Down Expand Up @@ -114,7 +118,7 @@ class AKPlugin: NSObject, Plugin {
})
}

func setupMouseMoved(mouseMoved: @escaping(CGFloat, CGFloat) -> Bool) {
func setupMouseMoved(_ mouseMoved: @escaping(CGFloat, CGFloat) -> Bool) {
let mask: NSEvent.EventTypeMask = [.leftMouseDragged, .otherMouseDragged, .rightMouseDragged]
NSEvent.addLocalMonitorForEvents(matching: mask, handler: { event in
let consumed = mouseMoved(event.deltaX, event.deltaY)
Expand All @@ -130,24 +134,24 @@ class AKPlugin: NSObject, Plugin {
})
}

func setupMouseButton(left: Bool, right: Bool, _ dontIgnore: @escaping(Bool) -> Bool) {
func setupMouseButton(left: Bool, right: Bool, _ consumed: @escaping(Int, Bool) -> Bool) {
let downType: NSEvent.EventTypeMask = left ? .leftMouseDown : right ? .rightMouseDown : .otherMouseDown
let upType: NSEvent.EventTypeMask = left ? .leftMouseUp : right ? .rightMouseUp : .otherMouseUp
NSEvent.addLocalMonitorForEvents(matching: downType, handler: { event in
// For traffic light buttons when fullscreen
if event.window != NSApplication.shared.windows.first! {
return event
}
if dontIgnore(true) {
return event
if consumed(event.buttonNumber, true) {
return nil
}
return nil
return event
})
NSEvent.addLocalMonitorForEvents(matching: upType, handler: { event in
if dontIgnore(false) {
return event
if consumed(event.buttonNumber, false) {
return nil
}
return nil
return event
})
}

Expand Down
180 changes: 160 additions & 20 deletions PlayTools.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

206 changes: 206 additions & 0 deletions PlayTools/Controls/ActionDispatcher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
//
// ActionDispatcher.swift
// PlayTools
//
// Created by 许沂聪 on 2023/9/16.
//

import Foundation
import Atomics

// If the same key is mapped to multiple different tasks, distinguish by priority
public enum ActionDispatchPriority: Int {
case DRAGGABLE
case DEFAULT
case CAMERA
}

// This class reads keymap and thereby dispatch events

public class ActionDispatcher {
static private let keymapVersion = "2.0."
static private var actions = [Action]()
static private var buttonHandlers: [String: [(Bool) -> Void]] = [:]

static private let PRIORITY_COUNT = 3
// You can't put more than 8 cameras or 8 joysticks in a keymap right?
static private let MAPPING_COUNT_PER_PRIORITY = 8
static private let directionPadHandlers: [[ManagedAtomic<AtomicHandler>]] = Array(
(0..<PRIORITY_COUNT).map({_ in
(0..<MAPPING_COUNT_PER_PRIORITY).map({_ in ManagedAtomic<AtomicHandler>(.EMPTY)})
})
)

static private func clear() {
invalidateActions()
actions = []
buttonHandlers.removeAll(keepingCapacity: true)
directionPadHandlers.forEach({ handlers in
handlers.forEach({ handler in
handler.store(.EMPTY, ordering: .relaxed)
})
})
}

// Backend interfaces

// This should be called whenever keymap may change
static public func build() {
clear()

actions.append(FakeMouseAction())

// current keymap version is 2.0.x.
// in future, keymap format will be upgraded.
// PlayTools would maintain limited backwards compatibility.
// Meanwhile, keymap format upgrade would be rare.
if !keymap.keymapData.version.hasPrefix(keymapVersion) {
DispatchQueue.main.asyncAfter(
deadline: .now() + .seconds(5)) {
Toast.showHint(title: "Keymap format too new",
text: ["Current keymap version \(keymap.keymapData.version)" +
" is too new and cannot be recognized\n" +
"For protection of your data, keymap is not loaded\n" +
"Please upgrade PlayCover, " +
"or import an older version of keymap (requires \(keymapVersion)x"])
}
return
}

for button in keymap.keymapData.buttonModels {
actions.append(ButtonAction(data: button))
}

for draggableButton in keymap.keymapData.draggableButtonModels {
actions.append(DraggableButtonAction(data: draggableButton))
}

for mouse in keymap.keymapData.mouseAreaModel {
actions.append(CameraAction(data: mouse))
}

for joystick in keymap.keymapData.joystickModel {
// Left Thumbstick, Right Thumbstick, Mouse
if joystick.keyName.contains(Character("u")) {
actions.append(ContinuousJoystickAction(data: joystick))
} else { // Keyboard
actions.append(JoystickAction(data: joystick))
}
}
// `cursorHideNecessary` is used to disable `option` toggle when there is no mouse mapping
// but in the case this new feature disabled, `option` should always function.
// this variable is set here to be checked for mouse mapping later.
cursorHideNecessary =
(getDispatchPriority(key: KeyCodeNames.leftMouseButton) ?? .DRAGGABLE) != .DRAGGABLE ||
(getDispatchPriority(key: KeyCodeNames.mouseMove) ?? .DRAGGABLE) != .DRAGGABLE
}

static public func register(key: String, handler: @escaping (Bool) -> Void) {
// this function is called when setting up `button` type of mapping
if buttonHandlers[key] == nil {
buttonHandlers[key] = []
}
buttonHandlers[key]!.append(handler)
}

static public func register(key: String,
handler: @escaping (CGFloat, CGFloat) -> Void,
priority: ActionDispatchPriority = .DEFAULT) {
let atomicHandler = directionPadHandlers[priority.rawValue].first(where: { handler in
handler.load(ordering: .relaxed).key == key
}) ??
directionPadHandlers[priority.rawValue].first(where: { handler in
handler.load(ordering: .relaxed).key.isEmpty
})
// DispatchQueue.main.async {
// if screen.keyWindow == nil {
// return
// }
// Toast.showHint(title: "register",
// text: ["key: \(key), atomicHandler: \(String(describing: atomicHandler))"])
// }
atomicHandler?.store(AtomicHandler(key, handler), ordering: .releasing)
}

static public func unregister(key: String) {
// Only draggable can be unregistered
let atomicHandler = directionPadHandlers[ActionDispatchPriority.DRAGGABLE.rawValue].first(where: { handler in
handler.load(ordering: .relaxed).key == key
})
// DispatchQueue.main.async {
// if screen.keyWindow == nil {
// return
// }
// Toast.showHint(title: "unregister",
// text: ["key: \(key), atomicHandler: \(String(describing: atomicHandler))"])
// }
atomicHandler?.store(.EMPTY, ordering: .releasing)
}

// Frontend interfaces

static public var cursorHideNecessary = true

static public func invalidateActions() {
for action in actions {
// This is just a rescue feature, in case any key stuck pressed for any reason
// Might be called on control mode state transition
action.invalidate()
}
}

static public func getDispatchPriority(key: String) -> ActionDispatchPriority? {
if let priority = directionPadHandlers.firstIndex(where: { handlers in
handlers.contains(where: { handler in
handler.load(ordering: .acquiring).key == key
})
}) {
// Toast.showHint(title: "\(key) priority", text: ["\(priority)"])
return ActionDispatchPriority(rawValue: priority)
}

if buttonHandlers[key] != nil {
return .DEFAULT
}
return nil
}

static public func dispatch(key: String, pressed: Bool) -> Bool {
guard let handlers = buttonHandlers[key] else {
return false
}
var mapped = false
for handler in handlers {
PlayInput.touchQueue.async(qos: .userInteractive, execute: {
handler(pressed)
})
mapped = true
}
// return value matters. A false value makes a beep sound
return mapped
}

static public func dispatch(key: String, valueX: CGFloat, valueY: CGFloat) -> Bool {
for priority in 0..<PRIORITY_COUNT {
if let handler = directionPadHandlers[priority].first(where: { handler in
handler.load(ordering: .acquiring).key == key
}) {
PlayInput.touchQueue.async(qos: .userInteractive, execute: {
handler.load(ordering: .relaxed).handle(valueX, valueY)
})
return true
}
}
return false
}
}

private final class AtomicHandler: AtomicReference {
static fileprivate let EMPTY = AtomicHandler("", {_, _ in })
let key: String
let handle: (CGFloat, CGFloat) -> Void
init(_ key: String, _ handle: @escaping (CGFloat, CGFloat) -> Void) {
self.key = key
self.handle = handle
}
}
Loading

0 comments on commit 13d28b7

Please sign in to comment.