Files
xcodesApp-mirror/Xcodes/XcodesApp.swift

178 lines
7.0 KiB
Swift

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))
}