diff --git a/mac/Config/Config/ConfigApp.swift b/mac/Config/Config/ConfigApp.swift index f77d068f4f..65f7260b83 100644 --- a/mac/Config/Config/ConfigApp.swift +++ b/mac/Config/Config/ConfigApp.swift @@ -32,7 +32,7 @@ struct ConfigApp: App { .environmentObject(installation) } Window("Installation", id: "install") { - ParentInstallView() + MainInstallView() .environmentObject(installation) } .windowResizability(.contentSize) diff --git a/mac/Config/Config/InstallationViews/GrantAccessibiltyPermissionView.swift b/mac/Config/Config/InstallationViews/GrantAccessibiltyPermissionView.swift index f520557a84..cb022fc75e 100644 --- a/mac/Config/Config/InstallationViews/GrantAccessibiltyPermissionView.swift +++ b/mac/Config/Config/InstallationViews/GrantAccessibiltyPermissionView.swift @@ -20,9 +20,19 @@ struct GrantAccessibiltyPermissionView: View { let namespace: Namespace.ID let onContinue: () -> Void + /** + * The flow of this view depends on the following @State variables. + * Once the user presses "Open Settings" in order to toggle the security permission they will be allowed to continue. + * When they press "Continue," a loading symbol will run until the view receives a notification of whether the access is + * granted or not. If not granted, an error message will appear. If granted, the user will be moved to the next screen. + */ + + // Tracks if the user clicked "Open Settings" (Enables the "Continue" button) @State var openSettingsButtonPressed: Bool = false + // Tracks if the app is currently running the background permission check @State var checkingPermission: Bool = false - @State var advancementRequestedAndPermissionNotGranted: Bool = false + // Tracks if the user clicked "Continue" but permission is still missing + @State var permissionNotGrantedAfterPrompt: Bool = false var body: some View { VStack { @@ -58,12 +68,13 @@ struct GrantAccessibiltyPermissionView: View { if checkingPermission { HStack { + // Shows spinner AKA ProgressView() ProgressView() .controlSize(.small) Text("Checking...") } - } else if advancementRequestedAndPermissionNotGranted { + } else if permissionNotGrantedAfterPrompt { Text("Access has not been granted.") .foregroundStyle(Color.red) .padding(7) @@ -89,7 +100,9 @@ struct GrantAccessibiltyPermissionView: View { .tint(.blue) .clipShape(Capsule()) .matchedGeometryEffect(id: "actionButton", in: namespace) + Button { + // Trigger the system task to check for accessibility permission checkingPermission = true installation.executeCurrentInstallationTask() } label: { @@ -101,16 +114,18 @@ struct GrantAccessibiltyPermissionView: View { .clipShape(Capsule()) } } + // Triggered when the system confirms accessibility has been granted. .onReceive( NotificationCenter.default.publisher(for: .accessibilityGranted)) { notification in withAnimation(.smooth) { - advancementRequestedAndPermissionNotGranted = true - onContinue() + permissionNotGrantedAfterPrompt = false + onContinue() // Moves the user to the next screen } } + // Triggered when the system confirms accessibility has not been granted. .onReceive( NotificationCenter.default.publisher(for: .accessibilityNotGranted)) { notification in withAnimation(.smooth) { - checkingPermission = false - advancementRequestedAndPermissionNotGranted = true + checkingPermission = false // Stops showing the loading spinner + permissionNotGrantedAfterPrompt = true // Shows the red error text } } } diff --git a/mac/Config/Config/InstallationViews/InitialRepairView.swift b/mac/Config/Config/InstallationViews/InitialRepairView.swift index 2849aa2d82..3643476a9b 100644 --- a/mac/Config/Config/InstallationViews/InitialRepairView.swift +++ b/mac/Config/Config/InstallationViews/InitialRepairView.swift @@ -13,9 +13,7 @@ import AppKit internal import UniformTypeIdentifiers struct InitialRepairView: View { - @EnvironmentObject var installation: InstallationContainer - let namespace: Namespace.ID let onContinue: () -> Void diff --git a/mac/Config/Config/InstallationViews/InstallPage.swift b/mac/Config/Config/InstallationViews/InstallPage.swift deleted file mode 100644 index 805355dc55..0000000000 --- a/mac/Config/Config/InstallationViews/InstallPage.swift +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Keyman is copyright (C) SIL Global. MIT License. - * - * Created by Eli Schantz on 2026-07-21 - * - * The @State variable currentPage is of type InstallPage. - * These pages are used to easily connect each installation phase with a SwiftUI view in ParentInstallView. - */ - -enum InstallPage: String, CaseIterable { - case loading - case initialInstall - case initialRepair - case completed - case enableInputMethod - case allowSecurityPermission - case rerunInstaller -} diff --git a/mac/Config/Config/InstallationViews/ParentInstallView.swift b/mac/Config/Config/InstallationViews/MainInstallView.swift similarity index 67% rename from mac/Config/Config/InstallationViews/ParentInstallView.swift rename to mac/Config/Config/InstallationViews/MainInstallView.swift index 0bc5ab0e6b..bcb749d914 100644 --- a/mac/Config/Config/InstallationViews/ParentInstallView.swift +++ b/mac/Config/Config/InstallationViews/MainInstallView.swift @@ -8,11 +8,33 @@ import SwiftUI -struct ParentInstallView: View { +enum InstallPage: String, CaseIterable { + case loading + case initialInstall + case initialRepair + case completed + case enableInputMethod + case allowSecurityPermission + case rerunInstaller + case restartMac +} + +struct MainInstallView: View { @EnvironmentObject var installation: InstallationContainer + /** + * A namespace is created here and passed to child views. + * Any subviews with the same string id and this namespace + * will animate smoothly when changing positions or states. + */ @Namespace var animation @State public var currentPage: InstallPage = .loading + /** + * chooseCurrentPage() will update the @State var currentPage according to the current task. + * If there is a task involved with the installationPhase, it will display the page associated with that task. + * If there is not a task involved with the installationPhase, it will display the page associated with that phase. + */ + func chooseCurrentPage() { if installation.installationPhase.hasTasks { switch installation.currentTask()?.taskType { @@ -21,6 +43,7 @@ struct ParentInstallView: View { case .enableInputMethod: currentPage = .enableInputMethod case .requestAccess: currentPage = .allowSecurityPermission case .confirmAccess: currentPage = .allowSecurityPermission + case .requestRestart: currentPage = .restartMac default: currentPage = .completed } } else { @@ -40,7 +63,9 @@ struct ParentInstallView: View { } var body: some View { - ZStack { + + VStack { + // The switch statement below updates the view this VStack contains whenever currentPage changes value switch currentPage { case .loading: ProgressView() case .initialInstall: InitialInstallView(namespace: animation,onContinue: { @@ -55,11 +80,13 @@ struct ParentInstallView: View { case .enableInputMethod: EnableInputMethodView(namespace: animation, onContinue: chooseCurrentPage) case .allowSecurityPermission: GrantAccessibiltyPermissionView(namespace: animation, onContinue: chooseCurrentPage) case .rerunInstaller: RerunInstallerView(namespace: animation) + case .restartMac: RestartComputerView(namespace: animation) } } + // While the installer is evaluating the Keyman installation, the loading screen will be shown .onAppear { - print("LOL ", installation.installationPhase) - print("LOL ", installation.currentTask()?.taskType ?? "no task available") + print("From MainInstallView onAppear: ", installation.installationPhase) + print("From MainInstallView onAppear: ", installation.currentTask()?.taskType ?? "no task available") if installation.installationPhase == .evaluatingInstallation { currentPage = .loading @@ -70,8 +97,6 @@ struct ParentInstallView: View { await MainActor.run { withAnimation(.smooth) { chooseCurrentPage() - print("LOOL ", installation.installationPhase) - } } } diff --git a/mac/Config/Config/InstallationViews/RerunInstallerView.swift b/mac/Config/Config/InstallationViews/RerunInstallerView.swift index 006510a14c..322a9c8ca2 100644 --- a/mac/Config/Config/InstallationViews/RerunInstallerView.swift +++ b/mac/Config/Config/InstallationViews/RerunInstallerView.swift @@ -11,20 +11,6 @@ import SwiftUI import AppKit internal import UniformTypeIdentifiers -private func chooseAndOpenKeymanInstaller() { - let panel = NSOpenPanel() - panel.message = "Open the Keyman .pkg file." - panel.allowedContentTypes = [UTType(filenameExtension: "pkg")!] - panel.directoryURL = FileManager.default.urls( - for: .downloadsDirectory, - in: .userDomainMask - ).first - - if panel.runModal() == .OK, let url = panel.url { - NSWorkspace.shared.open(url) - } -} - struct RerunInstallerView: View { @EnvironmentObject var installation: InstallationContainer let namespace: Namespace.ID @@ -59,17 +45,6 @@ struct RerunInstallerView: View { Text("Run Keyman installer") .font(.title2) .frame(maxWidth: .infinity, alignment: .leading) - Button { - chooseAndOpenKeymanInstaller() - } label: { - Text("Open Installer") - .padding(.horizontal, 16) - .padding(.vertical, 4) - } - .buttonStyle(.borderedProminent) - .tint(.blue) - .clipShape(Capsule()) - .matchedGeometryEffect(id: "actionButton", in: namespace) NavigationButton(action: .dismiss) } }