diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 8f3673d..981af01 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -249,8 +249,9 @@ struct ContentView: View { } /// Whether the notch/island should hide off-screen when closed on a non-notch display. + /// Temporarily reveals the notch when a sneakPeek HUD (volume, brightness, music, etc.) is active. private var shouldHideUntilHover: Bool { - hideNonNotchUntilHover && isNonNotchScreen && vm.notchState == .closed + hideNonNotchUntilHover && isNonNotchScreen && vm.notchState == .closed && !coordinator.sneakPeek.show } /// Pill shape for Dynamic Island mode with animated corner radius transitions. diff --git a/DynamicIsland/components/Live activities/InlineHUD.swift b/DynamicIsland/components/Live activities/InlineHUD.swift index 079bb7e..975e88d 100644 --- a/DynamicIsland/components/Live activities/InlineHUD.swift +++ b/DynamicIsland/components/Live activities/InlineHUD.swift @@ -212,7 +212,7 @@ struct InlineHUD: View { .frame(width: 20, height: 15, alignment: .leading) } case .brightness: - Image(systemName: BrightnessSymbol(value)) + Image(systemName: !icon.isEmpty ? icon : BrightnessSymbol(value)) .contentTransition(.interpolate) .frame(width: 20, height: 15, alignment: .center) case .backlight: diff --git a/DynamicIsland/managers/BetterDisplayManager.swift b/DynamicIsland/managers/BetterDisplayManager.swift index 8f1fcc2..5d10948 100644 --- a/DynamicIsland/managers/BetterDisplayManager.swift +++ b/DynamicIsland/managers/BetterDisplayManager.swift @@ -205,40 +205,44 @@ final class BetterDisplayManager: ObservableObject { private func routeOSDToHUD(_ osd: BetterDisplayOSDNotification) { let category = classifyControlTarget(osd.controlTarget, systemIconID: osd.systemIconID) let normalizedValue = normalizeValue(osd.value, maxValue: osd.maxValue) + let targetScreen = resolveScreen(for: osd.displayID) + let isExternalDisplay = isExternal(displayID: osd.displayID) switch category { case .brightness: guard Defaults[.enableBrightnessHUD] else { return } - dispatchBrightnessHUD(value: normalizedValue) + 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) + 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 } - dispatchBrightnessHUD(value: normalizedValue, customSymbol: osd.customSymbol) + let icon = isExternalDisplay ? "display" : osd.customSymbol + dispatchBrightnessHUD(value: normalizedValue, customSymbol: icon, onScreen: targetScreen) } } // MARK: - HUD Dispatch (mirrors SystemChangesObserver logic) - private func dispatchVolumeHUD(value: CGFloat, isMuted: Bool) { + private func dispatchVolumeHUD(value: CGFloat, isMuted: Bool, onScreen targetScreen: NSScreen? = nil) { if HUDSuppressionCoordinator.shared.shouldSuppressVolumeHUD { return } if Defaults[.enableCircularHUD] { - CircularHUDWindowManager.shared.show(type: .volume, value: value) + CircularHUDWindowManager.shared.show(type: .volume, value: value, onScreen: targetScreen) return } if Defaults[.enableVerticalHUD] { - VerticalHUDWindowManager.shared.show(type: .volume, value: value, icon: "") + VerticalHUDWindowManager.shared.show(type: .volume, value: value, icon: "", onScreen: targetScreen) return } if Defaults[.enableCustomOSD] && Defaults[.enableOSDVolume] { - CustomOSDWindowManager.shared.showVolume(value: value) + CustomOSDWindowManager.shared.showVolume(value: value, onScreen: targetScreen) } if Defaults[.enableSystemHUD] && !Defaults[.enableCustomOSD] && !Defaults[.enableVerticalHUD] && !Defaults[.enableCircularHUD] { coordinator?.toggleSneakPeek( @@ -250,30 +254,49 @@ final class BetterDisplayManager: ObservableObject { } } - private func dispatchBrightnessHUD(value: CGFloat, customSymbol: String? = nil) { + private func dispatchBrightnessHUD(value: CGFloat, customSymbol: String? = nil, onScreen targetScreen: NSScreen? = nil) { + let icon = customSymbol ?? "" if Defaults[.enableCircularHUD] { - CircularHUDWindowManager.shared.show(type: .brightness, value: value) + CircularHUDWindowManager.shared.show(type: .brightness, value: value, icon: icon, onScreen: targetScreen) return } if Defaults[.enableVerticalHUD] { - VerticalHUDWindowManager.shared.show(type: .brightness, value: value) + VerticalHUDWindowManager.shared.show(type: .brightness, value: value, icon: icon, onScreen: targetScreen) return } if Defaults[.enableCustomOSD] && Defaults[.enableOSDBrightness] { - CustomOSDWindowManager.shared.showBrightness(value: value) + CustomOSDWindowManager.shared.showBrightness(value: value, icon: icon, onScreen: targetScreen) } if Defaults[.enableSystemHUD] && !Defaults[.enableCustomOSD] && !Defaults[.enableVerticalHUD] && !Defaults[.enableCircularHUD] { coordinator?.toggleSneakPeek( status: true, type: .brightness, value: value, - icon: "" + icon: icon ) } } // MARK: - Helpers + /// Resolve a BetterDisplay `displayID` (CGDirectDisplayID) to the matching NSScreen, if any. + private func resolveScreen(for displayID: Int?) -> NSScreen? { + guard let displayID else { return nil } + let target = UInt32(displayID) + return NSScreen.screens.first { screen in + guard let screenNumber = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber else { + return false + } + return screenNumber.uint32Value == target + } + } + + /// Whether the given displayID refers to an external (non-built-in) display. + private func isExternal(displayID: Int?) -> Bool { + guard let displayID else { return false } + return CGDisplayIsBuiltin(UInt32(displayID)) == 0 + } + private func classifyControlTarget(_ target: String?, systemIconID: Int?) -> BetterDisplayControlCategory { if let target { if brightnessControlTargets.contains(target) { return .brightness } diff --git a/DynamicIsland/managers/CircularHUDWindowManager.swift b/DynamicIsland/managers/CircularHUDWindowManager.swift index 2ee90dd..a6847d4 100644 --- a/DynamicIsland/managers/CircularHUDWindowManager.swift +++ b/DynamicIsland/managers/CircularHUDWindowManager.swift @@ -66,13 +66,13 @@ final class CircularHUDWindowManager { } } - func show(type: SneakContentType, value: CGFloat, icon: String = "") { + func show(type: SneakContentType, value: CGFloat, icon: String = "", onScreen targetScreen: NSScreen? = nil) { guard Defaults[.enableCircularHUD] else { return } - let screens = NSScreen.screens + let screens = targetScreen.map { [$0] } ?? NSScreen.screens guard !screens.isEmpty else { return } - // Show on all screens + // Show on target screen(s) for screen in screens { let windowContext = ensureWindow(for: type, screen: screen) updateContent(window: windowContext, type: type, value: value, icon: icon) diff --git a/DynamicIsland/managers/CustomOSDWindowManager.swift b/DynamicIsland/managers/CustomOSDWindowManager.swift index 02d3120..ee808c8 100644 --- a/DynamicIsland/managers/CustomOSDWindowManager.swift +++ b/DynamicIsland/managers/CustomOSDWindowManager.swift @@ -46,14 +46,14 @@ final class CustomOSDWindowManager { // MARK: - Public API - func showVolume(value: CGFloat, isMuted: Bool = false, icon: String = "") { + func showVolume(value: CGFloat, isMuted: Bool = false, icon: String = "", onScreen targetScreen: NSScreen? = nil) { guard Defaults[.enableCustomOSD], isInitialized else { return } - show(type: .volume, value: value, icon: icon) + show(type: .volume, value: value, icon: icon, onScreen: targetScreen) } - func showBrightness(value: CGFloat) { + func showBrightness(value: CGFloat, icon: String = "", onScreen targetScreen: NSScreen? = nil) { guard Defaults[.enableCustomOSD], isInitialized else { return } - show(type: .brightness, value: value, icon: "") + show(type: .brightness, value: value, icon: icon, onScreen: targetScreen) } func showBacklight(value: CGFloat) { @@ -67,14 +67,14 @@ final class CustomOSDWindowManager { // MARK: - Private Implementation - private func show(type: SneakContentType, value: CGFloat, icon: String) { - let screens = NSScreen.screens + private func show(type: SneakContentType, value: CGFloat, icon: String, onScreen targetScreen: NSScreen? = nil) { + let screens = targetScreen.map { [$0] } ?? NSScreen.screens guard !screens.isEmpty else { return } // Close other windows first hideAllWindowsExcept(type: type) - // Show on all screens + // Show on target screen(s) for screen in screens { let window = ensureWindow(for: type, screen: screen) updateContent(window: window, type: type, value: value, icon: icon) diff --git a/DynamicIsland/managers/SystemHUDManager.swift b/DynamicIsland/managers/SystemHUDManager.swift index 3591425..77cdead 100644 --- a/DynamicIsland/managers/SystemHUDManager.swift +++ b/DynamicIsland/managers/SystemHUDManager.swift @@ -174,6 +174,14 @@ class SystemHUDManager { keyboardBacklightEnabled: change.newValue ) }.store(in: &cancellables) + + // When BetterDisplay integration toggled, restart observer to update Cmd+Brightness key interception + Defaults.publisher(.enableBetterDisplayIntegration, options: []).sink { [weak self] _ in + guard let self = self, self.isSetupComplete else { return } + Task { @MainActor in + await self.startSystemObserver() + } + }.store(in: &cancellables) } private var cancellables = Set() @@ -218,7 +226,7 @@ class SystemHUDManager { // Determine which controls to enable based on HUD or OSD mode let volumeEnabled: Bool let brightnessEnabled: Bool - let keyboardBacklightEnabled: Bool + var keyboardBacklightEnabled: Bool if Defaults[.enableCircularHUD] || Defaults[.enableVerticalHUD] { // Respect per-control toggles so the system HUD can handle disabled events @@ -234,6 +242,12 @@ class SystemHUDManager { brightnessEnabled = Defaults[.enableBrightnessHUD] keyboardBacklightEnabled = Defaults[.enableKeyboardBacklightHUD] } + + // When BetterDisplay integration is on, disable Cmd+Brightness key interception + // because BetterDisplay uses Cmd+F1/F2 for its own display controls. + if Defaults[.enableBetterDisplayIntegration] { + keyboardBacklightEnabled = false + } changesObserver?.startObserving( volumeEnabled: volumeEnabled, diff --git a/DynamicIsland/managers/VerticalHUDWindowManager.swift b/DynamicIsland/managers/VerticalHUDWindowManager.swift index 6f1dd9d..ec61c24 100644 --- a/DynamicIsland/managers/VerticalHUDWindowManager.swift +++ b/DynamicIsland/managers/VerticalHUDWindowManager.swift @@ -136,13 +136,13 @@ final class VerticalHUDWindowManager { windows.removeAll() } - func show(type: SneakContentType, value: CGFloat, icon: String = "") { + func show(type: SneakContentType, value: CGFloat, icon: String = "", onScreen targetScreen: NSScreen? = nil) { guard Defaults[.enableVerticalHUD] else { return } - let screens = NSScreen.screens + let screens = targetScreen.map { [$0] } ?? NSScreen.screens guard !screens.isEmpty else { return } - // Show on all screens + // Show on target screen(s) for screen in screens { let windowContext = ensureWindow(for: type, screen: screen) updateContent(window: windowContext, type: type, value: value, icon: icon)