import AppKit import Sparkle import SwiftUI @main struct XcodesApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate: AppDelegate @SwiftUI.Environment(\.scenePhase) private var scenePhase: ScenePhase @SwiftUI.Environment(\.openURL) var openURL: OpenURLAction @StateObject private var appState = AppState() @StateObject private var updater = ObservableUpdater() var body: some Scene { Window("Xcodes", id: "main") { MainWindow() .environmentObject(appState) .environmentObject(updater) // This is intentionally used on a View, and not on a WindowGroup, // so that it's triggered when an individual window's phase changes instead of all window phases. // When used on a View it's also invoked on launch, which doesn't occur with a WindowGroup. // FB8954581 ScenePhase read from App doesn't return a value on launch .onChange(of: scenePhase) { newScenePhase in guard !isTesting else { return } if case .active = newScenePhase { appState.updateIfNeeded() appState.updateInstalledRuntimes() } } } .commands { CommandGroup(replacing: .appInfo) { Button("Menu.About") { appDelegate.showAboutWindow() } } CommandGroup(after: .appInfo) { Button("Menu.CheckForUpdates") { updater.checkForUpdates() } } CommandGroup(after: CommandGroupPlacement.newItem) { Button("Refresh") { appState.update() } .keyboardShortcut(KeyEquivalent("r")) .disabled(appState.isUpdating) } XcodeCommands(appState: appState) CommandGroup(replacing: CommandGroupPlacement.help) { Button("Menu.GitHubRepo") { let xcodesRepoURL = URL(string: "https://github.com/XcodesOrg/XcodesApp/")! openURL(xcodesRepoURL) } Divider() Button("Menu.ReportABug") { let bugReportURL = URL(string: "https://github.com/XcodesOrg/XcodesApp/issues/new?assignees=&labels=bug&template=bug_report.md&title=")! openURL(bugReportURL) } Button("Menu.RequestNewFeature") { let featureRequestURL = URL(string: "https://github.com/XcodesOrg/XcodesApp/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=")! openURL(featureRequestURL) } } } #if os(macOS) Settings { PreferencesView() .environmentObject(appState) .environmentObject(updater) .alert(item: $appState.presentedPreferenceAlert, content: { presentedAlert in alert(for: presentedAlert) }) } Window("Platforms", id: "platforms") { PlatformsListView() .environmentObject(appState) .alert(item: $appState.presentedPreferenceAlert, content: { presentedAlert in alert(for: presentedAlert) }) } #endif } private func alert(for alertType: XcodesPreferencesAlert) -> Alert { switch alertType { case let .deletePlatform(runtime): return Alert( title: Text(String(format: localizeString("Alert.DeletePlatform.Title"), runtime.name)), primaryButton: .destructive( Text("Alert.DeletePlatform.PrimaryButton"), action: { Task { do { try await self.appState.deleteRuntime(runtime: runtime) } catch { var errorString: String if let error = error as? String { errorString = error } else { errorString = error.localizedDescription } self.appState.presentedPreferenceAlert = .generic(title: "Error", message: errorString) } } } ), secondaryButton: .cancel(Text("Cancel")) ) case let .generic(title, message): return Alert( title: Text(title), message: Text(message), dismissButton: .default( Text("OK"), action: { appState.presentedAlert = nil } ) ) } } } class AppDelegate: NSObject, NSApplicationDelegate { private lazy var aboutWindow = configure(NSWindow( contentRect: .zero, styleMask: [.closable, .resizable, .miniaturizable, .titled], backing: .buffered, defer: false )) { $0.title = localizeString("About") $0.contentView = NSHostingView(rootView: AboutView(showAcknowledgementsWindow: showAcknowledgementsWindow)) $0.isReleasedWhenClosed = false } private let acknowledgementsWindow = configure(NSWindow( contentRect: .zero, styleMask: [.closable, .resizable, .miniaturizable, .titled], backing: .buffered, defer: false )) { $0.title = localizeString("Acknowledgements") $0.contentView = NSHostingView(rootView: AcknowledgmentsView()) $0.isReleasedWhenClosed = false } /// If we wanted to use only SwiftUI API to do this we could make a new WindowGroup and use openURL and handlesExternalEvents. /// WindowGroup lets the user open more than one window right now, which is a little strange for an About window. /// (It's also weird that the main Xcode list window can be opened more than once, there should only be one.) /// To work around this, an AppDelegate holds onto a single instance of an NSWindow that is shown here. /// FB8954588 Scene / WindowGroup is missing API to limit the number of windows that can be created func showAboutWindow() { aboutWindow.center() aboutWindow.makeKeyAndOrderFront(nil) } func showAcknowledgementsWindow() { acknowledgementsWindow.center() acknowledgementsWindow.makeKeyAndOrderFront(nil) } func applicationDidFinishLaunching(_: Notification) {} func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return Current.defaults.bool(forKey: "terminateAfterLastWindowClosed") ?? false } } func localizeString(_ key: String, comment: String = "") -> String { return String(localized: String.LocalizationValue(key)) }