Add accessibility request button

This commit is contained in:
Hariharan Mudaliar 2025-12-01 22:30:40 +05:30
commit 05ce48968d
3 changed files with 194 additions and 4 deletions

View file

@ -1803,6 +1803,13 @@
}
}
},
"Accessibility" : {
},
"Accessibility permission required" : {
"comment" : "A label explaining that accessibility permissions are required.",
"isCommentAutoGenerated" : true
},
"Actions" : {
"localizations" : {
"fr" : {
@ -9526,6 +9533,12 @@
}
}
}
},
"Grant Accessibility permission in System Settings to control macOS HUD replacements from here." : {
},
"Grant Accessibility permission in System Settings to customize the on-screen display replacements here." : {
},
"Graph Visibility" : {
"localizations" : {
@ -14756,6 +14769,10 @@
},
"Open Model Settings" : {
},
"Open Settings" : {
"comment" : "A button that opens system settings when pressed.",
"isCommentAutoGenerated" : true
},
"Open System Settings" : {

View file

@ -1282,16 +1282,33 @@ struct HUD: View {
@Default(.enableKeyboardBacklightHUD) var enableKeyboardBacklightHUD
@Default(.systemHUDSensitivity) var systemHUDSensitivity
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
@ObservedObject private var accessibilityPermission = AccessibilityPermissionStore.shared
private func highlightID(_ title: String) -> String {
SettingsTab.hud.highlightID(for: title)
}
private var hasAccessibilityPermission: Bool {
accessibilityPermission.isAuthorized
}
var body: some View {
Form {
if !hasAccessibilityPermission {
Section {
SettingsPermissionCallout(
message: "Accessibility permission lets Dynamic Island replace the native volume, brightness, and keyboard HUDs.",
requestAction: { accessibilityPermission.requestAuthorizationPrompt() },
openSettingsAction: { accessibilityPermission.openSystemSettings() }
)
} header: {
Text("Accessibility")
}
}
Section {
Toggle("Enable HUDs", isOn: $enableSystemHUD)
.disabled(Defaults[.enableCustomOSD])
.disabled(Defaults[.enableCustomOSD] || !hasAccessibilityPermission)
} header: {
Text("General")
} footer: {
@ -1299,6 +1316,10 @@ struct HUD: View {
Text("HUDs are disabled because Custom OSD is enabled. Disable Custom OSD in the Custom OSD tab to use HUDs.")
.foregroundStyle(.secondary)
.font(.caption)
} else if !hasAccessibilityPermission {
Text("Grant Accessibility permission in System Settings to control macOS HUD replacements from here.")
.foregroundStyle(.secondary)
.font(.caption)
} else {
Text("Replaces macOS system HUD with Dynamic Island displays for volume, brightness, and keyboard backlight.")
.foregroundStyle(.secondary)
@ -1306,7 +1327,7 @@ struct HUD: View {
}
}
if enableSystemHUD && !Defaults[.enableCustomOSD] {
if enableSystemHUD && !Defaults[.enableCustomOSD] && hasAccessibilityPermission {
Section {
Toggle("Volume HUD", isOn: $enableVolumeHUD)
Toggle("Brightness HUD", isOn: $enableBrightnessHUD)
@ -1407,6 +1428,14 @@ struct HUD: View {
}
}
.navigationTitle("HUDs")
.onAppear {
accessibilityPermission.refreshStatus()
}
.onChange(of: accessibilityPermission.isAuthorized) { _, granted in
if !granted {
enableSystemHUD = false
}
}
}
}
@ -4418,6 +4447,7 @@ struct CustomOSDSettings: View {
@Default(.osdMaterial) var osdMaterial
@Default(.osdIconColorStyle) var osdIconColorStyle
@Default(.enableSystemHUD) var enableSystemHUD
@ObservedObject private var accessibilityPermission = AccessibilityPermissionStore.shared
@State private var showAlphaWarning = false
@State private var previewValue: CGFloat = 0.65
@ -4426,13 +4456,33 @@ struct CustomOSDSettings: View {
private func highlightID(_ title: String) -> String {
SettingsTab.osd.highlightID(for: title)
}
private var hasAccessibilityPermission: Bool {
accessibilityPermission.isAuthorized
}
var body: some View {
Form {
if !hasAccessibilityPermission {
Section {
SettingsPermissionCallout(
message: "Accessibility permission is needed to intercept system controls for the Custom OSD.",
requestAction: { accessibilityPermission.requestAuthorizationPrompt() },
openSettingsAction: { accessibilityPermission.openSystemSettings() }
)
} header: {
Text("Accessibility")
}
}
Section {
Toggle("Enable Custom OSD", isOn: Binding(
get: { enableCustomOSD },
set: { newValue in
guard hasAccessibilityPermission else {
enableCustomOSD = false
return
}
if newValue && !hasSeenOSDAlphaWarning {
showAlphaWarning = true
} else {
@ -4445,7 +4495,7 @@ struct CustomOSDSettings: View {
}
))
.settingsHighlight(id: highlightID("Enable Custom OSD"))
.disabled(enableSystemHUD)
.disabled(enableSystemHUD || !hasAccessibilityPermission)
} header: {
Text("General")
} footer: {
@ -4453,6 +4503,10 @@ struct CustomOSDSettings: View {
Text("Custom OSD is disabled because HUDs are enabled. Disable HUDs in the HUDs tab to use Custom OSD.")
.foregroundStyle(.secondary)
.font(.caption)
} else if !hasAccessibilityPermission {
Text("Grant Accessibility permission in System Settings to customize the on-screen display replacements here.")
.foregroundStyle(.secondary)
.font(.caption)
} else {
Text("Display custom macOS-style OSD windows at the bottom center of the screen when adjusting volume, brightness, or keyboard backlight.")
.foregroundStyle(.secondary)
@ -4460,7 +4514,7 @@ struct CustomOSDSettings: View {
}
}
if enableCustomOSD {
if enableCustomOSD && hasAccessibilityPermission {
Section {
Toggle("Volume OSD", isOn: $enableOSDVolume)
.settingsHighlight(id: highlightID("Volume OSD"))
@ -4572,6 +4626,49 @@ struct CustomOSDSettings: View {
Text("Custom OSD is an experimental alpha feature and may contain bugs or unexpected behavior.\n\nThis feature requires macOS 15 or later and is still under active development. It's recommended to keep it disabled unless you want to help test it.")
}
.navigationTitle("Custom OSD")
.onAppear {
accessibilityPermission.refreshStatus()
}
.onChange(of: accessibilityPermission.isAuthorized) { _, granted in
if !granted {
enableCustomOSD = false
}
}
}
}
struct SettingsPermissionCallout: View {
let message: String
let requestAction: () -> Void
let openSettingsAction: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Label("Accessibility permission required", systemImage: "exclamationmark.triangle.fill")
.font(.subheadline.weight(.semibold))
.foregroundStyle(.orange)
Text(message)
.font(.caption)
.foregroundStyle(.secondary)
HStack(spacing: 8) {
Button("Request Access") {
requestAction()
}
.buttonStyle(.borderedProminent)
Button("Open Settings") {
openSettingsAction()
}
.buttonStyle(.bordered)
}
.controlSize(.small)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
.background(Color.secondary.opacity(0.08))
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
}
}

View file

@ -0,0 +1,76 @@
import Foundation
import Combine
import AppKit
#if canImport(ApplicationServices)
import ApplicationServices
#endif
/// Tracks accessibility permission status and exposes helpers to request access.
@MainActor
final class AccessibilityPermissionStore: ObservableObject {
static let shared = AccessibilityPermissionStore()
@Published private(set) var isAuthorized: Bool = AccessibilityPermissionStore.isAccessibilityAuthorized()
private var pollingTask: Task<Void, Never>?
private init() {}
deinit {
pollingTask?.cancel()
}
func refreshStatus() {
updateAuthorizationStatus(to: Self.isAccessibilityAuthorized())
}
func requestAuthorizationPrompt() {
#if canImport(ApplicationServices)
let promptKey = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String
let options: CFDictionary = [promptKey: true] as CFDictionary
_ = AXIsProcessTrustedWithOptions(options)
#endif
beginPollingForStatusChanges()
}
func openSystemSettings() {
#if os(macOS)
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
NSWorkspace.shared.open(url)
}
#endif
}
private func beginPollingForStatusChanges() {
pollingTask?.cancel()
pollingTask = Task { [weak self] in
for _ in 0..<30 {
try? await Task.sleep(nanoseconds: 500_000_000)
let status = Self.isAccessibilityAuthorized()
await MainActor.run {
guard let self else { return }
self.updateAuthorizationStatus(to: status)
}
if status {
break
}
}
}
}
private func updateAuthorizationStatus(to newValue: Bool) {
guard newValue != isAuthorized else { return }
isAuthorized = newValue
if !newValue {
beginPollingForStatusChanges()
}
}
private static func isAccessibilityAuthorized() -> Bool {
#if canImport(ApplicationServices)
return AXIsProcessTrusted()
#else
return true
#endif
}
}