diff --git a/AKPlugin.swift b/AKPlugin.swift index ee9e72b1..7a6a1341 100644 --- a/AKPlugin.swift +++ b/AKPlugin.swift @@ -38,9 +38,10 @@ class AKPlugin: NSObject, Plugin { } var cmdPressed: Bool = false - + var cursorHideLevel = 0 func hideCursor() { NSCursor.hide() + cursorHideLevel += 1 CGAssociateMouseAndMouseCursorPosition(0) warpCursor() } @@ -54,7 +55,10 @@ class AKPlugin: NSObject, Plugin { func unhideCursor() { NSCursor.unhide() - CGAssociateMouseAndMouseCursorPosition(1) + cursorHideLevel -= 1 + if cursorHideLevel <= 0 { + CGAssociateMouseAndMouseCursorPosition(1) + } } func terminateApplication() { @@ -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) @@ -130,7 +134,7 @@ 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 @@ -138,16 +142,16 @@ class AKPlugin: NSObject, Plugin { 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 }) } diff --git a/PlayTools.xcodeproj/project.pbxproj b/PlayTools.xcodeproj/project.pbxproj index 7bdd955b..0f5ed15c 100644 --- a/PlayTools.xcodeproj/project.pbxproj +++ b/PlayTools.xcodeproj/project.pbxproj @@ -15,12 +15,27 @@ 6E84A14528D0F94E00BF7495 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA818CBA287ABFD5000BEE9D /* UIKit.framework */; }; 6E84A15028D0F97500BF7495 /* AKInterface.bundle in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6E84A14C28D0F96D00BF7495 /* AKInterface.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 951D8275299D097C00D35B20 /* Playtools.strings in Resources */ = {isa = PBXBuildFile; fileRef = 951D8277299D097C00D35B20 /* Playtools.strings */; }; - 95A553E729F2BBB200E34C26 /* PlayController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95A553E629F2BBB200E34C26 /* PlayController.swift */; }; + 9562D1512AB484C7002C329D /* EventAdapters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9562D1502AB484C7002C329D /* EventAdapters.swift */; }; + 9562D1532AB484FD002C329D /* EventAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9562D1522AB484FD002C329D /* EventAdapter.swift */; }; + 9562D1582AB4FB9B002C329D /* TouchscreenKeyboardEventAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9562D1572AB4FB9B002C329D /* TouchscreenKeyboardEventAdapter.swift */; }; + 9562D15A2AB4FBBB002C329D /* TransparentKeyboardEventAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9562D1592AB4FBBB002C329D /* TransparentKeyboardEventAdapter.swift */; }; + 9562D15C2AB4FC79002C329D /* TransparentMouseEventAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9562D15B2AB4FC79002C329D /* TransparentMouseEventAdapter.swift */; }; + 9562D15E2AB4FCB8002C329D /* TouchscreenControllerEventAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9562D15D2AB4FCB8002C329D /* TouchscreenControllerEventAdapter.swift */; }; + 9562D1602AB4FD46002C329D /* TransparentControllerEventAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9562D15F2AB4FD46002C329D /* TransparentControllerEventAdapter.swift */; }; + 9562D1622AB4FE73002C329D /* CameraControlMouseEventAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9562D1612AB4FE73002C329D /* CameraControlMouseEventAdapter.swift */; }; + 9562D1642AB4FEB4002C329D /* TouchscreenMouseEventAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9562D1632AB4FEB4002C329D /* TouchscreenMouseEventAdapter.swift */; }; + 9562D1662AB5049B002C329D /* KeyboardEventAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9562D1652AB5049B002C329D /* KeyboardEventAdapter.swift */; }; + 9562D16B2AB505AD002C329D /* MouseEventAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9562D16A2AB505AD002C329D /* MouseEventAdapter.swift */; }; + 9562D16D2AB505D4002C329D /* ControllerEventAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9562D16C2AB505D4002C329D /* ControllerEventAdapter.swift */; }; + 9562D16F2AB50D34002C329D /* ActionDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9562D16E2AB50D34002C329D /* ActionDispatcher.swift */; }; + 9562D1732AB52416002C329D /* EditorKeyboardEventAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9562D1722AB52416002C329D /* EditorKeyboardEventAdapter.swift */; }; + 9562D1752AB52479002C329D /* EditorMouseEventAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9562D1742AB52479002C329D /* EditorMouseEventAdapter.swift */; }; + 9562D1772AB52550002C329D /* EditorControllerEventAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9562D1762AB52550002C329D /* EditorControllerEventAdapter.swift */; }; + 9562D1792AB64458002C329D /* ModeAutomaton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9562D1782AB64458002C329D /* ModeAutomaton.swift */; }; AA71970B287A44D200623C15 /* PlayLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = AA719702287A44D200623C15 /* PlayLoader.m */; }; AA71970D287A44D200623C15 /* PlaySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA719704287A44D200623C15 /* PlaySettings.swift */; }; AA71970E287A44D200623C15 /* PlayLoader.h in Headers */ = {isa = PBXBuildFile; fileRef = AA719705287A44D200623C15 /* PlayLoader.h */; }; AA71970F287A44D200623C15 /* PlayCover.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA719706287A44D200623C15 /* PlayCover.swift */; }; - AA719758287A480D00623C15 /* PlayMice.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA719722287A480C00623C15 /* PlayMice.swift */; }; AA719759287A480D00623C15 /* PlayAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA719723287A480C00623C15 /* PlayAction.swift */; }; AA71975A287A480D00623C15 /* Toucher.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA719724287A480C00623C15 /* Toucher.swift */; }; AA719789287A480D00623C15 /* PlayInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA719755287A480C00623C15 /* PlayInput.swift */; }; @@ -50,10 +65,10 @@ AB7DA47529B85BFB0034ACB2 /* PlayShadow.m in Sources */ = {isa = PBXBuildFile; fileRef = AB7DA47429B85BFB0034ACB2 /* PlayShadow.m */; }; AB7DA47729B8A78B0034ACB2 /* PlayShadow.h in Headers */ = {isa = PBXBuildFile; fileRef = AB7DA47629B8A78B0034ACB2 /* PlayShadow.h */; }; ABCECEE629750BA600746595 /* PlayedApple.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCECEE529750BA600746595 /* PlayedApple.swift */; }; - B127172228817AB90025112B /* SwordRPC in Frameworks */ = {isa = PBXBuildFile; productRef = B127172128817AB90025112B /* SwordRPC */; }; B127172528817C040025112B /* DiscordIPC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B127172428817C040025112B /* DiscordIPC.swift */; }; B1271729288284BE0025112B /* DiscordActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1271728288284BE0025112B /* DiscordActivity.swift */; }; B1E8CF8A28BBE2AB004340D3 /* Keymapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1E8CF8928BBE2AB004340D3 /* Keymapping.swift */; }; + B6D774FF2ACFC3D900C0D9D8 /* SwordRPC in Frameworks */ = {isa = PBXBuildFile; productRef = B6D774FE2ACFC3D900C0D9D8 /* SwordRPC */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -79,7 +94,23 @@ 6E84A14C28D0F96D00BF7495 /* AKInterface.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AKInterface.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; 951D8276299D097C00D35B20 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Playtools.strings; sourceTree = ""; }; 951D8278299D098000D35B20 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Playtools.strings"; sourceTree = ""; }; - 95A553E629F2BBB200E34C26 /* PlayController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayController.swift; sourceTree = ""; }; + 9562D1502AB484C7002C329D /* EventAdapters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventAdapters.swift; sourceTree = ""; }; + 9562D1522AB484FD002C329D /* EventAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventAdapter.swift; sourceTree = ""; }; + 9562D1572AB4FB9B002C329D /* TouchscreenKeyboardEventAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchscreenKeyboardEventAdapter.swift; sourceTree = ""; }; + 9562D1592AB4FBBB002C329D /* TransparentKeyboardEventAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransparentKeyboardEventAdapter.swift; sourceTree = ""; }; + 9562D15B2AB4FC79002C329D /* TransparentMouseEventAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransparentMouseEventAdapter.swift; sourceTree = ""; }; + 9562D15D2AB4FCB8002C329D /* TouchscreenControllerEventAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchscreenControllerEventAdapter.swift; sourceTree = ""; }; + 9562D15F2AB4FD46002C329D /* TransparentControllerEventAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransparentControllerEventAdapter.swift; sourceTree = ""; }; + 9562D1612AB4FE73002C329D /* CameraControlMouseEventAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraControlMouseEventAdapter.swift; sourceTree = ""; }; + 9562D1632AB4FEB4002C329D /* TouchscreenMouseEventAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchscreenMouseEventAdapter.swift; sourceTree = ""; }; + 9562D1652AB5049B002C329D /* KeyboardEventAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardEventAdapter.swift; sourceTree = ""; }; + 9562D16A2AB505AD002C329D /* MouseEventAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MouseEventAdapter.swift; sourceTree = ""; }; + 9562D16C2AB505D4002C329D /* ControllerEventAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControllerEventAdapter.swift; sourceTree = ""; }; + 9562D16E2AB50D34002C329D /* ActionDispatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionDispatcher.swift; sourceTree = ""; }; + 9562D1722AB52416002C329D /* EditorKeyboardEventAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorKeyboardEventAdapter.swift; sourceTree = ""; }; + 9562D1742AB52479002C329D /* EditorMouseEventAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorMouseEventAdapter.swift; sourceTree = ""; }; + 9562D1762AB52550002C329D /* EditorControllerEventAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorControllerEventAdapter.swift; sourceTree = ""; }; + 9562D1782AB64458002C329D /* ModeAutomaton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModeAutomaton.swift; sourceTree = ""; }; AA7196D8287A447700623C15 /* PlayTools.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PlayTools.framework; sourceTree = BUILT_PRODUCTS_DIR; }; AA719702287A44D200623C15 /* PlayLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PlayLoader.m; sourceTree = ""; }; AA719704287A44D200623C15 /* PlaySettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaySettings.swift; sourceTree = ""; }; @@ -87,7 +118,6 @@ AA719706287A44D200623C15 /* PlayCover.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayCover.swift; sourceTree = ""; }; AA719708287A44D200623C15 /* PlayTools.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PlayTools.h; sourceTree = ""; }; AA71970A287A44D200623C15 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - AA719722287A480C00623C15 /* PlayMice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayMice.swift; sourceTree = ""; }; AA719723287A480C00623C15 /* PlayAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayAction.swift; sourceTree = ""; }; AA719724287A480C00623C15 /* Toucher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Toucher.swift; sourceTree = ""; }; AA719755287A480C00623C15 /* PlayInput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayInput.swift; sourceTree = ""; }; @@ -135,7 +165,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - B127172228817AB90025112B /* SwordRPC in Frameworks */, + B6D774FF2ACFC3D900C0D9D8 /* SwordRPC in Frameworks */, 6E84A14528D0F94E00BF7495 /* UIKit.framework in Frameworks */, AA818CB9287ABFB1000BEE9D /* IOKit.framework in Frameworks */, ); @@ -154,6 +184,103 @@ name = AKInterface; sourceTree = ""; }; + 9562D14F2AB4849C002C329D /* EventAdapter */ = { + isa = PBXGroup; + children = ( + 9562D1562AB48523002C329D /* Controller */, + 9562D1552AB4851C002C329D /* Mouse */, + 9562D1542AB48504002C329D /* Keyboard */, + 9562D1502AB484C7002C329D /* EventAdapters.swift */, + 9562D1522AB484FD002C329D /* EventAdapter.swift */, + ); + path = EventAdapter; + sourceTree = ""; + }; + 9562D1542AB48504002C329D /* Keyboard */ = { + isa = PBXGroup; + children = ( + 9562D1672AB5050D002C329D /* Instances */, + 9562D1652AB5049B002C329D /* KeyboardEventAdapter.swift */, + ); + path = Keyboard; + sourceTree = ""; + }; + 9562D1552AB4851C002C329D /* Mouse */ = { + isa = PBXGroup; + children = ( + 9562D1682AB50564002C329D /* Instances */, + 9562D16A2AB505AD002C329D /* MouseEventAdapter.swift */, + ); + path = Mouse; + sourceTree = ""; + }; + 9562D1562AB48523002C329D /* Controller */ = { + isa = PBXGroup; + children = ( + 9562D1692AB50588002C329D /* Instances */, + 9562D16C2AB505D4002C329D /* ControllerEventAdapter.swift */, + ); + path = Controller; + sourceTree = ""; + }; + 9562D1672AB5050D002C329D /* Instances */ = { + isa = PBXGroup; + children = ( + 9562D1572AB4FB9B002C329D /* TouchscreenKeyboardEventAdapter.swift */, + 9562D1592AB4FBBB002C329D /* TransparentKeyboardEventAdapter.swift */, + 9562D1722AB52416002C329D /* EditorKeyboardEventAdapter.swift */, + ); + path = Instances; + sourceTree = ""; + }; + 9562D1682AB50564002C329D /* Instances */ = { + isa = PBXGroup; + children = ( + 9562D15B2AB4FC79002C329D /* TransparentMouseEventAdapter.swift */, + 9562D1612AB4FE73002C329D /* CameraControlMouseEventAdapter.swift */, + 9562D1632AB4FEB4002C329D /* TouchscreenMouseEventAdapter.swift */, + 9562D1742AB52479002C329D /* EditorMouseEventAdapter.swift */, + ); + path = Instances; + sourceTree = ""; + }; + 9562D1692AB50588002C329D /* Instances */ = { + isa = PBXGroup; + children = ( + 9562D15D2AB4FCB8002C329D /* TouchscreenControllerEventAdapter.swift */, + 9562D15F2AB4FD46002C329D /* TransparentControllerEventAdapter.swift */, + 9562D1762AB52550002C329D /* EditorControllerEventAdapter.swift */, + ); + path = Instances; + sourceTree = ""; + }; + 9562D1702AB51B41002C329D /* Frontend */ = { + isa = PBXGroup; + children = ( + 9562D14F2AB4849C002C329D /* EventAdapter */, + 9562D1782AB64458002C329D /* ModeAutomaton.swift */, + AA719756287A480C00623C15 /* ControlMode.swift */, + ); + path = Frontend; + sourceTree = ""; + }; + 9562D1712AB51B57002C329D /* Backend */ = { + isa = PBXGroup; + children = ( + 9562D17A2AB6E5ED002C329D /* Action */, + AA719724287A480C00623C15 /* Toucher.swift */, + ); + path = Backend; + sourceTree = ""; + }; + 9562D17A2AB6E5ED002C329D /* Action */ = { + isa = PBXGroup; + children = ( + AA719723287A480C00623C15 /* PlayAction.swift */, + ); + path = Action; + sourceTree = ""; + }; AA7196CE287A447700623C15 = { isa = PBXGroup; children = ( @@ -198,13 +325,11 @@ isa = PBXGroup; children = ( AA719813287A813A00623C15 /* PTFakeTouch */, - AA719722287A480C00623C15 /* PlayMice.swift */, - AA719723287A480C00623C15 /* PlayAction.swift */, - AA719724287A480C00623C15 /* Toucher.swift */, - AA719755287A480C00623C15 /* PlayInput.swift */, - AA719756287A480C00623C15 /* ControlMode.swift */, + 9562D1712AB51B57002C329D /* Backend */, + 9562D1702AB51B41002C329D /* Frontend */, AA719757287A480C00623C15 /* MenuController.swift */, - 95A553E629F2BBB200E34C26 /* PlayController.swift */, + AA719755287A480C00623C15 /* PlayInput.swift */, + 9562D16E2AB50D34002C329D /* ActionDispatcher.swift */, ); path = Controls; sourceTree = ""; @@ -351,7 +476,7 @@ ); name = PlayTools; packageProductDependencies = ( - B127172128817AB90025112B /* SwordRPC */, + B6D774FE2ACFC3D900C0D9D8 /* SwordRPC */, ); productName = PlayTools; productReference = AA7196D8287A447700623C15 /* PlayTools.framework */; @@ -387,7 +512,7 @@ ); mainGroup = AA7196CE287A447700623C15; packageReferences = ( - B127172028817AB90025112B /* XCRemoteSwiftPackageReference "swordRPC" */, + B6D774FD2ACFC3D900C0D9D8 /* XCRemoteSwiftPackageReference "SwordRPC" */, ); productRefGroup = AA7196D9287A447700623C15 /* Products */; projectDirPath = ""; @@ -452,29 +577,43 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 95A553E729F2BBB200E34C26 /* PlayController.swift in Sources */, + 9562D16D2AB505D4002C329D /* ControllerEventAdapter.swift in Sources */, AA71975A287A480D00623C15 /* Toucher.swift in Sources */, AA7197A1287A481500623C15 /* CircleMenuLoader.swift in Sources */, 6E76639B28D0FAE700DE4AF9 /* Plugin.swift in Sources */, B1271729288284BE0025112B /* DiscordActivity.swift in Sources */, AA7197AB287A481500623C15 /* ControlModel.swift in Sources */, AA7197AE287A481500623C15 /* DragElementsView.swift in Sources */, + 9562D1622AB4FE73002C329D /* CameraControlMouseEventAdapter.swift in Sources */, + 9562D1642AB4FEB4002C329D /* TouchscreenMouseEventAdapter.swift in Sources */, + 9562D1602AB4FD46002C329D /* TransparentControllerEventAdapter.swift in Sources */, + 9562D1792AB64458002C329D /* ModeAutomaton.swift in Sources */, AA71970D287A44D200623C15 /* PlaySettings.swift in Sources */, AA719759287A480D00623C15 /* PlayAction.swift in Sources */, 6E7663A528D0FEBE00DE4AF9 /* AKPluginLoader.swift in Sources */, + 9562D16F2AB50D34002C329D /* ActionDispatcher.swift in Sources */, 2847AE48298EFC0F00B0F983 /* PlayScreen.swift in Sources */, + 9562D1532AB484FD002C329D /* EventAdapter.swift in Sources */, AA7197A2287A481500623C15 /* CircleMenu.swift in Sources */, + 9562D15A2AB4FBBB002C329D /* TransparentKeyboardEventAdapter.swift in Sources */, AA71978B287A480D00623C15 /* MenuController.swift in Sources */, + 9562D1732AB52416002C329D /* EditorKeyboardEventAdapter.swift in Sources */, + 9562D1752AB52479002C329D /* EditorMouseEventAdapter.swift in Sources */, AA7197AA287A481500623C15 /* Toast.swift in Sources */, AA719789287A480D00623C15 /* PlayInput.swift in Sources */, AA71970B287A44D200623C15 /* PlayLoader.m in Sources */, + 9562D1512AB484C7002C329D /* EventAdapters.swift in Sources */, AA7197AF287A481500623C15 /* KeyCodeNames.swift in Sources */, AA71978A287A480D00623C15 /* ControlMode.swift in Sources */, - AA719758287A480D00623C15 /* PlayMice.swift in Sources */, AA7197AD287A481500623C15 /* EditorController.swift in Sources */, + 9562D16B2AB505AD002C329D /* MouseEventAdapter.swift in Sources */, + 9562D15C2AB4FC79002C329D /* TransparentMouseEventAdapter.swift in Sources */, + 9562D1772AB52550002C329D /* EditorControllerEventAdapter.swift in Sources */, AA7197A3287A481500623C15 /* CircleMenuButton.swift in Sources */, + 9562D1582AB4FB9B002C329D /* TouchscreenKeyboardEventAdapter.swift in Sources */, AA7197A9287A481500623C15 /* PlayInfo.swift in Sources */, AA71986A287A81A000623C15 /* PTFakeMetaTouch.m in Sources */, + 9562D15E2AB4FCB8002C329D /* TouchscreenControllerEventAdapter.swift in Sources */, ABCECEE629750BA600746595 /* PlayedApple.swift in Sources */, AA71986C287A81A000623C15 /* NSObject+Swizzle.m in Sources */, AA71970F287A44D200623C15 /* PlayCover.swift in Sources */, @@ -483,6 +622,7 @@ AA71985F287A81A000623C15 /* IOHIDEvent+KIF.m in Sources */, B1E8CF8A28BBE2AB004340D3 /* Keymapping.swift in Sources */, AB7DA47529B85BFB0034ACB2 /* PlayShadow.m in Sources */, + 9562D1662AB5049B002C329D /* KeyboardEventAdapter.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -811,9 +951,9 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - B127172028817AB90025112B /* XCRemoteSwiftPackageReference "swordRPC" */ = { + B6D774FD2ACFC3D900C0D9D8 /* XCRemoteSwiftPackageReference "SwordRPC" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/khoralee/swordRPC"; + repositoryURL = "https://github.com/PlayCover/SwordRPC"; requirement = { branch = main; kind = branch; @@ -822,9 +962,9 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - B127172128817AB90025112B /* SwordRPC */ = { + B6D774FE2ACFC3D900C0D9D8 /* SwordRPC */ = { isa = XCSwiftPackageProductDependency; - package = B127172028817AB90025112B /* XCRemoteSwiftPackageReference "swordRPC" */; + package = B6D774FD2ACFC3D900C0D9D8 /* XCRemoteSwiftPackageReference "SwordRPC" */; productName = SwordRPC; }; /* End XCSwiftPackageProductDependency section */ diff --git a/PlayTools/Controls/ActionDispatcher.swift b/PlayTools/Controls/ActionDispatcher.swift new file mode 100644 index 00000000..8071172e --- /dev/null +++ b/PlayTools/Controls/ActionDispatcher.swift @@ -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]] = Array( + (0..(.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.. Void + init(_ key: String, _ handle: @escaping (CGFloat, CGFloat) -> Void) { + self.key = key + self.handle = handle + } +} diff --git a/PlayTools/Controls/PlayAction.swift b/PlayTools/Controls/Backend/Action/PlayAction.swift similarity index 51% rename from PlayTools/Controls/PlayAction.swift rename to PlayTools/Controls/Backend/Action/PlayAction.swift index cc49f5d7..4f024362 100644 --- a/PlayTools/Controls/PlayAction.swift +++ b/PlayTools/Controls/Backend/Action/PlayAction.swift @@ -8,6 +8,7 @@ import Foundation protocol Action { func invalidate() } +// Actions hold touch point IDs, perform fake touch class ButtonAction: Action { func invalidate() { @@ -26,7 +27,7 @@ class ButtonAction: Action { let code = keyCode let codeName = KeyCodeNames.keyCodes[code] ?? "Btn" // TODO: set both key names in draggable button, so as to depracate key code - PlayInput.registerButton(key: code == KeyCodeNames.defaultCode ? keyName: codeName, handler: self.update) + ActionDispatcher.register(key: code == KeyCodeNames.defaultCode ? keyName: codeName, handler: self.update) } convenience init(data: Button) { @@ -60,17 +61,23 @@ class DraggableButtonAction: ButtonAction { if pressed { Toucher.touchcam(point: point, phase: UITouch.Phase.began, tid: &id) self.releasePoint = point - PlayInput.draggableHandler[keyName] = self.onMouseMoved - AKInterface.shared!.hideCursor() + ActionDispatcher.register(key: KeyCodeNames.mouseMove, + handler: self.onMouseMoved, + priority: .DRAGGABLE) + if !mode.cursorHidden() { + AKInterface.shared!.hideCursor() + } } else { - PlayInput.draggableHandler.removeValue(forKey: keyName) + ActionDispatcher.unregister(key: KeyCodeNames.mouseMove) Toucher.touchcam(point: releasePoint, phase: UITouch.Phase.ended, tid: &id) - AKInterface.shared!.unhideCursor() + if !mode.cursorHidden() { + AKInterface.shared!.unhideCursor() + } } } override func invalidate() { - PlayInput.draggableHandler.removeValue(forKey: keyName) + ActionDispatcher.unregister(key: KeyCodeNames.mouseMove) super.invalidate() } @@ -96,10 +103,10 @@ class ContinuousJoystickAction: Action { self.key = data.keyName position = center self.sensitivity = data.transform.size.absoluteSize / 4 - if key == PlayMice.elementName { - PlayInput.joystickHandler[key] = self.mouseUpdate + if key == KeyCodeNames.mouseMove { + ActionDispatcher.register(key: key, handler: self.mouseUpdate) } else { - PlayInput.joystickHandler[key] = self.thumbstickUpdate + ActionDispatcher.register(key: key, handler: self.thumbstickUpdate) } } @@ -131,7 +138,7 @@ class ContinuousJoystickAction: Action { } func invalidate() { - PlayInput.joystickHandler.removeValue(forKey: key) + Toucher.touchcam(point: CGPoint(x: 10, y: 10), phase: UITouch.Phase.ended, tid: &id) } } @@ -149,7 +156,7 @@ class JoystickAction: Action { self.shift = shift / 4 for index in 0.. Void) { + let when = DispatchTime.now() + delay + PlayInput.touchQueue.asyncAfter(deadline: when, execute: closure) + } + // Count swipe duration + var counter = 0 + // if should wait before beginning next touch + var cooldown = false + var lastCounter = 0 + + func checkEnded() { + if self.counter == self.lastCounter { + if self.counter < 4 { + counter += 1 + } else { + self.doLiftOff() + } + } + self.lastCounter = self.counter + } + + public func move(from: () -> CGPoint?, deltaX: CGFloat, deltaY: CGFloat) { + if id == nil { + if cooldown { + return + } + guard let start = from() else {return} + location = start + counter = 0 + Toucher.touchcam(point: location, phase: UITouch.Phase.began, tid: &id) + timer.resume() + } + // count touch duration + counter += 1 + self.location.x += deltaX + self.location.y -= deltaY + Toucher.touchcam(point: self.location, phase: UITouch.Phase.moved, tid: &id) + } + + public func doLiftOff() { + if id == nil { + return + } + Toucher.touchcam(point: self.location, phase: UITouch.Phase.ended, tid: &id) + timer.suspend() + delay(0.02) { + self.cooldown = false + } + cooldown = true + } + + func invalidate() { + PlayInput.touchQueue.async(execute: self.doLiftOff) + } +} + +class FakeMouseAction: Action { + var id: Int? + var pos: CGPoint! + public init() { + ActionDispatcher.register(key: KeyCodeNames.fakeMouse, handler: buttonPressHandler) + ActionDispatcher.register(key: KeyCodeNames.fakeMouse, handler: buttonLiftHandler) + } + + func buttonPressHandler(xValue: CGFloat, yValue: CGFloat) { + pos = CGPoint(x: xValue, y: yValue) +// DispatchQueue.main.async { +// Toast.showHint(title: "Fake mouse pressed", text: ["\(self.pos)"]) +// } + Toucher.touchcam(point: pos, phase: UITouch.Phase.began, tid: &id) + ActionDispatcher.register(key: KeyCodeNames.fakeMouse, + handler: movementHandler, + priority: .DRAGGABLE) + } + + func buttonLiftHandler(pressed: Bool) { + if pressed { + Toast.showHint(title: "Error", text: ["Fake mouse lift handler received a press event"]) + return + } +// DispatchQueue.main.async { +// Toast.showHint(title: " lift Fake mouse", text: ["\(self.pos)"]) +// } + ActionDispatcher.unregister(key: KeyCodeNames.fakeMouse) + Toucher.touchcam(point: pos, phase: UITouch.Phase.ended, tid: &id) + } + + func movementHandler(xValue: CGFloat, yValue: CGFloat) { + pos.x = xValue + pos.y = yValue + Toucher.touchcam(point: pos, phase: UITouch.Phase.moved, tid: &id) + } + + func invalidate() { + Toucher.touchcam(point: pos ?? CGPoint(x: 10, y: 10), + phase: UITouch.Phase.ended, tid: &self.id) + } + +} diff --git a/PlayTools/Controls/Toucher.swift b/PlayTools/Controls/Backend/Toucher.swift similarity index 100% rename from PlayTools/Controls/Toucher.swift rename to PlayTools/Controls/Backend/Toucher.swift diff --git a/PlayTools/Controls/ControlMode.swift b/PlayTools/Controls/ControlMode.swift deleted file mode 100644 index f690406c..00000000 --- a/PlayTools/Controls/ControlMode.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// ControlMode.swift -// PlayTools -// - -import Foundation - -let mode = ControlMode.mode - -public class ControlMode { - - static public let mode = ControlMode() - public var visible: Bool = true - public var keyboardMapped = true - - public static func trySwap() -> Bool { - if PlayInput.shouldLockCursor { - mode.show(!mode.visible) - return true - } - mode.show(true) - return false - } - - func setMapping(_ mapped: Bool) { - if mapped { - PlayInput.shared.parseKeymap() - } else { - show(true) - PlayInput.shared.invalidate() - } - keyboardMapped = mapped - } - - func show(_ show: Bool) { - if keyboardMapped { - if show { - if !visible { - NotificationCenter.default.post(name: NSNotification.Name.playtoolsCursorWillShow, - object: nil, userInfo: [:]) - if screen.fullscreen { - screen.switchDock(true) - } - AKInterface.shared!.unhideCursor() - } - } else { - if visible { - NotificationCenter.default.post(name: NSNotification.Name.playtoolsCursorWillHide, - object: nil, userInfo: [:]) - AKInterface.shared!.hideCursor() - if screen.fullscreen { - screen.switchDock(false) - } - } - } - Toucher.writeLog(logMessage: "cursor show switched to \(show)") - visible = show - } - } -} - -extension NSNotification.Name { - public static let playtoolsCursorWillHide: NSNotification.Name - = NSNotification.Name("playtools.cursorWillHide") - - public static let playtoolsCursorWillShow: NSNotification.Name - = NSNotification.Name("playtools.cursorWillShow") -} diff --git a/PlayTools/Controls/Frontend/ControlMode.swift b/PlayTools/Controls/Frontend/ControlMode.swift new file mode 100644 index 00000000..cf6eb9a2 --- /dev/null +++ b/PlayTools/Controls/Frontend/ControlMode.swift @@ -0,0 +1,149 @@ +// +// ControlMode.swift +// PlayTools +// + +import Foundation +import GameController + +let mode = ControlMode.mode + +public enum ControlModeLiteral: String { + case TEXT_INPUT = "textInput" + case CAMERA_ROTATE = "cameraRotate" + case ARBITRARY_CLICK = "arbitraryClick" + case OFF = "off" + case EDITOR = "editor" +} +// This class handles different control logic under different control mode + +public class ControlMode: Equatable { + static public let mode = ControlMode() + + private var controlMode = ControlModeLiteral.OFF + + private var keyboardAdapter: KeyboardEventAdapter! + private var mouseAdapter: MouseEventAdapter! + private var controllerAdapter: ControllerEventAdapter! + + public func cursorHidden() -> Bool { + return mouseAdapter?.cursorHidden() ?? false + } + + public func initialize() { + let centre = NotificationCenter.default + let main = OperationQueue.main + if PlaySettings.shared.noKMOnInput { + centre.addObserver(forName: UITextField.textDidEndEditingNotification, object: nil, queue: main) { _ in + ModeAutomaton.onUITextInputEndEdit() + Toucher.writeLog(logMessage: "uitextinput end edit") + } + centre.addObserver(forName: UITextField.textDidBeginEditingNotification, object: nil, queue: main) { _ in + ModeAutomaton.onUITextInputBeginEdit() + Toucher.writeLog(logMessage: "uitextinput begin edit") + } + centre.addObserver(forName: UITextView.textDidEndEditingNotification, object: nil, queue: main) { _ in + ModeAutomaton.onUITextInputEndEdit() + Toucher.writeLog(logMessage: "uitextinput end edit") + } + centre.addObserver(forName: UITextView.textDidBeginEditingNotification, object: nil, queue: main) { _ in + ModeAutomaton.onUITextInputBeginEdit() + Toucher.writeLog(logMessage: "uitextinput begin edit") + } + set(.ARBITRARY_CLICK) + } else { + set(.OFF) + } + + centre.addObserver(forName: NSNotification.Name.GCControllerDidConnect, object: nil, queue: main) { _ in + GCController.current?.extendedGamepad?.valueChangedHandler = {profile, element in + self.controllerAdapter.handleValueChanged(profile, element) + } + } + + AKInterface.shared!.setupKeyboard(keyboard: { keycode, pressed, isRepeat in + self.keyboardAdapter.handleKey(keycode: keycode, pressed: pressed, isRepeat: isRepeat)}, + swapMode: ModeAutomaton.onOption) + + if PlaySettings.shared.enableScrollWheel { + AKInterface.shared!.setupScrollWheel({deltaX, deltaY in + self.mouseAdapter.handleScrollWheel(deltaX: deltaX, deltaY: deltaY) + }) + } + + AKInterface.shared!.setupMouseMoved({deltaX, deltaY in + self.mouseAdapter.handleMove(deltaX: deltaX, deltaY: deltaY) + }) + + AKInterface.shared!.setupMouseButton(left: true, right: false, {_, pressed in + self.mouseAdapter.handleLeftButton(pressed: pressed) + }) + + AKInterface.shared!.setupMouseButton(left: false, right: false, {id, pressed in + self.mouseAdapter.handleOtherButton(id: id, pressed: pressed) + }) + + AKInterface.shared!.setupMouseButton(left: false, right: true, {id, pressed in + self.mouseAdapter.handleOtherButton(id: id, pressed: pressed) + }) + + ActionDispatcher.build() + } + + public func set(_ mode: ControlModeLiteral) { + let wasHidden = mouseAdapter?.cursorHidden() ?? false + let first = mouseAdapter == nil + keyboardAdapter = EventAdapters.keyboard(controlMode: mode) + mouseAdapter = EventAdapters.mouse(controlMode: mode) + controllerAdapter = EventAdapters.controller(controlMode: mode) + controlMode = mode + if !first { +// Toast.showHint(title: "should hide cursor? \(mouseAdapter.cursorHidden())", +// text: ["current state: " + mode]) + } + if mouseAdapter.cursorHidden() != wasHidden && settings.keymapping { + if wasHidden { + NotificationCenter.default.post(name: NSNotification.Name.playtoolsCursorWillShow, + object: nil, userInfo: [:]) + if screen.fullscreen { + screen.switchDock(true) + } + + if mode == .OFF || mode == .EDITOR { + ActionDispatcher.invalidateActions() + } + + AKInterface.shared!.unhideCursor() + } else { + NotificationCenter.default.post(name: NSNotification.Name.playtoolsCursorWillHide, + object: nil, userInfo: [:]) + AKInterface.shared!.hideCursor() + if screen.fullscreen { + screen.switchDock(false) + } + } + Toucher.writeLog(logMessage: "cursor show switched to \(!wasHidden)") + } + } + + public static func == (lhs: ControlModeLiteral, rhs: ControlMode) -> Bool { + lhs == rhs.controlMode + } + + public static func == (lhs: ControlMode, rhs: ControlModeLiteral) -> Bool { + rhs == lhs + } + + public static func == (lhs: ControlMode, rhs: ControlMode) -> Bool { + rhs.controlMode == lhs.controlMode + } + +} + +extension NSNotification.Name { + public static let playtoolsCursorWillHide: NSNotification.Name + = NSNotification.Name("playtools.cursorWillHide") + + public static let playtoolsCursorWillShow: NSNotification.Name + = NSNotification.Name("playtools.cursorWillShow") +} diff --git a/PlayTools/Controls/Frontend/EventAdapter/Controller/ControllerEventAdapter.swift b/PlayTools/Controls/Frontend/EventAdapter/Controller/ControllerEventAdapter.swift new file mode 100644 index 00000000..039d457d --- /dev/null +++ b/PlayTools/Controls/Frontend/EventAdapter/Controller/ControllerEventAdapter.swift @@ -0,0 +1,15 @@ +// +// ControllerEventAdapter.swift +// PlayTools +// +// Created by 许沂聪 on 2023/9/16. +// + +import Foundation +import GameController + +// All controller events under any mode + +public protocol ControllerEventAdapter: EventAdapter { + func handleValueChanged(_ profile: GCExtendedGamepad, _ element: GCControllerElement) +} diff --git a/PlayTools/Controls/Frontend/EventAdapter/Controller/Instances/EditorControllerEventAdapter.swift b/PlayTools/Controls/Frontend/EventAdapter/Controller/Instances/EditorControllerEventAdapter.swift new file mode 100644 index 00000000..655c2367 --- /dev/null +++ b/PlayTools/Controls/Frontend/EventAdapter/Controller/Instances/EditorControllerEventAdapter.swift @@ -0,0 +1,36 @@ +// +// EditorControllerEventAdapter.swift +// PlayTools +// +// Created by 许沂聪 on 2023/9/16. +// + +import Foundation +import GameController + +// Controller events handler when in editor mode + +public class EditorControllerEventAdapter: ControllerEventAdapter { + public func handleValueChanged(_ profile: GCExtendedGamepad, _ element: GCControllerElement) { + // 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! + } + } + EditorController.shared.setKey(alias) + } + +} diff --git a/PlayTools/Controls/Frontend/EventAdapter/Controller/Instances/TouchscreenControllerEventAdapter.swift b/PlayTools/Controls/Frontend/EventAdapter/Controller/Instances/TouchscreenControllerEventAdapter.swift new file mode 100644 index 00000000..f7943aa0 --- /dev/null +++ b/PlayTools/Controls/Frontend/EventAdapter/Controller/Instances/TouchscreenControllerEventAdapter.swift @@ -0,0 +1,99 @@ +// +// TouchscreenControllerEventAdapter.swift +// PlayTools +// +// Created by 许沂聪 on 2023/9/16. +// + +import Foundation +import GameController + +// Controller events handler when keymap is on + +public class TouchscreenControllerEventAdapter: ControllerEventAdapter { + + private var directionPadXValue: Float = 0, + directionPadYValue: Float = 0 + static private var thumbstickCursorControl: [String: (CGFloat, CGFloat) -> Void] = [:] + + public func handleValueChanged(_ profile: GCExtendedGamepad, _ element: GCControllerElement) { + let name: String = element.aliases.first! + if let buttonElement = element as? GCControllerButtonInput { + _ = ActionDispatcher.dispatch(key: name, pressed: buttonElement.isPressed) + } else if let dpadElement = element as? GCControllerDirectionPad { + handleDirectionPad(profile, dpadElement) + } else { + Toast.showOver(msg: "unrecognised controller element input happens") + } + } + + private func handleDirectionPad(_ profile: GCExtendedGamepad, _ dpad: GCControllerDirectionPad) { + let name = dpad.aliases.first! + let xAxis = dpad.xAxis, yAxis = dpad.yAxis + if name == "Direction Pad" { + if (xAxis.value > 0) != (directionPadXValue > 0) { + handleValueChanged(profile, dpad.right) + } + if (xAxis.value < 0) != (directionPadXValue < 0) { + handleValueChanged(profile, dpad.left) + } + if (yAxis.value > 0) != (directionPadYValue > 0) { + handleValueChanged(profile, dpad.up) + } + if (yAxis.value < 0) != (directionPadYValue < 0) { + handleValueChanged(profile, dpad.down) + } + directionPadXValue = xAxis.value + directionPadYValue = yAxis.value + return + } + let deltaX = xAxis.value, deltaY = yAxis.value + let cgDx = CGFloat(deltaX) + let cgDy = CGFloat(deltaY) + let dispatchType = ActionDispatcher.getDispatchPriority(key: name) + if dispatchType == nil { + return + } else if dispatchType == .DEFAULT { + _ = ActionDispatcher.dispatch(key: name, valueX: cgDx, valueY: cgDy) + } else { + if TouchscreenControllerEventAdapter.thumbstickCursorControl[name] == nil { + TouchscreenControllerEventAdapter.thumbstickCursorControl[name] = ThumbstickCursorControl(name).update + } + TouchscreenControllerEventAdapter.thumbstickCursorControl[name]!(cgDx * 6, cgDy * 6) + } + } + +} + +class ThumbstickCursorControl { + private var thumbstickVelocity: CGVector = CGVector.zero, + thumbstickPolling: Bool = false, + key: String + + init(_ key: String) { + self.key = key + } + + static private func isVectorSignificant(_ vector: CGVector) -> Bool { + return vector.dx.magnitude + vector.dy.magnitude > 0.2 + } + + public func update(velocityX: CGFloat, velocityY: CGFloat) { + self.thumbstickVelocity.dx = velocityX + self.thumbstickVelocity.dy = velocityY + if !thumbstickPolling { + PlayInput.touchQueue.async(execute: self.thumbstickPoll) + self.thumbstickPolling = true + } + } + + private func thumbstickPoll() { + if !ThumbstickCursorControl.isVectorSignificant(self.thumbstickVelocity) { + self.thumbstickPolling = false + return + } + _ = ActionDispatcher.dispatch(key: key, valueX: thumbstickVelocity.dx, valueY: thumbstickVelocity.dy) + PlayInput.touchQueue.asyncAfter( + deadline: DispatchTime.now() + 0.017, execute: self.thumbstickPoll) + } +} diff --git a/PlayTools/Controls/Frontend/EventAdapter/Controller/Instances/TransparentControllerEventAdapter.swift b/PlayTools/Controls/Frontend/EventAdapter/Controller/Instances/TransparentControllerEventAdapter.swift new file mode 100644 index 00000000..519ad53f --- /dev/null +++ b/PlayTools/Controls/Frontend/EventAdapter/Controller/Instances/TransparentControllerEventAdapter.swift @@ -0,0 +1,27 @@ +// +// TransparentControllerEventAdapter.swift +// PlayTools +// +// Created by 许沂聪 on 2023/9/16. +// + +import Foundation +import GameController + +// Controller events handler when keymap is off + +public class TransparentControllerEventAdapter: ControllerEventAdapter { + public func handleValueChanged(_ profile: GCExtendedGamepad, _ element: GCControllerElement) { + /* + Controller event is currently handled by GameController APIs. + This API runs concurrently with the NSEvent things. + In other words, whatever I do in its handler, the NSEvent is not affected. + The drawbacks of this API is, it executes through Main Dispatch Queue + and may be delayed under high CPU pressure + + However, we didn't find a suitable alternative for it. + But high CPU 3D games usually don't need to map controllers + so it's OK for now + */ + } +} diff --git a/PlayTools/Controls/Frontend/EventAdapter/EventAdapter.swift b/PlayTools/Controls/Frontend/EventAdapter/EventAdapter.swift new file mode 100644 index 00000000..4dea098b --- /dev/null +++ b/PlayTools/Controls/Frontend/EventAdapter/EventAdapter.swift @@ -0,0 +1,12 @@ +// +// EventAdapter.swift +// PlayTools +// +// Created by 许沂聪 on 2023/9/15. +// + +import Foundation + +public protocol EventAdapter { + // Just a stub, may add things later +} diff --git a/PlayTools/Controls/Frontend/EventAdapter/EventAdapters.swift b/PlayTools/Controls/Frontend/EventAdapter/EventAdapters.swift new file mode 100644 index 00000000..95371743 --- /dev/null +++ b/PlayTools/Controls/Frontend/EventAdapter/EventAdapters.swift @@ -0,0 +1,53 @@ +// +// EventAdapters.swift +// PlayTools +// +// Created by 许沂聪 on 2023/9/15. +// + +import Foundation + +// This is a builder class for event adapters + +public class EventAdapters { + + static func keyboard(controlMode: ControlModeLiteral) -> KeyboardEventAdapter { + switch controlMode { + case .OFF: fallthrough + case .TEXT_INPUT: + return TransparentKeyboardEventAdapter() + case .CAMERA_ROTATE: fallthrough + case .ARBITRARY_CLICK: + return TouchscreenKeyboardEventAdapter() + case .EDITOR: + return EditorKeyboardEventAdapter() + } + } + + static func mouse(controlMode: ControlModeLiteral) -> MouseEventAdapter { + switch controlMode { + case .OFF: fallthrough + case .TEXT_INPUT: + return TransparentMouseEventAdapter() + case .CAMERA_ROTATE: + return CameraControlMouseEventAdapter() + case .ARBITRARY_CLICK: + return TouchscreenMouseEventAdapter() + case .EDITOR: + return EditorMouseEventAdapter() + } + } + + static func controller(controlMode: ControlModeLiteral) -> ControllerEventAdapter { + switch controlMode { + case .OFF: + return TransparentControllerEventAdapter() + case .TEXT_INPUT: fallthrough + case .CAMERA_ROTATE: fallthrough + case .ARBITRARY_CLICK: + return TouchscreenControllerEventAdapter() + case .EDITOR: + return EditorControllerEventAdapter() + } + } +} diff --git a/PlayTools/Controls/Frontend/EventAdapter/Keyboard/Instances/EditorKeyboardEventAdapter.swift b/PlayTools/Controls/Frontend/EventAdapter/Keyboard/Instances/EditorKeyboardEventAdapter.swift new file mode 100644 index 00000000..796c553b --- /dev/null +++ b/PlayTools/Controls/Frontend/EventAdapter/Keyboard/Instances/EditorKeyboardEventAdapter.swift @@ -0,0 +1,38 @@ +// +// EditorKeyboardEventAdapter.swift +// PlayTools +// +// Created by 许沂聪 on 2023/9/16. +// + +import Foundation +import GameController + +// Keyboard events handler when in editor mode + +public class EditorKeyboardEventAdapter: KeyboardEventAdapter { + private static let FORBIDDEN: [GCKeyCode] = [ + .leftGUI, + .rightGUI, + .leftAlt, + .rightAlt, + .printScreen + ] + + public func handleKey(keycode: UInt16, pressed: Bool, isRepeat: Bool) -> Bool { + if AKInterface.shared!.cmdPressed || !pressed || isRepeat { + return false + } + guard let rawValue = KeyCodeNames.mapNSEventVirtualCodeToGCKeyCodeRawValue[keycode] else { + return false + } + let gcKeyCode = GCKeyCode(rawValue: rawValue) + if EditorKeyboardEventAdapter.FORBIDDEN.contains(gcKeyCode) { +// Toast.showHint(title: "Invalid Key", text: ["This key is intentionally forbidden. Keyname: \(name)"]) + return false + } + EditorController.shared.setKey(rawValue) + return true + } + +} diff --git a/PlayTools/Controls/Frontend/EventAdapter/Keyboard/Instances/TouchscreenKeyboardEventAdapter.swift b/PlayTools/Controls/Frontend/EventAdapter/Keyboard/Instances/TouchscreenKeyboardEventAdapter.swift new file mode 100644 index 00000000..18d3251d --- /dev/null +++ b/PlayTools/Controls/Frontend/EventAdapter/Keyboard/Instances/TouchscreenKeyboardEventAdapter.swift @@ -0,0 +1,24 @@ +// +// TouchscreenKeyboardEventAdapter.swift +// PlayTools +// +// Created by 许沂聪 on 2023/9/16. +// + +import Foundation + +// Keyboard events handler when keyboard mapping is on + +public class TouchscreenKeyboardEventAdapter: KeyboardEventAdapter { + public func handleKey(keycode: UInt16, pressed: Bool, isRepeat: Bool) -> Bool { + + if isRepeat { + // eat, eat, eat! + return true + } + + let name = KeyCodeNames.virtualCodes[keycode] ?? "Btn" + return ActionDispatcher.dispatch(key: name, pressed: pressed) + } + +} diff --git a/PlayTools/Controls/Frontend/EventAdapter/Keyboard/Instances/TransparentKeyboardEventAdapter.swift b/PlayTools/Controls/Frontend/EventAdapter/Keyboard/Instances/TransparentKeyboardEventAdapter.swift new file mode 100644 index 00000000..03c181b2 --- /dev/null +++ b/PlayTools/Controls/Frontend/EventAdapter/Keyboard/Instances/TransparentKeyboardEventAdapter.swift @@ -0,0 +1,18 @@ +// +// TextInputKeyboardEventAdapter.swift +// PlayTools +// +// Created by 许沂聪 on 2023/9/16. +// + +import Foundation + +// Keyboard events handler when keyboard mapping is off + +public class TransparentKeyboardEventAdapter: KeyboardEventAdapter { + public func handleKey(keycode: UInt16, pressed: Bool, isRepeat: Bool) -> Bool { + // explicitly eat repeated Enter key + isRepeat && keycode == 36 + } + +} diff --git a/PlayTools/Controls/Frontend/EventAdapter/Keyboard/KeyboardEventAdapter.swift b/PlayTools/Controls/Frontend/EventAdapter/Keyboard/KeyboardEventAdapter.swift new file mode 100644 index 00000000..ca742a8c --- /dev/null +++ b/PlayTools/Controls/Frontend/EventAdapter/Keyboard/KeyboardEventAdapter.swift @@ -0,0 +1,14 @@ +// +// KeyboardEventAdapter.swift +// PlayTools +// +// Created by 许沂聪 on 2023/9/16. +// + +import Foundation + +// All keyboard events under any mode + +public protocol KeyboardEventAdapter: EventAdapter { + func handleKey(keycode: UInt16, pressed: Bool, isRepeat: Bool) -> Bool +} diff --git a/PlayTools/Controls/Frontend/EventAdapter/Mouse/Instances/CameraControlMouseEventAdapter.swift b/PlayTools/Controls/Frontend/EventAdapter/Mouse/Instances/CameraControlMouseEventAdapter.swift new file mode 100644 index 00000000..38de66f6 --- /dev/null +++ b/PlayTools/Controls/Frontend/EventAdapter/Mouse/Instances/CameraControlMouseEventAdapter.swift @@ -0,0 +1,40 @@ +// +// CameraControlMouseEventAdapter.swift +// PlayTools +// +// Created by 许沂聪 on 2023/9/16. +// + +import Foundation + +// Mouse events handler when cursor is locked and keyboard mapping is on + +public class CameraControlMouseEventAdapter: MouseEventAdapter { + public func handleScrollWheel(deltaX: CGFloat, deltaY: CGFloat) -> Bool { + _ = ActionDispatcher.dispatch(key: KeyCodeNames.scrollWheelScale, valueX: deltaX, valueY: deltaY) + // I dont know why but this is the logic before the refactor. + // Might be a mistake but keeping it for now + return true + } + + public func handleMove(deltaX: CGFloat, deltaY: CGFloat) -> Bool { + let sensy = CGFloat(PlaySettings.shared.sensitivity * 0.6) + let cgDx = deltaX * sensy, + cgDy = -deltaY * sensy + return ActionDispatcher.dispatch(key: KeyCodeNames.mouseMove, valueX: cgDx, valueY: cgDy) + } + + public func handleLeftButton(pressed: Bool) -> Bool { + ActionDispatcher.dispatch(key: KeyCodeNames.leftMouseButton, pressed: pressed) + } + + public func handleOtherButton(id: Int, pressed: Bool) -> Bool { + ActionDispatcher.dispatch(key: EditorMouseEventAdapter.getMouseButtonName(id), + pressed: pressed) + } + + public func cursorHidden() -> Bool { + true + } + +} diff --git a/PlayTools/Controls/Frontend/EventAdapter/Mouse/Instances/EditorMouseEventAdapter.swift b/PlayTools/Controls/Frontend/EventAdapter/Mouse/Instances/EditorMouseEventAdapter.swift new file mode 100644 index 00000000..77295cf9 --- /dev/null +++ b/PlayTools/Controls/Frontend/EventAdapter/Mouse/Instances/EditorMouseEventAdapter.swift @@ -0,0 +1,56 @@ +// +// EditorMouseEventAdapter.swift +// PlayTools +// +// Created by 许沂聪 on 2023/9/16. +// + +import Foundation + +// Mouse events handler when in editor mode + +public class EditorMouseEventAdapter: MouseEventAdapter { + private static let buttonName: [String] = [ +// "InvalidMouseButton", + KeyCodeNames.leftMouseButton, + KeyCodeNames.rightMouseButton, + KeyCodeNames.middleMouseButton + ] + + public static func getMouseButtonName(_ id: Int) -> String { + return id < EditorMouseEventAdapter.buttonName.count ? + EditorMouseEventAdapter.buttonName[id] : + "MBtn\(id)" + } + + public func handleOtherButton(id: Int, pressed: Bool) -> Bool { + if pressed { + let name = EditorMouseEventAdapter.getMouseButtonName(id) + // asynced to return quickly. Editor contains UI operation so main queue. + // main queue is fine. should not be slower than keyboard + DispatchQueue.main.async(qos: .userInteractive, execute: { + EditorController.shared.setKey(name) + Toucher.writeLog(logMessage: "mouse button editor set") + }) + } + return true + } + + public func handleScrollWheel(deltaX: CGFloat, deltaY: CGFloat) -> Bool { + false + } + + public func handleMove(deltaX: CGFloat, deltaY: CGFloat) -> Bool { + false + } + + public func handleLeftButton(pressed: Bool) -> Bool { + // Event flows to EditorController via UIKit + false + } + + public func cursorHidden() -> Bool { + false + } + +} diff --git a/PlayTools/Controls/Frontend/EventAdapter/Mouse/Instances/TouchscreenMouseEventAdapter.swift b/PlayTools/Controls/Frontend/EventAdapter/Mouse/Instances/TouchscreenMouseEventAdapter.swift new file mode 100644 index 00000000..325c5007 --- /dev/null +++ b/PlayTools/Controls/Frontend/EventAdapter/Mouse/Instances/TouchscreenMouseEventAdapter.swift @@ -0,0 +1,84 @@ +// +// TouchscreenMouseEventAdapter.swift +// PlayTools +// +// Created by 许沂聪 on 2023/9/16. +// + +import Foundation + +// Mouse events handler when cursor is free and keyboard mapping is on + +public class TouchscreenMouseEventAdapter: MouseEventAdapter { + + static public func cursorPos() -> CGPoint? { + // IMPROVE: this is expensive (maybe?) + var point = AKInterface.shared!.mousePoint + let rect = AKInterface.shared!.windowFrame + if rect.width < 1 || rect.height < 1 { + return nil + } + let viewRect: CGRect = screen.screenRect + let widthRate = viewRect.width / rect.width + var rate = viewRect.height / rect.height + if widthRate > rate { + // Keep aspect ratio + rate = widthRate + } + if screen.fullscreen { + // Vertically in center + point.y -= (rect.height - viewRect.height / rate)/2 + } + point.y *= rate + point.y = viewRect.height - point.y + // For traffic light buttons when not fullscreen + if point.y < 0 { + return nil + } + // Horizontally in center + point.x -= (rect.width - viewRect.width / rate)/2 + point.x *= rate + return point + } + + public func handleScrollWheel(deltaX: CGFloat, deltaY: CGFloat) -> Bool { + _ = ActionDispatcher.dispatch(key: KeyCodeNames.scrollWheelDrag, valueX: deltaX, valueY: deltaY) + // I dont know why but this is the logic before the refactor. + // Might be a mistake but keeping it for now + return false + } + + public func handleMove(deltaX: CGFloat, deltaY: CGFloat) -> Bool { + // fake mouse handler: + // default direction pad: press handler + // draggable direction pad: move handler + // default button: lift handler + // kinda hacky but.. IT WORKS! + if ActionDispatcher.getDispatchPriority(key: KeyCodeNames.fakeMouse) == .DRAGGABLE { + guard let pos = TouchscreenMouseEventAdapter.cursorPos() else { return false } + return ActionDispatcher.dispatch(key: KeyCodeNames.fakeMouse, valueX: pos.x, valueY: pos.y) + } + return false + } + + public func handleLeftButton(pressed: Bool) -> Bool { + // It is necessary to calculate pos before pushing to dispatch queue + // Otherwise, we don't know whether to return false or true + guard let pos = TouchscreenMouseEventAdapter.cursorPos() else { return false } + if pressed { + return ActionDispatcher.dispatch(key: KeyCodeNames.fakeMouse, valueX: pos.x, valueY: pos.y) + } else { + return ActionDispatcher.dispatch(key: KeyCodeNames.fakeMouse, pressed: pressed) + } + } + + public func handleOtherButton(id: Int, pressed: Bool) -> Bool { + ActionDispatcher.dispatch(key: EditorMouseEventAdapter.getMouseButtonName(id), + pressed: pressed) + } + + public func cursorHidden() -> Bool { + false + } + +} diff --git a/PlayTools/Controls/Frontend/EventAdapter/Mouse/Instances/TransparentMouseEventAdapter.swift b/PlayTools/Controls/Frontend/EventAdapter/Mouse/Instances/TransparentMouseEventAdapter.swift new file mode 100644 index 00000000..590907f3 --- /dev/null +++ b/PlayTools/Controls/Frontend/EventAdapter/Mouse/Instances/TransparentMouseEventAdapter.swift @@ -0,0 +1,34 @@ +// +// TransparentMouseEventAdapter.swift +// PlayTools +// +// Created by 许沂聪 on 2023/9/16. +// + +import Foundation + +// Mouse events handler when keymapping is disabled + +public class TransparentMouseEventAdapter: MouseEventAdapter { + public func handleScrollWheel(deltaX: CGFloat, deltaY: CGFloat) -> Bool { + // When editing text, scroll should effect on text input box + false + } + + public func handleMove(deltaX: CGFloat, deltaY: CGFloat) -> Bool { + false + } + + public func handleLeftButton(pressed: Bool) -> Bool { + false + } + + public func handleOtherButton(id: Int, pressed: Bool) -> Bool { + false + } + + public func cursorHidden() -> Bool { + false + } + +} diff --git a/PlayTools/Controls/Frontend/EventAdapter/Mouse/MouseEventAdapter.swift b/PlayTools/Controls/Frontend/EventAdapter/Mouse/MouseEventAdapter.swift new file mode 100644 index 00000000..bb2f5279 --- /dev/null +++ b/PlayTools/Controls/Frontend/EventAdapter/Mouse/MouseEventAdapter.swift @@ -0,0 +1,19 @@ +// +// MouseEventAdapter.swift +// PlayTools +// +// Created by 许沂聪 on 2023/9/16. +// + +import Foundation + +// All mouse events under any mode + +public protocol MouseEventAdapter: EventAdapter { + func cursorHidden() -> Bool + + func handleScrollWheel(deltaX: CGFloat, deltaY: CGFloat) -> Bool + func handleMove(deltaX: CGFloat, deltaY: CGFloat) -> Bool + func handleLeftButton(pressed: Bool) -> Bool + func handleOtherButton(id: Int, pressed: Bool) -> Bool +} diff --git a/PlayTools/Controls/Frontend/ModeAutomaton.swift b/PlayTools/Controls/Frontend/ModeAutomaton.swift new file mode 100644 index 00000000..2aca0fec --- /dev/null +++ b/PlayTools/Controls/Frontend/ModeAutomaton.swift @@ -0,0 +1,64 @@ +// +// ModeAutomaton.swift +// PlayTools +// +// Created by 许沂聪 on 2023/9/17. +// + +import Foundation + +// This class manages control mode transitions + +public class ModeAutomaton { + static public func onOption() -> Bool { + if mode == .EDITOR || mode == .TEXT_INPUT { + return false + } + if mode == .OFF { + mode.set(.CAMERA_ROTATE) + + } else if mode == .ARBITRARY_CLICK && ActionDispatcher.cursorHideNecessary { + mode.set(.CAMERA_ROTATE) + + } else if mode == .CAMERA_ROTATE { + if PlaySettings.shared.noKMOnInput { + mode.set(.ARBITRARY_CLICK) + } else { + mode.set(.OFF) + } + } + // Some people want option key act as touchpad-touchscreen mapper + return false + } + + static public func onCmdK() { + guard settings.keymapping else { + return + } + + EditorController.shared.switchMode() + + if mode == .EDITOR && !EditorController.shared.editorMode { + mode.set(.CAMERA_ROTATE) + ActionDispatcher.build() + Toucher.writeLog(logMessage: "editor closed") + } else if EditorController.shared.editorMode { + mode.set(.EDITOR) + Toucher.writeLog(logMessage: "editor opened") + } + } + + static public func onUITextInputBeginEdit() { + if mode == .EDITOR { + return + } + mode.set(.TEXT_INPUT) + } + + static public func onUITextInputEndEdit() { + if mode == .EDITOR { + return + } + mode.set(.ARBITRARY_CLICK) + } +} diff --git a/PlayTools/Controls/MenuController.swift b/PlayTools/Controls/MenuController.swift index 1da0786c..19db6674 100644 --- a/PlayTools/Controls/MenuController.swift +++ b/PlayTools/Controls/MenuController.swift @@ -27,7 +27,7 @@ class RotateViewController: UIViewController { extension UIApplication { @objc func switchEditorMode(_ sender: AnyObject) { - EditorController.shared.switchMode() + ModeAutomaton.onCmdK() } @objc @@ -51,6 +51,21 @@ extension UIApplication { Toucher.writeLog(logMessage: "mark") Toast.showHint(title: "Log marked") } + + @objc + func rotateView(_ sender: AnyObject) { + for scene in connectedScenes { + guard let windowScene = scene as? UIWindowScene else { continue } + for window in windowScene.windows { + guard let rootViewController = window.rootViewController else { continue } + rootViewController.rotateView(sender) + } + } + + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2, execute: { + Toast.showHint(title: "Rotated") + }) + } } extension UIViewController { @@ -85,7 +100,7 @@ var keymappingSelectors = [#selector(UIApplication.switchEditorMode(_:)), #selector(UIApplication.removeElement(_:)), #selector(UIApplication.upscaleElement(_:)), #selector(UIApplication.downscaleElement(_:)), - #selector(UIViewController.rotateView(_:)) + #selector(UIApplication.rotateView(_:)) ] class MenuController { diff --git a/PlayTools/Controls/PTFakeTouch/NSObject+Swizzle.m b/PlayTools/Controls/PTFakeTouch/NSObject+Swizzle.m index 9733eba3..591c57aa 100644 --- a/PlayTools/Controls/PTFakeTouch/NSObject+Swizzle.m +++ b/PlayTools/Controls/PTFakeTouch/NSObject+Swizzle.m @@ -11,6 +11,7 @@ #import "UIKit/UIKit.h" #import #import "PTFakeMetaTouch.h" +#import __attribute__((visibility("hidden"))) @interface PTSwizzleLoader : NSObject @@ -49,6 +50,16 @@ - (void) swizzleInstanceMethod:(SEL)origSelector withMethod:(SEL)newSelector } } +- (void) swizzleExchangeMethod:(SEL)origSelector withMethod:(SEL)newSelector +{ + Class cls = [self class]; + // If current class doesn't exist selector, then get super + Method originalMethod = class_getInstanceMethod(cls, origSelector); + Method swizzledMethod = class_getInstanceMethod(cls, newSelector); + + method_exchangeImplementations(originalMethod, swizzledMethod); +} + - (BOOL) hook_prefersPointerLocked { return false; } @@ -111,6 +122,15 @@ - (double) get_default_width { } +- (void) hook_setCurrentSubscription:(VSSubscription *)currentSubscription { + // do nothing +} + +// Hook for UIUserInterfaceIdiom + +// - (long long) hook_userInterfaceIdiom { +// return UIUserInterfaceIdiomPad; +// } bool menuWasCreated = false; - (id) initWithRootMenuHook:(id)rootMenu { @@ -157,7 +177,11 @@ + (void)load { // This is an experimental fix if ([[PlaySettings shared] inverseScreenValues]) { // This lines set External Scene settings and other IOS10 Runtime services by swizzling - [objc_getClass("FBSSceneSettings") swizzleInstanceMethod:@selector(frame) withMethod:@selector(hook_frameDefault)]; + // In Sonoma 14.1 betas, frame method seems to be moved to FBSSceneSettingsCore + if(@available(iOS 17.1, *)) + [objc_getClass("FBSSceneSettingsCore") swizzleExchangeMethod:@selector(frame) withMethod:@selector(hook_frameDefault)]; + else + [objc_getClass("FBSSceneSettings") swizzleInstanceMethod:@selector(frame) withMethod:@selector(hook_frameDefault)]; [objc_getClass("FBSSceneSettings") swizzleInstanceMethod:@selector(bounds) withMethod:@selector(hook_boundsDefault)]; [objc_getClass("FBSDisplayMode") swizzleInstanceMethod:@selector(size) withMethod:@selector(hook_sizeDelfault)]; @@ -168,7 +192,10 @@ + (void)load { [objc_getClass("UIScreen") swizzleInstanceMethod:@selector(scale) withMethod:@selector(hook_scale)]; } else { // This acutally runs when adaptiveDisplay is normally triggered - [objc_getClass("FBSSceneSettings") swizzleInstanceMethod:@selector(frame) withMethod:@selector(hook_frame)]; + if(@available(iOS 17.1, *)) + [objc_getClass("FBSSceneSettingsCore") swizzleExchangeMethod:@selector(frame) withMethod:@selector(hook_frame)]; + else + [objc_getClass("FBSSceneSettings") swizzleInstanceMethod:@selector(frame) withMethod:@selector(hook_frame)]; [objc_getClass("FBSSceneSettings") swizzleInstanceMethod:@selector(bounds) withMethod:@selector(hook_bounds)]; [objc_getClass("FBSDisplayMode") swizzleInstanceMethod:@selector(size) withMethod:@selector(hook_size)]; @@ -189,7 +216,10 @@ + (void)load { CGFloat newValueH = (CGFloat)[self get_default_height]; [[PlaySettings shared] setValue:@(newValueH) forKey:@"windowSizeHeight"]; if (![[PlaySettings shared] inverseScreenValues]) { - [objc_getClass("FBSSceneSettings") swizzleInstanceMethod:@selector(frame) withMethod:@selector(hook_frameDefault)]; + if(@available(iOS 17.1, *)) + [objc_getClass("FBSSceneSettingsCore") swizzleExchangeMethod:@selector(frame) withMethod:@selector(hook_frameDefault)]; + else + [objc_getClass("FBSSceneSettings") swizzleInstanceMethod:@selector(frame) withMethod:@selector(hook_frameDefault)]; [objc_getClass("FBSSceneSettings") swizzleInstanceMethod:@selector(bounds) withMethod:@selector(hook_boundsDefault)]; [objc_getClass("FBSDisplayMode") swizzleInstanceMethod:@selector(size) withMethod:@selector(hook_sizeDelfault)]; } @@ -203,7 +233,10 @@ + (void)load { } else { if ([[PlaySettings shared] adaptiveDisplay]) { - [objc_getClass("FBSSceneSettings") swizzleInstanceMethod:@selector(frame) withMethod:@selector(hook_frame)]; + if(@available(iOS 17.1, *)) + [objc_getClass("FBSSceneSettingsCore") swizzleExchangeMethod:@selector(frame) withMethod:@selector(hook_frame)]; + else + [objc_getClass("FBSSceneSettings") swizzleInstanceMethod:@selector(frame) withMethod:@selector(hook_frame)]; [objc_getClass("FBSSceneSettings") swizzleInstanceMethod:@selector(bounds) withMethod:@selector(hook_bounds)]; [objc_getClass("FBSDisplayMode") swizzleInstanceMethod:@selector(size) withMethod:@selector(hook_size)]; } @@ -211,6 +244,11 @@ + (void)load { [objc_getClass("_UIMenuBuilder") swizzleInstanceMethod:sel_getUid("initWithRootMenu:") withMethod:@selector(initWithRootMenuHook:)]; [objc_getClass("IOSViewController") swizzleInstanceMethod:@selector(prefersPointerLocked) withMethod:@selector(hook_prefersPointerLocked)]; + // Set idiom to iPad + // [objc_getClass("UIDevice") swizzleInstanceMethod:@selector(userInterfaceIdiom) withMethod:@selector(hook_userInterfaceIdiom)]; + // [objc_getClass("UITraitCollection") swizzleInstanceMethod:@selector(userInterfaceIdiom) withMethod:@selector(hook_userInterfaceIdiom)]; + + [objc_getClass("VSSubscriptionRegistrationCenter") swizzleInstanceMethod:@selector(setCurrentSubscription:) withMethod:@selector(hook_setCurrentSubscription:)]; } @end diff --git a/PlayTools/Controls/PlayController.swift b/PlayTools/Controls/PlayController.swift deleted file mode 100644 index 64c641ec..00000000 --- a/PlayTools/Controls/PlayController.swift +++ /dev/null @@ -1,121 +0,0 @@ -// -// PlayController.swift -// PlayTools -// -// Created by 许沂聪 on 2023/4/21. -// - -import Foundation -import GameController - -class PlayController { - private static var directionPadXValue: Float = 0, - directionPadYValue: Float = 0, - thumbstickCursorControl: [String: (((CGFloat, CGFloat) -> Void)?, CGFloat, CGFloat) -> Void] - = ["Left Thumbstick": ThumbstickCursorControl().update, "Right Thumbstick": ThumbstickCursorControl().update] - - public static func initialize() { - GCController.current?.extendedGamepad?.valueChangedHandler = handleEvent - } - - static func handleEditorEvent(_ profile: GCExtendedGamepad, _ element: GCControllerElement) { - // 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! - } - } - EditorController.shared.setKey(alias) - } - - static func handleEvent(_ profile: GCExtendedGamepad, _ element: GCControllerElement) { - let name: String = element.aliases.first! - if let buttonElement = element as? GCControllerButtonInput { - 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 { - PlayController.handleDirectionPad(profile, dpadElement) - } else { - Toast.showOver(msg: "unrecognised controller element input happens") - } - } - public static func handleDirectionPad(_ profile: GCExtendedGamepad, _ dpad: GCControllerDirectionPad) { - let name = dpad.aliases.first! - let xAxis = dpad.xAxis, yAxis = dpad.yAxis - if name == "Direction Pad" { - if (xAxis.value > 0) != (directionPadXValue > 0) { - PlayController.handleEvent(profile, dpad.right) - } - if (xAxis.value < 0) != (directionPadXValue < 0) { - PlayController.handleEvent(profile, dpad.left) - } - if (yAxis.value > 0) != (directionPadYValue > 0) { - PlayController.handleEvent(profile, dpad.up) - } - if (yAxis.value < 0) != (directionPadYValue < 0) { - PlayController.handleEvent(profile, dpad.down) - } - directionPadXValue = xAxis.value - directionPadYValue = yAxis.value - return - } - let deltaX = xAxis.value, deltaY = yAxis.value - let cgDx = CGFloat(deltaX) - let cgDy = CGFloat(deltaY) - thumbstickCursorControl[name]!( - PlayInput.draggableHandler[name] ?? PlayInput.cameraMoveHandler[name], cgDx * 6, cgDy * 6) - PlayInput.joystickHandler[name]?(cgDx, cgDy) - } -} - -class ThumbstickCursorControl { - private var thumbstickVelocity: CGVector = CGVector.zero, - thumbstickPolling: Bool = false, - eventHandler: ((CGFloat, CGFloat) -> Void)! - - static private func isVectorSignificant(_ vector: CGVector) -> Bool { - return vector.dx.magnitude + vector.dy.magnitude > 0.2 - } - - public func update(handler: ((CGFloat, CGFloat) -> Void)?, velocityX: CGFloat, velocityY: CGFloat) { - guard let hdlr = handler else { - if thumbstickPolling { - self.thumbstickVelocity.dx = 0 - self.thumbstickVelocity.dy = 0 - } - return - } - self.eventHandler = hdlr - self.thumbstickVelocity.dx = velocityX - self.thumbstickVelocity.dy = velocityY - if !thumbstickPolling { - PlayInput.touchQueue.async(execute: self.thumbstickPoll) - self.thumbstickPolling = true - } - } - - private func thumbstickPoll() { - if !ThumbstickCursorControl.isVectorSignificant(self.thumbstickVelocity) { - self.thumbstickPolling = false - return - } - self.eventHandler(self.thumbstickVelocity.dx, self.thumbstickVelocity.dy) - PlayInput.touchQueue.asyncAfter( - deadline: DispatchTime.now() + 0.017, execute: self.thumbstickPoll) - } -} diff --git a/PlayTools/Controls/PlayInput.swift b/PlayTools/Controls/PlayInput.swift index d9866252..bcd6c339 100644 --- a/PlayTools/Controls/PlayInput.swift +++ b/PlayTools/Controls/PlayInput.swift @@ -1,89 +1,14 @@ import Foundation -import GameController import UIKit +// This class is a coordinator (and module entrance), coordinating other concrete classes + class PlayInput { static let shared = PlayInput() - var actions = [Action]() - static var shouldLockCursor = true - static var touchQueue = DispatchQueue.init(label: "playcover.toucher", qos: .userInteractive, + static var touchQueue = DispatchQueue.init(label: "playcover.toucher", + qos: .userInteractive, autoreleaseFrequency: .workItem) - static public var buttonHandlers: [String: [(Bool) -> Void]] = [:], - draggableHandler: [String: (CGFloat, CGFloat) -> Void] = [:], - cameraMoveHandler: [String: (CGFloat, CGFloat) -> Void] = [:], - cameraScaleHandler: [String: (CGFloat, CGFloat) -> Void] = [:], - joystickHandler: [String: (CGFloat, CGFloat) -> Void] = [:] - - func invalidate() { - for action in self.actions { - action.invalidate() - } - } - - static public func registerButton(key: String, handler: @escaping (Bool) -> Void) { - if "LMB" == key { - PlayInput.shouldLockCursor = true - } - if PlayInput.buttonHandlers[key] == nil { - PlayInput.buttonHandlers[key] = [] - } - PlayInput.buttonHandlers[key]!.append(handler) - } - - func parseKeymap() { - actions = [PlayMice.shared] - PlayInput.buttonHandlers.removeAll(keepingCapacity: true) - PlayInput.shouldLockCursor = false - 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)) - } - } - if !PlayInput.shouldLockCursor { - PlayInput.shouldLockCursor = PlayMice.shared.mouseMovementMapped() - } - } - - public func toggleEditor(show: Bool) { - mode.setMapping(!show) - Toucher.writeLog(logMessage: "editor opened? \(show)") - if show { - self.invalidate() - if let keyboard = GCKeyboard.coalesced!.keyboardInput { - keyboard.keyChangedHandler = { _, _, keyCode, pressed in - PlayKeyboard.handleEditorEvent(keyCode: keyCode, pressed: pressed) - } - } - if let controller = GCController.current?.extendedGamepad { - controller.valueChangedHandler = PlayController.handleEditorEvent - } - } else { - GCKeyboard.coalesced?.keyboardInput?.keyChangedHandler = nil - PlayController.initialize() - parseKeymap() - _ = ControlMode.trySwap() - } - } - - static public func cmdPressed() -> Bool { - return AKInterface.shared!.cmdPressed - } func initialize() { if !PlaySettings.shared.keymapping { @@ -93,86 +18,19 @@ class PlayInput { let centre = NotificationCenter.default let main = OperationQueue.main - centre.addObserver(forName: NSNotification.Name.GCControllerDidConnect, object: nil, queue: main) { _ in - if EditorController.shared.editorMode { - self.toggleEditor(show: true) - } else { - PlayController.initialize() - } - } - parseKeymap() centre.addObserver(forName: NSNotification.Name(rawValue: "NSWindowDidBecomeKeyNotification"), object: nil, queue: main) { _ in - if !mode.visible { + if mode.cursorHidden() { AKInterface.shared!.warpCursor() } } + DispatchQueue.main.asyncAfter(deadline: .now() + 5, qos: .utility) { - if !mode.visible || self.actions.count <= 0 || !PlayInput.shouldLockCursor { + if mode.cursorHidden() || !ActionDispatcher.cursorHideNecessary { return } Toast.initialize() } - PlayKeyboard.initialize() - PlayMice.shared.initialize() - - } -} - -class PlayKeyboard { - public static func handleEditorEvent(keyCode: GCKeyCode, pressed: Bool) { - if !PlayInput.cmdPressed() - && !PlayKeyboard.FORBIDDEN.contains(keyCode) - && KeyCodeNames.keyCodes[keyCode.rawValue] != nil { - EditorController.shared.setKey(keyCode.rawValue) - } - } - - private static let FORBIDDEN: [GCKeyCode] = [ - .leftGUI, - .rightGUI, - .leftAlt, - .rightAlt, - .printScreen - ] - - static func handleEvent(_ keyCode: UInt16, _ pressed: Bool) -> Bool { - let name = KeyCodeNames.virtualCodes[keyCode] ?? "Btn" - guard let handlers = PlayInput.buttonHandlers[name] else { - return false - } - var mapped = false - for handler in handlers { - PlayInput.touchQueue.async(qos: .userInteractive, execute: { - handler(pressed) - }) - mapped = true - } - return mapped - } - - public static func initialize() { - let centre = NotificationCenter.default - let main = OperationQueue.main - centre.addObserver(forName: UIApplication.keyboardDidHideNotification, object: nil, queue: main) { _ in - mode.setMapping(true) - Toucher.writeLog(logMessage: "virtual keyboard did hide") - } - centre.addObserver(forName: UIApplication.keyboardWillShowNotification, object: nil, queue: main) { _ in - mode.setMapping(false) - Toucher.writeLog(logMessage: "virtual keyboard will show") - } - AKInterface.shared!.setupKeyboard(keyboard: {keycode, pressed, isRepeat in - if !mode.keyboardMapped { - // explicitly ignore repeated Enter key - return isRepeat && keycode == 36 - } - if isRepeat { - return true - } - let mapped = PlayKeyboard.handleEvent(keycode, pressed) - return mapped - }, - swapMode: ControlMode.trySwap) + mode.initialize() } } diff --git a/PlayTools/Controls/PlayMice.swift b/PlayTools/Controls/PlayMice.swift deleted file mode 100644 index 487090db..00000000 --- a/PlayTools/Controls/PlayMice.swift +++ /dev/null @@ -1,296 +0,0 @@ -// -// PlayMice.swift -// PlayTools -// - -import Foundation - -public class PlayMice: Action { - - public static let shared = PlayMice() - public static let elementName = "Mouse" - - public func initialize() { - setupLeftButton() - setupMouseButton(right: true) - setupMouseButton(right: false) - AKInterface.shared!.setupMouseMoved(mouseMoved: {deltaX, deltaY in - // this closure's return value only takes effect when any mouse button pressed - if !mode.keyboardMapped { - return false - } - PlayInput.touchQueue.async(qos: .userInteractive, execute: { - self.handleMouseMoved(deltaX: deltaX, deltaY: deltaY) - }) - return true - }) - AKInterface.shared!.setupScrollWheel({deltaX, deltaY in - if let cameraScale = PlayInput.cameraScaleHandler[PlayMice.elementName] { - cameraScale(deltaX, deltaY) - let eventConsumed = !mode.visible - return eventConsumed - } - return false - }) - } - - var fakedMouseTouchPointId: Int? - var fakedMousePressed: Bool {fakedMouseTouchPointId != nil} - - public func mouseMovementMapped() -> Bool { - for handler in [PlayInput.cameraMoveHandler, PlayInput.joystickHandler] - where handler[PlayMice.elementName] != nil { - return true - } - return false - } - - public func cursorPos() -> CGPoint? { - var point = AKInterface.shared!.mousePoint - let rect = AKInterface.shared!.windowFrame - if rect.width < 1 || rect.height < 1 { - return nil - } - let viewRect: CGRect = screen.screenRect - let widthRate = viewRect.width / rect.width - var rate = viewRect.height / rect.height - if widthRate > rate { - // Keep aspect ratio - rate = widthRate - } - if screen.fullscreen { - // Vertically in center - point.y -= (rect.height - viewRect.height / rate)/2 - } - point.y *= rate - point.y = viewRect.height - point.y - // For traffic light buttons when not fullscreen - if point.y < 0 { - return nil - } - // Horizontally in center - point.x -= (rect.width - viewRect.width / rate)/2 - point.x *= rate - return point - } - - public func handleMouseMoved(deltaX: CGFloat, deltaY: CGFloat) { - let sensy = CGFloat(PlaySettings.shared.sensitivity * 0.6) - let cgDx = deltaX * sensy, - cgDy = -deltaY * sensy - let name = PlayMice.elementName - if let draggableUpdate = PlayInput.draggableHandler[name] { - draggableUpdate(cgDx, cgDy) - } else if mode.visible { - if self.fakedMousePressed { - if let pos = self.cursorPos() { - Toucher.touchcam(point: pos, phase: UITouch.Phase.moved, tid: &fakedMouseTouchPointId) - } - } - } else { - PlayInput.cameraMoveHandler[name]?(cgDx, cgDy) - PlayInput.joystickHandler[name]?(cgDx, cgDy) - } - } - - private func setupMouseButton(right: Bool) { - let keyCode = right ? -2 : -3 - guard let keyName = KeyCodeNames.keyCodes[keyCode] else { - Toast.showHint(title: "Failed initializing \(right ? "right" : "other") mouse button input") - return - } - AKInterface.shared!.setupMouseButton(left: false, right: right) {pressed in - if mode.keyboardMapped { // if mapping - if let handlers = PlayInput.buttonHandlers[keyName] { - PlayInput.touchQueue.async(qos: .userInteractive, execute: { - for handler in handlers { - handler(pressed) - } - }) - // if mapped to any button, consumed and dispatch - return false - } - // if not mapped, transpass to app - return true - } else if EditorController.shared.editorMode { // if editor is open, consumed and set button - if pressed { - // asynced to return quickly. this branch contains UI operation so main queue. - // main queue is fine. should not be slower than keyboard - DispatchQueue.main.async(qos: .userInteractive, execute: { - EditorController.shared.setKey(keyCode) - Toucher.writeLog(logMessage: "mouse button editor set") - }) - } - return false - } else { // if typing, transpass event to app - Toucher.writeLog(logMessage: "mouse button pressed? \(pressed)") - return true - } - } - } - // using high priority event handlers to prevent lag and stutter in demanding games - // but no free lunch. high priority handlers cannot execute for too long - // exceeding the time limit causes even more lag - private func setupLeftButton() { - AKInterface.shared!.setupMouseButton(left: true, right: false) {pressed in - if !mode.keyboardMapped { - Toucher.writeLog(logMessage: "left button pressed? \(pressed)") - return true - } - guard let curPos = self.cursorPos() else { return true } - PlayInput.touchQueue.async(qos: .userInteractive, execute: { - // considering cases where cursor becomes hidden while holding left button - if self.fakedMousePressed { - Toucher.touchcam(point: curPos, phase: UITouch.Phase.ended, tid: &self.fakedMouseTouchPointId) - return - } - if mode.visible && pressed { - Toucher.touchcam(point: curPos, - phase: UITouch.Phase.began, - tid: &self.fakedMouseTouchPointId) - return - } - // considering cases where cursor becomes visible while holding left button - if let handlers = PlayInput.buttonHandlers["LMB"] { - for handler in handlers { - handler(pressed) - } - return - } - }) - return false - } - } - // For all other actions, this is a destructor. should release held resources. - func invalidate() { - Toucher.touchcam(point: self.cursorPos() ?? CGPoint(x: 10, y: 10), - phase: UITouch.Phase.ended, tid: &self.fakedMouseTouchPointId) - } -} - -class CameraAction: Action { - var swipeMove, swipeScale1, swipeScale2: SwipeAction - static var swipeDrag = SwipeAction() - var key: String! - var center: CGPoint - var distance1: CGFloat = 100, distance2: CGFloat = 100 - init(data: MouseArea) { - self.key = data.keyName - let centerX = data.transform.xCoord.absoluteX - let centerY = data.transform.yCoord.absoluteY - center = CGPoint(x: centerX, y: centerY) - swipeMove = SwipeAction() - swipeScale1 = SwipeAction() - swipeScale2 = SwipeAction() - PlayInput.cameraMoveHandler[key] = self.moveUpdated - PlayInput.cameraScaleHandler[PlayMice.elementName] = {deltaX, deltaY in - PlayInput.touchQueue.async(qos: .userInteractive, execute: { - if mode.visible { - CameraAction.dragUpdated(deltaX, deltaY) - } else { - self.scaleUpdated(deltaX, deltaY) - } - }) - } - } - func moveUpdated(_ deltaX: CGFloat, _ deltaY: CGFloat) { - swipeMove.move(from: {return center}, deltaX: deltaX, deltaY: deltaY) - } - - func scaleUpdated(_ deltaX: CGFloat, _ deltaY: CGFloat) { - let distance = distance1 + distance2 - let moveY = deltaY * (distance / 100.0) - distance1 += moveY - distance2 += moveY - - swipeScale1.move(from: { - self.distance1 = 100 - return CGPoint(x: center.x, y: center.y - 100) - }, deltaX: 0, deltaY: moveY) - - swipeScale2.move(from: { - self.distance2 = 100 - return CGPoint(x: center.x, y: center.y + 100) - }, deltaX: 0, deltaY: -moveY) - } - - static func dragUpdated(_ deltaX: CGFloat, _ deltaY: CGFloat) { - swipeDrag.move(from: PlayMice.shared.cursorPos, deltaX: deltaX * 4, deltaY: -deltaY * 4) - } - - func invalidate() { - PlayInput.cameraMoveHandler.removeValue(forKey: key) - PlayInput.cameraScaleHandler[PlayMice.elementName] = nil - swipeMove.invalidate() - swipeScale1.invalidate() - swipeScale2.invalidate() - } -} - -class SwipeAction: Action { - var location: CGPoint = CGPoint.zero - private var id: Int? - let timer = DispatchSource.makeTimerSource(flags: [], queue: PlayInput.touchQueue) - init() { - timer.schedule(deadline: DispatchTime.now() + 1, repeating: 0.1, leeway: DispatchTimeInterval.milliseconds(50)) - timer.setEventHandler(qos: .userInteractive, handler: self.checkEnded) - timer.activate() - timer.suspend() - } - - func delay(_ delay: Double, closure: @escaping () -> Void) { - let when = DispatchTime.now() + delay - PlayInput.touchQueue.asyncAfter(deadline: when, execute: closure) - } - // Count swipe duration - var counter = 0 - // if should wait before beginning next touch - var cooldown = false - var lastCounter = 0 - - func checkEnded() { - if self.counter == self.lastCounter { - if self.counter < 4 { - counter += 1 - } else { - timer.suspend() - self.doLiftOff() - } - } - self.lastCounter = self.counter - } - - public func move(from: () -> CGPoint?, deltaX: CGFloat, deltaY: CGFloat) { - if id == nil { - if cooldown { - return - } - guard let start = from() else {return} - location = start - counter = 0 - Toucher.touchcam(point: location, phase: UITouch.Phase.began, tid: &id) - timer.resume() - } - // count touch duration - counter += 1 - self.location.x += deltaX - self.location.y -= deltaY - Toucher.touchcam(point: self.location, phase: UITouch.Phase.moved, tid: &id) - } - - public func doLiftOff() { - if id == nil { - return - } - Toucher.touchcam(point: self.location, phase: UITouch.Phase.ended, tid: &id) - delay(0.02) { - self.cooldown = false - } - cooldown = true - } - - func invalidate() { - timer.cancel() - self.doLiftOff() - } -} diff --git a/PlayTools/Keymap/DragElementsView.swift b/PlayTools/Keymap/DragElementsView.swift index 84e93e03..24dfe7a1 100644 --- a/PlayTools/Keymap/DragElementsView.swift +++ b/PlayTools/Keymap/DragElementsView.swift @@ -57,7 +57,7 @@ class KeymapHolder: CircleMenuDelegate { case 2: EditorController.shared.addMouseArea(globalPoint!) default: - Toast.showOver(msg: "item \(atIndex) is not recognised") + Toast.showHint(title: "item \(atIndex) is not recognizable") // EditorController.shared.addMouseJoystick(globalPoint!) } hideWithAnimation() diff --git a/PlayTools/Keymap/EditorController.swift b/PlayTools/Keymap/EditorController.swift index 3b44ee66..70eccdcc 100644 --- a/PlayTools/Keymap/EditorController.swift +++ b/PlayTools/Keymap/EditorController.swift @@ -58,12 +58,10 @@ class EditorController { // menu still holds this object until next responder hit test editorWindow = nil previousWindow?.makeKeyAndVisible() - PlayInput.shared.toggleEditor(show: false) focusedControl = nil Toast.showHint(title: NSLocalizedString("hint.keymapSaved", tableName: "Playtools", value: "Keymap Saved", comment: "")) } else { - PlayInput.shared.toggleEditor(show: true) previousWindow = screen.keyWindow editorWindow = initWindow() editorWindow?.makeKeyAndVisible() diff --git a/PlayTools/Keymap/KeyCodeNames.swift b/PlayTools/Keymap/KeyCodeNames.swift index 4e62696b..fc953d3c 100644 --- a/PlayTools/Keymap/KeyCodeNames.swift +++ b/PlayTools/Keymap/KeyCodeNames.swift @@ -2,6 +2,16 @@ class KeyCodeNames { public static let defaultCode = -10 + public static let leftMouseButton = "LMB" + public static let rightMouseButton = "RMB" + public static let middleMouseButton = "MMB" + public static let mouseMove = "Mouse" + + // Internal used names, not stored to keymap + public static let scrollWheelScale = "ScrollWheelScale" + public static let scrollWheelDrag = "ScrollWheelDrag" + public static let fakeMouse = "FakeMouse" + public static let keyCodes = [ -4: "cA", -5: "cX", @@ -177,6 +187,85 @@ public static let virtualCodes: [UInt16: String] = [ 54: "RCmd", 59: "LCtrl", 62: "RCtrl" +] + public static let mapNSEventVirtualCodeToGCKeyCodeRawValue: [UInt16: Int] = [ + 0: 4, + 1: 22, + 2: 7, + 3: 9, + 4: 11, + 5: 10, + 6: 29, + 7: 27, + 8: 6, + 9: 25, + 11: 5, + 12: 20, + 13: 26, + 14: 8, + 15: 21, + 16: 28, + 17: 23, + 18: 30, + 19: 31, + 20: 32, + 21: 33, + 22: 35, + 23: 34, + 24: 46, + 25: 38, + 26: 36, + 27: 45, + 28: 37, + 29: 39, + 30: 48, + 31: 18, + 32: 24, + 33: 47, + 34: 12, + 35: 19, + 36: 40, + 37: 15, + 38: 13, + 39: 52, + 40: 14, + 41: 51, + 42: 49, + 43: 54, + 44: 56, + 45: 17, + 46: 16, + 47: 55, + 48: 43, + 49: 44, + 50: 53, + 51: 42, + 53: 41, + 54: 231, + 55: 227, + 56: 225, + 57: 57, + 58: 226, + 59: 224, + 60: 229, + 61: 230, + 62: 228, + 96: 62, + 97: 63, + 98: 64, + 99: 60, + 100: 65, + 101: 66, + 103: 68, + 109: 67, + 111: 69, + 118: 61, + 120: 59, + 122: 58, + 123: 80, + 124: 79, + 125: 81, + 126: 82 ] } let mapGCKeyCodeRawValuetoNSEventVirtualCode = [ diff --git a/PlayTools/MysticRunes/PlayShadow.m b/PlayTools/MysticRunes/PlayShadow.m index edf61ccb..8e2d5160 100644 --- a/PlayTools/MysticRunes/PlayShadow.m +++ b/PlayTools/MysticRunes/PlayShadow.m @@ -159,12 +159,18 @@ @implementation PlayShadowLoader + (void) load { [self debugLogger:@"PlayShadow is now loading"]; - if ([[PlaySettings shared] bypass]) [self loadJailbreakBypass]; + // Gate this behind an environment variable + if ([[NSProcessInfo processInfo].environment[@"USE_EXTRA_ANTIJB"] isEqualToString:@"1"]) { + [self loadJailbreakBypass]; + } // if ([[PlaySettings shared] bypass]) [self loadEnvironmentBypass]; # disabled as it might be too powerful // Swizzle ATTrackingManager [objc_getClass("ATTrackingManager") swizzleClassMethod:@selector(requestTrackingAuthorizationWithCompletionHandler:) withMethod:@selector(pm_return_2_with_completion_handler:)]; [objc_getClass("ATTrackingManager") swizzleClassMethod:@selector(trackingAuthorizationStatus) withMethod:@selector(pm_return_2)]; + + // canResizeToFitContent + // [objc_getClass("UIWindow") swizzleInstanceMethod:@selector(canResizeToFitContent) withMethod:@selector(pm_return_true)]; } + (void) loadJailbreakBypass { diff --git a/PlayTools/MysticRunes/PlayedApple.swift b/PlayTools/MysticRunes/PlayedApple.swift index 34acf2b5..0e8bbf9a 100644 --- a/PlayTools/MysticRunes/PlayedApple.swift +++ b/PlayTools/MysticRunes/PlayedApple.swift @@ -35,8 +35,11 @@ public class PlayKeychain: NSObject { return keychainFolder } - private static func keychainPath(_ attributes: NSDictionary) -> URL { + private static func getKeychainPath(_ attributes: NSDictionary) -> URL { let keychainFolder = getKeychainDirectory() + if attributes["r_Ref"] as? Int == 1 { + attributes.setValue("keys", forKey: "class") + } // Generate a key path based on the key attributes let accountName = attributes[kSecAttrAccount as String] as? String ?? "" let serviceName = attributes[kSecAttrService as String] as? String ?? "" @@ -45,6 +48,27 @@ public class PlayKeychain: NSObject { .appendingPathComponent("\(serviceName)-\(accountName)-\(classType).plist") } + private static func findSimilarKeys(_ attributes: NSDictionary) -> URL? { + // Things we can fuzz: accountName + + let keychainFolder = getKeychainDirectory() + let serviceName = attributes[kSecAttrService as String] as? String ?? "" + let classType = attributes[kSecClass as String] as? String ?? "" + + let everyKeys = try? FileManager.default.contentsOfDirectory(at: keychainFolder!, + includingPropertiesForKeys: nil, + options: .skipsHiddenFiles) + let searchRegex = try? NSRegularExpression(pattern: "\(serviceName)-.*-\(classType).plist", + options: .caseInsensitive) + + for key in everyKeys! where searchRegex!.matches(in: key.path, + options: [], + range: NSRange(location: 0, length: key.path.count)).count > 0 { + return keychainFolder!.appendingPathComponent(key.lastPathComponent) + } + return nil + } + @objc public static func debugLogger(_ logContent: String) { if PlaySettings.shared.settingsData.playChainDebugging { NSLog("PC-DEBUG: \(logContent)") @@ -54,12 +78,12 @@ public class PlayKeychain: NSObject { // Store the entire dictionary as a plist // SecItemAdd(CFDictionaryRef attributes, CFTypeRef *result) @objc static public func add(_ attributes: NSDictionary, result: UnsafeMutablePointer?) -> OSStatus { - let keychainPath = keychainPath(attributes) + let keychainPath = getKeychainPath(attributes) // Check if the keychain file already exists - if FileManager.default.fileExists(atPath: keychainPath.path) { - debugLogger("Keychain file already exists") - return errSecDuplicateItem - } + // if FileManager.default.fileExists(atPath: keychainPath.path) { + // debugLogger("Keychain file already exists") + // return errSecDuplicateItem + // } // Write the dictionary to the keychain file do { try attributes.write(to: keychainPath) @@ -78,7 +102,7 @@ public class PlayKeychain: NSObject { // SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate) @objc static public func update(_ query: NSDictionary, attributesToUpdate: NSDictionary) -> OSStatus { // Get the path to the keychain file - let keychainPath = keychainPath(query) + let keychainPath = getKeychainPath(query) // Read the dictionary from the keychain file let keychainDict = NSDictionary(contentsOf: keychainPath) debugLogger("Read keychain file from \(keychainPath)") @@ -111,7 +135,7 @@ public class PlayKeychain: NSObject { // SecItemDelete(CFDictionaryRef query) @objc static public func delete(_ query: NSDictionary) -> OSStatus { // Get the path to the keychain file - let keychainPath = keychainPath(query) + let keychainPath = getKeychainPath(query) // Delete the keychain file do { try FileManager.default.removeItem(at: keychainPath) @@ -127,20 +151,77 @@ public class PlayKeychain: NSObject { @objc static public func copyMatching(_ query: NSDictionary, result: UnsafeMutablePointer?) -> OSStatus { // Get the path to the keychain file - let keychainPath = keychainPath(query) + var keychainPath = getKeychainPath(query) + // If the keychain file doesn't exist, attempt to find a similar key + if !FileManager.default.fileExists(atPath: keychainPath.path) { + return errSecItemNotFound + // if let similarKey = findSimilarKeys(query) { + // NSLog("Found similar key at \(similarKey)") + // keychainPath = similarKey + // } else { + // debugLogger("Keychain file not found at \(keychainPath)") + // return errSecItemNotFound + // } + } + // Read the dictionary from the keychain file let keychainDict = NSDictionary(contentsOf: keychainPath) // Check the `r_Attributes` key. If it is set to 1 in the query - // DROP, NOT IMPLEMENTED let classType = query[kSecClass as String] as? String ?? "" - if query["r_Attributes"] as? Int == 1 { - return errSecItemNotFound - } + // If the keychain file doesn't exist, return errSecItemNotFound if keychainDict == nil { debugLogger("Keychain file not found at \(keychainPath)") return errSecItemNotFound } + + if query["r_Attributes"] as? Int == 1 { + // if the keychainDict is nil, we need to return errSecItemNotFound + if keychainDict == nil { + debugLogger("Keychain file not found at \(keychainPath)") + return errSecItemNotFound + } + + // Create a dummy dictionary and return it + let dummyDict = NSMutableDictionary() + dummyDict.setValue(classType, forKey: "class") + dummyDict.setValue(keychainDict![kSecAttrAccount as String], forKey: "acct") + dummyDict.setValue(keychainDict![kSecAttrService as String], forKey: "svce") + dummyDict.setValue(keychainDict![kSecAttrGeneric as String], forKey: "gena") + result?.pointee = dummyDict + return errSecSuccess + } + + // Check for r_Ref + if query["r_Ref"] as? Int == 1 { + // Return the data on v_PersistentRef or v_Data if they exist + var key: CFTypeRef? + if let vData = keychainDict!["v_Data"] { + NSLog("found v_Data") + debugLogger("Read keychain file from \(keychainPath)") + key = vData as CFTypeRef + } + if let vPersistentRef = keychainDict!["v_PersistentRef"] { + NSLog("found persistent ref") + debugLogger("Read keychain file from \(keychainPath)") + key = vPersistentRef as CFTypeRef + } + + if key == nil { + debugLogger("Keychain file not found at \(keychainPath)") + return errSecItemNotFound + } + + let dummyKeyAttrs = [ + kSecAttrKeyType: keychainDict?["type"] ?? kSecAttrKeyTypeRSA, + kSecAttrKeyClass: keychainDict!["kcls"] ?? kSecAttrKeyClassPublic + ] as CFDictionary + + let secKey = SecKeyCreateWithData(key as! CFData, dummyKeyAttrs, nil) // swiftlint:disable:this force_cast + result?.pointee = secKey + return errSecSuccess + } + // Return v_Data if it exists if let vData = keychainDict!["v_Data"] { debugLogger("Read keychain file from \(keychainPath)") @@ -164,4 +245,4 @@ public class PlayKeychain: NSObject { return errSecItemNotFound } -} \ No newline at end of file +} diff --git a/PlayTools/PlaySettings.swift b/PlayTools/PlaySettings.swift index 0fab777e..8ba67509 100644 --- a/PlayTools/PlaySettings.swift +++ b/PlayTools/PlaySettings.swift @@ -69,8 +69,12 @@ let settings = PlaySettings.shared @objc lazy var windowFixMethod = settingsData.windowFixMethod @objc lazy var customScaler = settingsData.customScaler - + @objc lazy var rootWorkDir = settingsData.rootWorkDir + + @objc lazy var noKMOnInput = settingsData.noKMOnInput + + @objc lazy var enableScrollWheel = settingsData.enableScrollWheel } struct AppSettingsData: Codable { @@ -93,4 +97,6 @@ struct AppSettingsData: Codable { var inverseScreenValues = false var windowFixMethod = 0 var rootWorkDir = true + var noKMOnInput = false + var enableScrollWheel = true } diff --git a/Plugin.swift b/Plugin.swift index 56bbe84d..f10429c1 100644 --- a/Plugin.swift +++ b/Plugin.swift @@ -25,8 +25,8 @@ public protocol Plugin: NSObjectProtocol { func terminateApplication() func setupKeyboard(keyboard: @escaping(UInt16, Bool, Bool) -> Bool, swapMode: @escaping() -> Bool) - func setupMouseMoved(mouseMoved: @escaping(CGFloat, CGFloat) -> Bool) - func setupMouseButton(left: Bool, right: Bool, _ dontIgnore: @escaping(Bool) -> Bool) + func setupMouseMoved(_ mouseMoved: @escaping(CGFloat, CGFloat) -> Bool) + func setupMouseButton(left: Bool, right: Bool, _ consumed: @escaping(Int, Bool) -> Bool) func setupScrollWheel(_ onMoved: @escaping(CGFloat, CGFloat) -> Bool) func urlForApplicationWithBundleIdentifier(_ value: String) -> URL? func setMenuBarVisible(_ value: Bool)