From eb939a8f9e8bf8c1322867d39c87e61c59de79e5 Mon Sep 17 00:00:00 2001 From: viatearz Date: Sat, 11 Oct 2025 19:56:58 +0800 Subject: [PATCH 1/3] feat: resizable window --- AKPlugin.swift | 23 +++++++++++++++++ PlayTools/Controls/Frontend/ControlMode.swift | 25 +++++++++++++++++++ .../TouchscreenMouseEventAdapter.swift | 8 ++++++ .../Controls/PTFakeTouch/NSObject+Swizzle.m | 16 +++++++++++- PlayTools/PlayCover.swift | 1 + PlayTools/PlayScreen.swift | 25 +++++++++++++++++++ PlayTools/PlaySettings.swift | 5 ++++ 7 files changed, 102 insertions(+), 1 deletion(-) diff --git a/AKPlugin.swift b/AKPlugin.swift index 296fab66..b0ebfece 100644 --- a/AKPlugin.swift +++ b/AKPlugin.swift @@ -13,6 +13,9 @@ import Foundation private struct AKAppSettingsData: Codable { var hideTitleBar: Bool? var floatingWindow: Bool? + var resolution: Int? + var resizableAspectRatioWidth: Int? + var resizableAspectRatioHeight: Int? } class AKPlugin: NSObject, Plugin { @@ -35,6 +38,11 @@ class AKPlugin: NSObject, Plugin { if self.floatingWindowSetting == true { window.level = .floating } + + if let aspectRatio = self.aspectRatioSetting { + window.contentAspectRatio = aspectRatio + } + NSWindow.allowsAutomaticWindowTabbing = true } @@ -57,6 +65,10 @@ class AKPlugin: NSObject, Plugin { if self.floatingWindowSetting == true { win.level = .floating } + + if let aspectRatio = self.aspectRatioSetting { + win.contentAspectRatio = aspectRatio + } } } @@ -278,6 +290,17 @@ class AKPlugin: NSObject, Plugin { /// Convenience instance property that exposes the cached static preference. private var hideTitleBarSetting: Bool { Self.akAppSettingsData?.hideTitleBar ?? false } private var floatingWindowSetting: Bool { Self.akAppSettingsData?.floatingWindow ?? false } + private var aspectRatioSetting: NSSize? { + guard Self.akAppSettingsData?.resolution == 6 else { + return nil + } + let width = Self.akAppSettingsData?.resizableAspectRatioWidth ?? 0 + let height = Self.akAppSettingsData?.resizableAspectRatioHeight ?? 0 + guard width > 0 && height > 0 else { + return nil + } + return NSSize(width: width, height: height) + } fileprivate static var akAppSettingsData: AKAppSettingsData? = { let bundleIdentifier = Bundle.main.bundleIdentifier ?? "" diff --git a/PlayTools/Controls/Frontend/ControlMode.swift b/PlayTools/Controls/Frontend/ControlMode.swift index dd4ab098..f2f69a2a 100644 --- a/PlayTools/Controls/Frontend/ControlMode.swift +++ b/PlayTools/Controls/Frontend/ControlMode.swift @@ -25,11 +25,14 @@ public class ControlMode: Equatable { private var keyboardAdapter: KeyboardEventAdapter! private var mouseAdapter: MouseEventAdapter! private var controllerAdapter: ControllerEventAdapter! + private var keyWindowObserver: NSObjectProtocol? public func cursorHidden() -> Bool { return mouseAdapter?.cursorHidden() ?? false } + // FIXME: Refactor this function to reduce its body length + // swiftlint:disable function_body_length public func initialize() { let centre = NotificationCenter.default let main = OperationQueue.main @@ -87,8 +90,30 @@ public class ControlMode: Equatable { self.mouseAdapter.handleOtherButton(id: id, pressed: pressed) }) + if PlaySettings.shared.resizableWindow { + initializeResizableWindowSupport() + } + ActionDispatcher.build() } + // swiftlint:enable function_body_length + + private func initializeResizableWindowSupport() { + // Reactivate keymapping once the key window is initialized + keyWindowObserver = NotificationCenter.default.addObserver(forName: UIWindow.didBecomeKeyNotification, + object: nil, queue: .main) { _ in + ActionDispatcher.build() + if let observer = self.keyWindowObserver { + NotificationCenter.default.removeObserver(observer) + self.keyWindowObserver = nil + } + } + // Reactivate keymapping once the user finishes resizing the window + NotificationCenter.default.addObserver(forName: Notification.Name("NSWindowDidEndLiveResizeNotification"), + object: nil, queue: .main) { _ in + ActionDispatcher.build() + } + } private func setupMouseMoved(maxPollingRate: Int) { let minMoveInterval = diff --git a/PlayTools/Controls/Frontend/EventAdapter/Mouse/Instances/TouchscreenMouseEventAdapter.swift b/PlayTools/Controls/Frontend/EventAdapter/Mouse/Instances/TouchscreenMouseEventAdapter.swift index 66e0e2b3..65941fb7 100644 --- a/PlayTools/Controls/Frontend/EventAdapter/Mouse/Instances/TouchscreenMouseEventAdapter.swift +++ b/PlayTools/Controls/Frontend/EventAdapter/Mouse/Instances/TouchscreenMouseEventAdapter.swift @@ -18,6 +18,14 @@ public class TouchscreenMouseEventAdapter: MouseEventAdapter { if rect.width < 1 || rect.height < 1 { return nil } + if screen.resizable && !screen.fullscreen { + // Allow user to resize window by dragging edges + let margin = CGFloat(10) + if point.x < margin || point.x > rect.width - margin || + point.y < margin || point.y > rect.height - margin { + return nil + } + } let viewRect: CGRect = screen.screenRect let widthRate = viewRect.width / rect.width var rate = viewRect.height / rect.height diff --git a/PlayTools/Controls/PTFakeTouch/NSObject+Swizzle.m b/PlayTools/Controls/PTFakeTouch/NSObject+Swizzle.m index aace284a..6796b2b7 100644 --- a/PlayTools/Controls/PTFakeTouch/NSObject+Swizzle.m +++ b/PlayTools/Controls/PTFakeTouch/NSObject+Swizzle.m @@ -149,6 +149,14 @@ - (double) get_default_width { } +- (CGRect) hook_boundsResizable { + return [PlayScreen boundsResizable:[self hook_boundsResizable]]; +} + +- (BOOL) hook_requiresFullScreen { + return NO; +} + - (void) hook_setCurrentSubscription:(VSSubscription *)currentSubscription { // do nothing } @@ -240,7 +248,13 @@ @implementation PTSwizzleLoader + (void)load { // This might need refactor soon if(@available(iOS 16.3, *)) { - if ([[PlaySettings shared] adaptiveDisplay]) { + if ([[PlaySettings shared] resizableWindow]) { + [objc_getClass("_UIApplicationInfoParser") swizzleInstanceMethod:NSSelectorFromString(@"requiresFullScreen") withMethod:@selector(hook_requiresFullScreen)]; + [objc_getClass("UIScreen") swizzleInstanceMethod:@selector(bounds) withMethod:@selector(hook_boundsResizable)]; + [objc_getClass("UIScreen") swizzleInstanceMethod:@selector(nativeScale) withMethod:@selector(hook_nativeScale)]; + [objc_getClass("UIScreen") swizzleInstanceMethod:@selector(scale) withMethod:@selector(hook_scale)]; + } + else if ([[PlaySettings shared] adaptiveDisplay]) { // This is an experimental fix if ([[PlaySettings shared] inverseScreenValues]) { // This lines set External Scene settings and other IOS10 Runtime services by swizzling diff --git a/PlayTools/PlayCover.swift b/PlayTools/PlayCover.swift index a45a24ab..c9e65df6 100644 --- a/PlayTools/PlayCover.swift +++ b/PlayTools/PlayCover.swift @@ -14,6 +14,7 @@ public class PlayCover: NSObject { @objc static public func launch() { quitWhenClose() AKInterface.initialize() + PlayScreen.shared.initialize() PlayInput.shared.initialize() DiscordIPC.shared.initialize() diff --git a/PlayTools/PlayScreen.swift b/PlayTools/PlayScreen.swift index d443a57f..2e3aa019 100644 --- a/PlayTools/PlayScreen.swift +++ b/PlayTools/PlayScreen.swift @@ -85,6 +85,20 @@ extension UIScreen { public class PlayScreen: NSObject { @objc public static let shared = PlayScreen() + func initialize() { + if resizable { + // Remove default size restrictions + NotificationCenter.default.addObserver(forName: UIWindow.didBecomeKeyNotification, object: nil, + queue: .main) { notification in + if let window = notification.object as? UIWindow, + let windowScene = window.windowScene { + windowScene.sizeRestrictions?.minimumSize = CGSize(width: 0, height: 0) + windowScene.sizeRestrictions?.maximumSize = CGSize(width: .max, height: .max) + } + } + } + } + @objc public static func frame(_ rect: CGRect) -> CGRect { return rect.toAspectRatioReversed() } @@ -113,6 +127,10 @@ public class PlayScreen: NSObject { return AKInterface.shared!.isFullscreen } + var resizable: Bool { + return PlaySettings.shared.resizableWindow + } + @objc public var screenRect: CGRect { return UIScreen.main.bounds } @@ -183,6 +201,13 @@ public class PlayScreen: NSObject { return rect.toAspectRatioDefault() } + private static weak var cachedWindow: UIWindow? + @objc public static func boundsResizable(_ rect: CGRect) -> CGRect { + if cachedWindow == nil { + cachedWindow = PlayScreen.shared.keyWindow + } + return cachedWindow?.bounds ?? rect + } } extension CGFloat { diff --git a/PlayTools/PlaySettings.swift b/PlayTools/PlaySettings.swift index d4d80ba4..bd570b91 100644 --- a/PlayTools/PlaySettings.swift +++ b/PlayTools/PlaySettings.swift @@ -41,6 +41,8 @@ let settings = PlaySettings.shared @objc lazy var adaptiveDisplay = settingsData.resolution == 0 ? false : true + @objc lazy var resizableWindow = settingsData.resolution == 6 ? true : false + @objc lazy var deviceModel = settingsData.iosDeviceModel as NSString @objc lazy var oemID: NSString = { @@ -120,4 +122,7 @@ struct AppSettingsData: Codable { var checkMicPermissionSync = false var limitMotionUpdateFrequency = false var disableBuiltinMouse = false + var resizableAspectRatioType = 0 + var resizableAspectRatioWidth = 0 + var resizableAspectRatioHeight = 0 } From 7eccf130b952e61b47fd06887f8e2f4c60df0a29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Moreno?= <47700212+JoseMoreville@users.noreply.github.com> Date: Mon, 13 Oct 2025 13:17:15 -0700 Subject: [PATCH 2/3] Fix: Swiftlint complain on control mode --- PlayTools/Controls/Frontend/ControlMode.swift | 110 +++++++++++------- 1 file changed, 67 insertions(+), 43 deletions(-) diff --git a/PlayTools/Controls/Frontend/ControlMode.swift b/PlayTools/Controls/Frontend/ControlMode.swift index f2f69a2a..0fc43afb 100644 --- a/PlayTools/Controls/Frontend/ControlMode.swift +++ b/PlayTools/Controls/Frontend/ControlMode.swift @@ -31,72 +31,95 @@ public class ControlMode: Equatable { return mouseAdapter?.cursorHidden() ?? false } - // FIXME: Refactor this function to reduce its body length - // swiftlint:disable function_body_length 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") - } + setupTextInputObservers() set(.arbitraryClick) } else { set(.off) } + setupGameController() + setupKeyboard() + if PlaySettings.shared.enableScrollWheel { + setupScrollWheel() + } + + // Mouse polling rate as high as 1000 causes issue to some games + setupMouseMoved(maxPollingRate: 125) + setupMouseButtons() + + if PlaySettings.shared.resizableWindow { + initializeResizableWindowSupport() + } + + ActionDispatcher.build() + } + + private func setupTextInputObservers() { + let centre = NotificationCenter.default + let main = OperationQueue.main + 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") + } + } + + private func setupGameController() { + let centre = NotificationCenter.default + let main = OperationQueue.main centre.addObserver(forName: NSNotification.Name.GCControllerDidConnect, object: nil, queue: main) { _ in - GCController.current?.extendedGamepad?.valueChangedHandler = {profile, element in + GCController.current?.extendedGamepad?.valueChangedHandler = { profile, element in self.controllerAdapter.handleValueChanged(profile, element) } } + } - AKInterface.shared!.setupKeyboard(keyboard: { keycode, pressed, isRepeat, ctrlModified in - self.keyboardAdapter.handleKey(keycode: keycode, pressed: pressed, - isRepeat: isRepeat, ctrlModified: ctrlModified)}, - swapMode: ModeAutomaton.onOption) - - if PlaySettings.shared.enableScrollWheel { - AKInterface.shared!.setupScrollWheel({deltaX, deltaY in - self.mouseAdapter.handleScrollWheel(deltaX: deltaX, deltaY: deltaY) - }) - } + private func setupKeyboard() { + AKInterface.shared!.setupKeyboard( + keyboard: { keycode, pressed, isRepeat, ctrlModified in + self.keyboardAdapter.handleKey( + keycode: keycode, + pressed: pressed, + isRepeat: isRepeat, + ctrlModified: ctrlModified + ) + }, + swapMode: ModeAutomaton.onOption + ) + } - // Mouse polling rate as high as 1000 causes issue to some games - setupMouseMoved(maxPollingRate: 125) + private func setupScrollWheel() { + AKInterface.shared!.setupScrollWheel({ deltaX, deltaY in + self.mouseAdapter.handleScrollWheel(deltaX: deltaX, deltaY: deltaY) + }) + } - AKInterface.shared!.setupMouseButton(left: true, right: false, {_, pressed in + private func setupMouseButtons() { + AKInterface.shared!.setupMouseButton(left: true, right: false, { _, pressed in self.mouseAdapter.handleLeftButton(pressed: pressed) }) - AKInterface.shared!.setupMouseButton(left: false, right: false, {id, pressed in + 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 + AKInterface.shared!.setupMouseButton(left: false, right: true, { id, pressed in self.mouseAdapter.handleOtherButton(id: id, pressed: pressed) }) - - if PlaySettings.shared.resizableWindow { - initializeResizableWindowSupport() - } - - ActionDispatcher.build() } - // swiftlint:enable function_body_length private func initializeResizableWindowSupport() { // Reactivate keymapping once the key window is initialized @@ -207,3 +230,4 @@ extension NSNotification.Name { public static let playtoolsCursorWillShow: NSNotification.Name = NSNotification.Name("playtools.cursorWillShow") } + From 677d637ebdff6480657ad5521f3f42fbbd064b48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Moreno?= <47700212+JoseMoreville@users.noreply.github.com> Date: Tue, 14 Oct 2025 01:23:54 +0100 Subject: [PATCH 3/3] Remove unnecessary blank line in ControlMode.swift --- PlayTools/Controls/Frontend/ControlMode.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/PlayTools/Controls/Frontend/ControlMode.swift b/PlayTools/Controls/Frontend/ControlMode.swift index 0fc43afb..1913d842 100644 --- a/PlayTools/Controls/Frontend/ControlMode.swift +++ b/PlayTools/Controls/Frontend/ControlMode.swift @@ -230,4 +230,3 @@ extension NSNotification.Name { public static let playtoolsCursorWillShow: NSNotification.Name = NSNotification.Name("playtools.cursorWillShow") } -