diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index bb20861..df19672 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -8747,6 +8747,9 @@ } } } + }, + "Disabled while BetterDisplay integration is active — BetterDisplay uses Cmd+Brightness keys for its own controls." : { + }, "Discrete transitions snap between Green (0–60%), Yellow (60–85%), and Red (85–100%)." : { "comment" : "A description of how the color-coded progress bars work when not using smooth transitions.", diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 1ee0ec8..7484500 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -1384,8 +1384,7 @@ private struct HUDAndOSDSettingsView: View { @Default(.enableVolumeHUD) var enableVolumeHUD @Default(.enableBrightnessHUD) var enableBrightnessHUD @Default(.enableKeyboardBacklightHUD) var enableKeyboardBacklightHUD - - // Vertical HUD Props + @Default(.enableBetterDisplayIntegration) var enableBetterDisplayIntegration @Default(.verticalHUDShowValue) var verticalHUDShowValue @Default(.verticalHUDInteractive) var verticalHUDInteractive @Default(.verticalHUDHeight) var verticalHUDHeight @@ -1621,7 +1620,7 @@ private struct HUDAndOSDSettingsView: View { } case .vertical: Form { - if !accessibilityPermission.isAuthorized { + if !accessibilityPermission.isAuthorized && !enableBetterDisplayIntegration { Section { SettingsPermissionCallout( message: "Accessibility permission is needed to intercept system controls for the Vertical HUD.", @@ -1637,11 +1636,13 @@ private struct HUDAndOSDSettingsView: View { } } - if accessibilityPermission.isAuthorized { + if accessibilityPermission.isAuthorized || enableBetterDisplayIntegration { Section { Toggle("Volume HUD", isOn: $enableVolumeHUD) Toggle("Brightness HUD", isOn: $enableBrightnessHUD) Toggle("Keyboard Backlight HUD", isOn: $enableKeyboardBacklightHUD) + .disabled(enableBetterDisplayIntegration) + .help(enableBetterDisplayIntegration ? "Disabled while BetterDisplay integration is active — BetterDisplay uses Cmd+Brightness keys for its own controls." : "") } header: { Text("Controls") } footer: { @@ -1736,7 +1737,7 @@ private struct HUDAndOSDSettingsView: View { case .circular: Form { - if !accessibilityPermission.isAuthorized { + if !accessibilityPermission.isAuthorized && !enableBetterDisplayIntegration { Section { SettingsPermissionCallout( message: "Accessibility permission is needed to intercept system controls for the Circular HUD.", @@ -1752,11 +1753,13 @@ private struct HUDAndOSDSettingsView: View { } } - if accessibilityPermission.isAuthorized { + if accessibilityPermission.isAuthorized || enableBetterDisplayIntegration { Section { Toggle("Volume HUD", isOn: $enableVolumeHUD) Toggle("Brightness HUD", isOn: $enableBrightnessHUD) Toggle("Keyboard Backlight HUD", isOn: $enableKeyboardBacklightHUD) + .disabled(enableBetterDisplayIntegration) + .help(enableBetterDisplayIntegration ? "Disabled while BetterDisplay integration is active — BetterDisplay uses Cmd+Brightness keys for its own controls." : "") } header: { Text("Controls") } footer: { @@ -2020,6 +2023,7 @@ struct HUD: View { @Default(.enableVolumeHUD) var enableVolumeHUD @Default(.enableBrightnessHUD) var enableBrightnessHUD @Default(.enableKeyboardBacklightHUD) var enableKeyboardBacklightHUD + @Default(.enableBetterDisplayIntegration) var enableBetterDisplayIntegration @Default(.systemHUDSensitivity) var systemHUDSensitivity @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @ObservedObject private var accessibilityPermission = AccessibilityPermissionStore.shared @@ -2038,7 +2042,7 @@ struct HUD: View { var body: some View { Form { - if !hasAccessibilityPermission { + if !hasAccessibilityPermission && !enableBetterDisplayIntegration { Section { SettingsPermissionCallout( message: "Accessibility permission lets Dynamic Island replace the native volume, brightness, and keyboard HUDs.", @@ -2052,11 +2056,13 @@ struct HUD: View { - if enableSystemHUD && !Defaults[.enableCustomOSD] && hasAccessibilityPermission { + if enableSystemHUD && !Defaults[.enableCustomOSD] && (hasAccessibilityPermission || enableBetterDisplayIntegration) { Section { Toggle("Volume HUD", isOn: $enableVolumeHUD) Toggle("Brightness HUD", isOn: $enableBrightnessHUD) Toggle("Keyboard Backlight HUD", isOn: $enableKeyboardBacklightHUD) + .disabled(enableBetterDisplayIntegration) + .help(enableBetterDisplayIntegration ? "Disabled while BetterDisplay integration is active \u{2014} BetterDisplay uses Cmd+Brightness keys for its own controls." : "") } header: { Text("Controls") } footer: { @@ -6702,6 +6708,7 @@ struct CustomOSDSettings: View { @Default(.enableOSDVolume) var enableOSDVolume @Default(.enableOSDBrightness) var enableOSDBrightness @Default(.enableOSDKeyboardBacklight) var enableOSDKeyboardBacklight + @Default(.enableBetterDisplayIntegration) var enableBetterDisplayIntegration @Default(.osdMaterial) var osdMaterial @Default(.osdLiquidGlassCustomizationMode) var osdLiquidGlassCustomizationMode @Default(.osdLiquidGlassVariant) var osdLiquidGlassVariant @@ -6744,7 +6751,7 @@ struct CustomOSDSettings: View { var body: some View { Form { - if !hasAccessibilityPermission { + if !hasAccessibilityPermission && !enableBetterDisplayIntegration { Section { SettingsPermissionCallout( message: "Accessibility permission is needed to intercept system controls for the Custom OSD.", @@ -6756,7 +6763,7 @@ struct CustomOSDSettings: View { } } - if hasAccessibilityPermission { + if hasAccessibilityPermission || enableBetterDisplayIntegration { Section { Toggle("Volume OSD", isOn: $enableOSDVolume) .settingsHighlight(id: highlightID("Volume OSD")) @@ -6764,6 +6771,8 @@ struct CustomOSDSettings: View { .settingsHighlight(id: highlightID("Brightness OSD")) Toggle("Keyboard Backlight OSD", isOn: $enableOSDKeyboardBacklight) .settingsHighlight(id: highlightID("Keyboard Backlight OSD")) + .disabled(enableBetterDisplayIntegration) + .help(enableBetterDisplayIntegration ? "Disabled while BetterDisplay integration is active \u{2014} BetterDisplay uses Cmd+Brightness keys for its own controls." : "") } header: { Text("Controls") } footer: { diff --git a/DynamicIsland/managers/BetterDisplayManager.swift b/DynamicIsland/managers/BetterDisplayManager.swift index 5d10948..16a4b8a 100644 --- a/DynamicIsland/managers/BetterDisplayManager.swift +++ b/DynamicIsland/managers/BetterDisplayManager.swift @@ -190,6 +190,8 @@ final class BetterDisplayManager: ObservableObject { return } + NSLog("📺 BetterDisplay OSD raw payload: \(notificationString)") + do { let osd = try JSONDecoder().decode( BetterDisplayOSDNotification.self, @@ -208,21 +210,20 @@ final class BetterDisplayManager: ObservableObject { let targetScreen = resolveScreen(for: osd.displayID) let isExternalDisplay = isExternal(displayID: osd.displayID) + NSLog("📺 BetterDisplay OSD: target=\(osd.controlTarget ?? "nil") displayID=\(osd.displayID.map(String.init) ?? "nil") resolvedScreen=\(targetScreen?.localizedName ?? "nil") isExternal=\(isExternalDisplay) value=\(osd.value ?? -1)") + switch category { case .brightness: - guard Defaults[.enableBrightnessHUD] else { return } let icon = isExternalDisplay ? "display" : nil dispatchBrightnessHUD(value: normalizedValue, customSymbol: icon, onScreen: targetScreen) case .volume: - guard Defaults[.enableVolumeHUD] else { return } let isMuted = osd.controlTarget == "mute" || osd.systemIconID == 4 dispatchVolumeHUD(value: normalizedValue, isMuted: isMuted, onScreen: targetScreen) case .other: // For unsupported control targets (contrast, gamma, temperature, etc.), // show a generic brightness-style HUD if the user has brightness HUD enabled - guard Defaults[.enableBrightnessHUD] else { return } let icon = isExternalDisplay ? "display" : osd.customSymbol dispatchBrightnessHUD(value: normalizedValue, customSymbol: icon, onScreen: targetScreen) } @@ -281,14 +282,26 @@ final class BetterDisplayManager: ObservableObject { /// Resolve a BetterDisplay `displayID` (CGDirectDisplayID) to the matching NSScreen, if any. private func resolveScreen(for displayID: Int?) -> NSScreen? { - guard let displayID else { return nil } + guard let displayID else { + NSLog("📺 BetterDisplay resolveScreen: displayID is nil, falling back to all screens") + return nil + } let target = UInt32(displayID) - return NSScreen.screens.first { screen in + let availableScreens = NSScreen.screens.map { screen -> (String, UInt32) in + let num = (screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber)?.uint32Value ?? 0 + return (screen.localizedName, num) + } + NSLog("📺 BetterDisplay resolveScreen: looking for displayID=\(displayID) (UInt32=\(target)) among screens: \(availableScreens)") + let matched = NSScreen.screens.first { screen in guard let screenNumber = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber else { return false } return screenNumber.uint32Value == target } + if matched == nil { + NSLog("📺 BetterDisplay resolveScreen: no match found for displayID=\(displayID), HUD will show on all screens") + } + return matched } /// Whether the given displayID refers to an external (non-built-in) display. diff --git a/DynamicIsland/managers/SystemHUDManager.swift b/DynamicIsland/managers/SystemHUDManager.swift index 77cdead..efc7893 100644 --- a/DynamicIsland/managers/SystemHUDManager.swift +++ b/DynamicIsland/managers/SystemHUDManager.swift @@ -108,70 +108,76 @@ class SystemHUDManager { }.store(in: &cancellables) // Observe individual HUD toggles - Defaults.publisher(.enableVolumeHUD, options: []).sink { [weak self] change in + Defaults.publisher(.enableVolumeHUD, options: []).sink { [weak self] _ in guard let self = self, self.isSetupComplete, self.requiresSystemToggleHandling else { return } + let flags = self.resolvedControlFlags() self.changesObserver?.update( - volumeEnabled: change.newValue, - brightnessEnabled: Defaults[.enableBrightnessHUD], - keyboardBacklightEnabled: Defaults[.enableKeyboardBacklightHUD] + volumeEnabled: flags.volume, + brightnessEnabled: flags.brightness, + keyboardBacklightEnabled: flags.backlight ) }.store(in: &cancellables) - Defaults.publisher(.enableBrightnessHUD, options: []).sink { [weak self] change in + Defaults.publisher(.enableBrightnessHUD, options: []).sink { [weak self] _ in guard let self = self, self.isSetupComplete, self.requiresSystemToggleHandling else { return } + let flags = self.resolvedControlFlags() self.changesObserver?.update( - volumeEnabled: Defaults[.enableVolumeHUD], - brightnessEnabled: change.newValue, - keyboardBacklightEnabled: Defaults[.enableKeyboardBacklightHUD] + volumeEnabled: flags.volume, + brightnessEnabled: flags.brightness, + keyboardBacklightEnabled: flags.backlight ) }.store(in: &cancellables) - Defaults.publisher(.enableKeyboardBacklightHUD, options: []).sink { [weak self] change in + Defaults.publisher(.enableKeyboardBacklightHUD, options: []).sink { [weak self] _ in guard let self = self, self.isSetupComplete, self.requiresSystemToggleHandling else { return } + let flags = self.resolvedControlFlags() self.changesObserver?.update( - volumeEnabled: Defaults[.enableVolumeHUD], - brightnessEnabled: Defaults[.enableBrightnessHUD], - keyboardBacklightEnabled: change.newValue + volumeEnabled: flags.volume, + brightnessEnabled: flags.brightness, + keyboardBacklightEnabled: flags.backlight ) }.store(in: &cancellables) // Observe individual OSD toggles - Defaults.publisher(.enableOSDVolume, options: []).sink { [weak self] change in + Defaults.publisher(.enableOSDVolume, options: []).sink { [weak self] _ in guard let self = self, self.isSetupComplete, Defaults[.enableCustomOSD] else { return } + let flags = self.resolvedControlFlags() self.changesObserver?.update( - volumeEnabled: change.newValue, - brightnessEnabled: Defaults[.enableOSDBrightness], - keyboardBacklightEnabled: Defaults[.enableOSDKeyboardBacklight] + volumeEnabled: flags.volume, + brightnessEnabled: flags.brightness, + keyboardBacklightEnabled: flags.backlight ) }.store(in: &cancellables) - Defaults.publisher(.enableOSDBrightness, options: []).sink { [weak self] change in + Defaults.publisher(.enableOSDBrightness, options: []).sink { [weak self] _ in guard let self = self, self.isSetupComplete, Defaults[.enableCustomOSD] else { return } + let flags = self.resolvedControlFlags() self.changesObserver?.update( - volumeEnabled: Defaults[.enableOSDVolume], - brightnessEnabled: change.newValue, - keyboardBacklightEnabled: Defaults[.enableOSDKeyboardBacklight] + volumeEnabled: flags.volume, + brightnessEnabled: flags.brightness, + keyboardBacklightEnabled: flags.backlight ) }.store(in: &cancellables) - Defaults.publisher(.enableOSDKeyboardBacklight, options: []).sink { [weak self] change in + Defaults.publisher(.enableOSDKeyboardBacklight, options: []).sink { [weak self] _ in guard let self = self, self.isSetupComplete, Defaults[.enableCustomOSD] else { return } + let flags = self.resolvedControlFlags() self.changesObserver?.update( - volumeEnabled: Defaults[.enableOSDVolume], - brightnessEnabled: Defaults[.enableOSDBrightness], - keyboardBacklightEnabled: change.newValue + volumeEnabled: flags.volume, + brightnessEnabled: flags.brightness, + keyboardBacklightEnabled: flags.backlight ) }.store(in: &cancellables) @@ -214,22 +220,13 @@ class SystemHUDManager { } } - @MainActor - private func startSystemObserver() async { - guard let coordinator = coordinator, !isSystemOperationInProgress else { return } - - isSystemOperationInProgress = true - await stopSystemObserver() // Stop any existing observer - - changesObserver = SystemChangesObserver(coordinator: coordinator) - - // Determine which controls to enable based on HUD or OSD mode - let volumeEnabled: Bool - let brightnessEnabled: Bool + /// Resolves the effective control flags, applying BetterDisplay overrides. + private func resolvedControlFlags() -> (volume: Bool, brightness: Bool, backlight: Bool) { + var volumeEnabled: Bool + var brightnessEnabled: Bool var keyboardBacklightEnabled: Bool - + if Defaults[.enableCircularHUD] || Defaults[.enableVerticalHUD] { - // Respect per-control toggles so the system HUD can handle disabled events volumeEnabled = Defaults[.enableVolumeHUD] brightnessEnabled = Defaults[.enableBrightnessHUD] keyboardBacklightEnabled = Defaults[.enableKeyboardBacklightHUD] @@ -243,22 +240,40 @@ class SystemHUDManager { keyboardBacklightEnabled = Defaults[.enableKeyboardBacklightHUD] } - // When BetterDisplay integration is on, disable Cmd+Brightness key interception - // because BetterDisplay uses Cmd+F1/F2 for its own display controls. + // When BetterDisplay integration is on, stop intercepting brightness and + // Cmd+Brightness keys so BetterDisplay receives them and sends OSD notifications. if Defaults[.enableBetterDisplayIntegration] { + brightnessEnabled = false keyboardBacklightEnabled = false } + + return (volumeEnabled, brightnessEnabled, keyboardBacklightEnabled) + } + + @MainActor + private func startSystemObserver() async { + guard let coordinator = coordinator, !isSystemOperationInProgress else { return } + isSystemOperationInProgress = true + + // Stop any existing observer directly (cannot call stopSystemObserver() + // because isSystemOperationInProgress is already true). + changesObserver?.stopObserving() + changesObserver = nil + + changesObserver = SystemChangesObserver(coordinator: coordinator) + + let flags = resolvedControlFlags() changesObserver?.startObserving( - volumeEnabled: volumeEnabled, - brightnessEnabled: brightnessEnabled, - keyboardBacklightEnabled: keyboardBacklightEnabled + volumeEnabled: flags.volume, + brightnessEnabled: flags.brightness, + keyboardBacklightEnabled: flags.backlight ) // Force disable system HUD to ensure no duplicates SystemOSDManager.disableSystemHUD() - print("System observer started (HUD: \(Defaults[.enableSystemHUD]), OSD: \(Defaults[.enableCustomOSD]), Vertical: \(Defaults[.enableVerticalHUD]))") + print("System observer started (HUD: \(Defaults[.enableSystemHUD]), OSD: \(Defaults[.enableCustomOSD]), Vertical: \(Defaults[.enableVerticalHUD]), BD: \(Defaults[.enableBetterDisplayIntegration]))") isSystemOperationInProgress = false }