diff --git a/Cores/DeltaCore b/Cores/DeltaCore index c9ac6b5..4b5084c 160000 --- a/Cores/DeltaCore +++ b/Cores/DeltaCore @@ -1 +1 @@ -Subproject commit c9ac6b5f4f50e9ae4b5fa7f8b420f240003df127 +Subproject commit 4b5084c75149dc0fc7b4abb4e6ab4f41df025ecd diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index 286b92e..4f55f29 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -183,6 +183,7 @@ D54F710429E89DFC009C069A /* NotificationName+Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54F710329E89DFC009C069A /* NotificationName+Settings.swift */; }; D55C468F29E761C000EA6DE9 /* AnyFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55C468E29E761C000EA6DE9 /* AnyFeature.swift */; }; D55C469129E7631000EA6DE9 /* AnyOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55C469029E7631000EA6DE9 /* AnyOption.swift */; }; + D560BD8629EDC45600289847 /* ExternalDisplaySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D560BD8529EDC45600289847 /* ExternalDisplaySceneDelegate.swift */; }; D57D795629F300E100BB2CF8 /* CustomTintColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A817AF29DF4E6E00904AFE /* CustomTintColor.swift */; }; D57D795F29F315F700BB2CF8 /* FeatureDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54A4BB229E4D27E004C7D57 /* FeatureDetailView.swift */; }; D57D796029F315F700BB2CF8 /* ExperimentalFeaturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9C00229DDED6D00A8D610 /* ExperimentalFeaturesView.swift */; }; @@ -437,6 +438,7 @@ D54F710329E89DFC009C069A /* NotificationName+Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationName+Settings.swift"; sourceTree = ""; }; D55C468E29E761C000EA6DE9 /* AnyFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyFeature.swift; sourceTree = ""; }; D55C469029E7631000EA6DE9 /* AnyOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyOption.swift; sourceTree = ""; }; + D560BD8529EDC45600289847 /* ExternalDisplaySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalDisplaySceneDelegate.swift; sourceTree = ""; }; D586496F297734280081477E /* CheatMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatMetadata.swift; sourceTree = ""; }; D586497129774ABD0081477E /* CheatBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatBase.swift; sourceTree = ""; }; D5864977297756CE0081477E /* CheatBaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatBaseView.swift; sourceTree = ""; }; @@ -950,8 +952,8 @@ isa = PBXGroup; children = ( BFFA71DC1AAC406100EE9DD1 /* AppDelegate.swift */, - D5A98CE1284EF14B00E023E5 /* SceneDelegate.swift */, BFFA71E01AAC406100EE9DD1 /* Main.storyboard */, + D502F82129EF05F800ACCDA1 /* Scenes */, BFFC46211D59848000AF2CC6 /* Launch */, BF46894D1AAC469800A2586D /* Game Selection */, BFFB709D1AF99ACA00DE56FE /* Emulation */, @@ -1012,6 +1014,15 @@ path = Launch; sourceTree = ""; }; + D502F82129EF05F800ACCDA1 /* Scenes */ = { + isa = PBXGroup; + children = ( + D5A98CE1284EF14B00E023E5 /* SceneDelegate.swift */, + D560BD8529EDC45600289847 /* ExternalDisplaySceneDelegate.swift */, + ); + path = Scenes; + sourceTree = ""; + }; D517F6BB29E737F5000D14D0 /* Types */ = { isa = PBXGroup; children = ( @@ -1505,6 +1516,7 @@ BF6BF3131EB7E47F008E83CD /* ImportOption.swift in Sources */, BFEE943D23F2180200CDA07D /* Delta4ToDelta5.xcmappingmodel in Sources */, BF31878B1D489AAA00BD020D /* CheatValidator.swift in Sources */, + D560BD8629EDC45600289847 /* ExternalDisplaySceneDelegate.swift in Sources */, BFFC46201D59823500AF2CC6 /* InitialGamesStoryboardSegue.swift in Sources */, BF15AF841F54B43B009B6AAB /* ActionInput.swift in Sources */, BF4828841F9027B600028B97 /* Delta.xcdatamodeld in Sources */, diff --git a/Delta/AppDelegate.swift b/Delta/AppDelegate.swift index c4f7169..bcd7e0a 100644 --- a/Delta/AppDelegate.swift +++ b/Delta/AppDelegate.swift @@ -121,7 +121,17 @@ extension AppDelegate { // Called when a new scene session is being created. // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Main", sessionRole: connectingSceneSession.role) + + if connectingSceneSession.role == .windowExternalDisplay + { + // External Display + return UISceneConfiguration(name: "External Display", sessionRole: connectingSceneSession.role) + } + else + { + // Default Scene + return UISceneConfiguration(name: "Main", sessionRole: connectingSceneSession.role) + } } func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) diff --git a/Delta/Emulation/GameViewController.swift b/Delta/Emulation/GameViewController.swift index 2d417e4..714eb97 100644 --- a/Delta/Emulation/GameViewController.swift +++ b/Delta/Emulation/GameViewController.swift @@ -212,6 +212,9 @@ class GameViewController: DeltaCore.GameViewController NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.emulationDidQuit(with:)), name: EmulatorCore.emulationDidQuitNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.didEnableJIT(with:)), name: ServerManager.didEnableJITNotification, object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.sceneWillConnect(with:)), name: UIScene.willConnectNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.sceneDidDisconnect(with:)), name: UIScene.didDisconnectNotification, object: nil) } deinit @@ -679,8 +682,45 @@ private extension GameViewController self.controllerView.controllerSkin = touchControllerSkin } + self.updateExternalDisplay() + self.view.setNeedsLayout() } + + func updateGameViews() + { + if UIApplication.shared.isExternalDisplayConnected + { + // AirPlaying, hide all (non-touch) screens. + + if let traits = self.controllerView.controllerSkinTraits, let screens = self.controllerView.controllerSkin?.screens(for: traits) + { + for (screen, gameView) in zip(screens, self.gameViews) + { + gameView.isEnabled = screen.isTouchScreen + gameView.isHidden = !screen.isTouchScreen + } + } + else + { + // Either self.controllerView.controllerSkin is `nil`, or it doesn't support these traits. + // Most likely this system only has 1 screen, so just hide self.gameView. + + self.gameView.isEnabled = false + self.gameView.isHidden = true + } + } + else + { + // Not AirPlaying, show all screens. + + for gameView in self.gameViews + { + gameView.isEnabled = true + gameView.isHidden = false + } + } + } } //MARK: - Game Saves - @@ -1072,12 +1112,60 @@ extension GameViewController } } +private extension GameViewController +{ + func connectExternalDisplay(for scene: ExternalDisplayScene) + { + // We need to receive gameViewController(_:didUpdateGameViews) callback. + scene.gameViewController.delegate = self + + self.updateGameViews() + self.updateExternalDisplay() + } + + func updateExternalDisplay() + { + guard let scene = UIApplication.shared.externalDisplayScene else { return } + + if scene.game?.fileURL != self.game?.fileURL + { + scene.game = self.game + } + + self.updateExternalDisplayGameViews() + } + + func updateExternalDisplayGameViews() + { + guard let scene = UIApplication.shared.externalDisplayScene, let emulatorCore = self.emulatorCore else { return } + + for gameView in scene.gameViewController.gameViews + { + emulatorCore.add(gameView) + } + } + + func disconnectExternalDisplay(for scene: ExternalDisplayScene) + { + scene.gameViewController.delegate = nil + + for gameView in scene.gameViewController.gameViews + { + self.emulatorCore?.remove(gameView) + } + + self.updateGameViews() + } +} + //MARK: - GameViewControllerDelegate - /// GameViewControllerDelegate extension GameViewController: GameViewControllerDelegate { func gameViewController(_ gameViewController: DeltaCore.GameViewController, handleMenuInputFrom gameController: GameController) { + guard gameViewController == self else { return } + if let pausingGameController = self.pausingGameController { guard pausingGameController == gameController else { return } @@ -1103,6 +1191,8 @@ extension GameViewController: GameViewControllerDelegate func gameViewControllerShouldResumeEmulation(_ gameViewController: DeltaCore.GameViewController) -> Bool { + guard gameViewController == self else { return false } + var result = false rst_dispatch_sync_on_main_thread { @@ -1111,6 +1201,20 @@ extension GameViewController: GameViewControllerDelegate return result } + + func gameViewController(_ gameViewController: DeltaCore.GameViewController, didUpdateGameViews gameViews: [GameView]) + { + // gameViewController could be `self` or ExternalDisplayScene.gameViewController. + + if gameViewController == self + { + self.updateGameViews() + } + else + { + self.updateExternalDisplayGameViews() + } + } } private extension GameViewController @@ -1364,6 +1468,18 @@ private extension GameViewController } } } + + @objc func sceneWillConnect(with notification: Notification) + { + guard let scene = notification.object as? ExternalDisplayScene else { return } + self.connectExternalDisplay(for: scene) + } + + @objc func sceneDidDisconnect(with notification: Notification) + { + guard let scene = notification.object as? ExternalDisplayScene else { return } + self.disconnectExternalDisplay(for: scene) + } } private extension UserDefaults diff --git a/Delta/Scenes/ExternalDisplaySceneDelegate.swift b/Delta/Scenes/ExternalDisplaySceneDelegate.swift new file mode 100644 index 0000000..6a40337 --- /dev/null +++ b/Delta/Scenes/ExternalDisplaySceneDelegate.swift @@ -0,0 +1,85 @@ +// +// ExternalDisplaySceneDelegate.swift +// Delta +// +// Created by Riley Testut on 4/17/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import UIKit + +import DeltaCore + +extension UIApplication +{ + var isExternalDisplayConnected: Bool { + let scene = UIApplication.shared.connectedScenes.first { $0.session.role == .windowExternalDisplay } + return scene != nil + } + + var externalDisplayScene: ExternalDisplayScene? { + let scene = UIApplication.shared.connectedScenes.compactMap({ $0 as? ExternalDisplayScene }).first(where: { $0.session.role == .windowExternalDisplay }) + return scene + } +} + +class ExternalDisplayScene: UIWindowScene +{ + let gameViewController = DeltaCore.GameViewController() + + var game: GameProtocol? { + get { self.gameViewController.game } + set { self.gameViewController.game = newValue } + } +} + +class ExternalDisplaySceneDelegate: UIResponder, UIWindowSceneDelegate +{ + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) + { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let windowScene = scene as? UIWindowScene, let externalDisplayScene = scene as? ExternalDisplayScene else { return } + + self.window = GameWindow(windowScene: windowScene) + self.window?.tintColor = .deltaPurple + self.window?.rootViewController = externalDisplayScene.gameViewController + self.window?.makeKeyAndVisible() + } + + func sceneDidDisconnect(_ scene: UIScene) + { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) + { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) + { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) + { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) + { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } +} diff --git a/Delta/SceneDelegate.swift b/Delta/Scenes/SceneDelegate.swift similarity index 100% rename from Delta/SceneDelegate.swift rename to Delta/Scenes/SceneDelegate.swift diff --git a/Delta/Supporting Files/Info.plist b/Delta/Supporting Files/Info.plist index add916c..ceaac46 100644 --- a/Delta/Supporting Files/Info.plist +++ b/Delta/Supporting Files/Info.plist @@ -222,6 +222,17 @@ Main + UIWindowSceneSessionRoleExternalDisplay + + + UISceneClassName + $(PRODUCT_MODULE_NAME).ExternalDisplayScene + UISceneConfigurationName + External Display + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).ExternalDisplaySceneDelegate + + UIBackgroundModes