mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 23:24:32 +00:00
Update HUDs
This commit is contained in:
commit
7e9fc2ecb8
12 changed files with 1515 additions and 263 deletions
151
DynamicIsland/managers/CircularHUDWindowManager.swift
Normal file
151
DynamicIsland/managers/CircularHUDWindowManager.swift
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
#if os(macOS)
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
import SkyLightWindow
|
||||
import QuartzCore
|
||||
import Defaults
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
final class CircularHUDWindowManager {
|
||||
static let shared = CircularHUDWindowManager()
|
||||
|
||||
private var windows: [NSScreen: OSDWindow] = [:]
|
||||
private var hideWorkItem: DispatchWorkItem?
|
||||
private let displayDuration: TimeInterval = 2.0
|
||||
private let animationDuration: TimeInterval = 0.2
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// Layout constants removed in favor of dynamic calculation
|
||||
|
||||
private init() {
|
||||
setupSizeObserver()
|
||||
}
|
||||
|
||||
private func setupSizeObserver() {
|
||||
Defaults.publisher(.circularHUDSize, options: []).sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
Task { @MainActor in
|
||||
self.updateWindowLayout()
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func updateWindowLayout() {
|
||||
guard !windows.isEmpty else { return }
|
||||
|
||||
for (screen, window) in windows {
|
||||
let screenFrame = screen.frame
|
||||
let hudSize = Defaults[.circularHUDSize]
|
||||
let windowSize = hudSize + 80 // Padding
|
||||
|
||||
let x = screenFrame.midX - (windowSize / 2)
|
||||
let y = screenFrame.midY - (windowSize / 2)
|
||||
let newFrame = NSRect(x: x, y: y, width: windowSize, height: windowSize)
|
||||
|
||||
window.nsWindow.setFrame(newFrame, display: true, animate: true)
|
||||
}
|
||||
}
|
||||
|
||||
func show(type: SneakContentType, value: CGFloat, icon: String = "") {
|
||||
guard Defaults[.enableCircularHUD] else { return }
|
||||
|
||||
let screens = NSScreen.screens
|
||||
guard !screens.isEmpty else { return }
|
||||
|
||||
// Show on all screens
|
||||
for screen in screens {
|
||||
let windowContext = ensureWindow(for: type, screen: screen)
|
||||
updateContent(window: windowContext, type: type, value: value, icon: icon)
|
||||
|
||||
// Ensure visible
|
||||
windowContext.nsWindow.orderFrontRegardless()
|
||||
|
||||
NSAnimationContext.runAnimationGroup { context in
|
||||
context.duration = animationDuration
|
||||
windowContext.nsWindow.animator().alphaValue = 1
|
||||
}
|
||||
}
|
||||
|
||||
scheduleHide()
|
||||
}
|
||||
|
||||
private func ensureWindow(for type: SneakContentType, screen: NSScreen) -> OSDWindow {
|
||||
if let existing = windows[screen] {
|
||||
return existing
|
||||
}
|
||||
|
||||
let osdView = CircularHUDView(type: .constant(type), value: .constant(0), icon: .constant(""))
|
||||
let hostingView = NSHostingView(rootView: osdView)
|
||||
|
||||
let screenFrame = screen.frame
|
||||
|
||||
let hudSize = Defaults[.circularHUDSize]
|
||||
let windowSize = hudSize + 80 // Padding
|
||||
|
||||
// Position at center of screen
|
||||
let x = screenFrame.midX - (windowSize / 2)
|
||||
let y = screenFrame.midY - (windowSize / 2)
|
||||
let frame = NSRect(x: x, y: y, width: windowSize, height: windowSize)
|
||||
|
||||
let win = NSWindow(
|
||||
contentRect: frame,
|
||||
styleMask: [.borderless, .nonactivatingPanel],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
|
||||
win.isOpaque = false
|
||||
win.backgroundColor = .clear
|
||||
win.level = .screenSaver
|
||||
win.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle, .fullScreenAuxiliary]
|
||||
win.hasShadow = false
|
||||
win.contentView = hostingView
|
||||
win.alphaValue = 0
|
||||
|
||||
SkyLightOperator.shared.delegateWindow(win)
|
||||
|
||||
let windowStruct = OSDWindow(nsWindow: win, hostingView: hostingView, type: type)
|
||||
windows[screen] = windowStruct
|
||||
return windowStruct
|
||||
}
|
||||
|
||||
private func updateContent(window: OSDWindow, type: SneakContentType, value: CGFloat, icon: String) {
|
||||
let osdView = CircularHUDView(type: .constant(type), value: .constant(value), icon: .constant(icon))
|
||||
window.hostingView.rootView = osdView
|
||||
}
|
||||
|
||||
private func scheduleHide() {
|
||||
hideWorkItem?.cancel()
|
||||
|
||||
let workItem = DispatchWorkItem { [weak self] in
|
||||
guard let self else { return }
|
||||
Task { @MainActor in
|
||||
self.hideWindow()
|
||||
}
|
||||
}
|
||||
|
||||
hideWorkItem = workItem
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + displayDuration, execute: workItem)
|
||||
}
|
||||
|
||||
private func hideWindow() {
|
||||
for window in windows.values {
|
||||
NSAnimationContext.runAnimationGroup { context in
|
||||
context.duration = animationDuration
|
||||
window.nsWindow.animator().alphaValue = 0
|
||||
} completionHandler: {
|
||||
// Keep window around but hidden for faster subsequent shows
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper struct
|
||||
private struct OSDWindow {
|
||||
let nsWindow: NSWindow
|
||||
let hostingView: NSHostingView<CircularHUDView>
|
||||
let type: SneakContentType
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
@ -11,9 +11,9 @@ import Defaults
|
|||
final class CustomOSDWindowManager {
|
||||
static let shared = CustomOSDWindowManager()
|
||||
|
||||
private var volumeWindow: OSDWindow?
|
||||
private var brightnessWindow: OSDWindow?
|
||||
private var backlightWindow: OSDWindow?
|
||||
private var volumeWindows: [NSScreen: OSDWindow] = [:]
|
||||
private var brightnessWindows: [NSScreen: OSDWindow] = [:]
|
||||
private var backlightWindows: [NSScreen: OSDWindow] = [:]
|
||||
|
||||
private var hideWorkItem: DispatchWorkItem?
|
||||
private let displayDuration: TimeInterval = 2.0
|
||||
|
|
@ -50,62 +50,61 @@ final class CustomOSDWindowManager {
|
|||
// MARK: - Private Implementation
|
||||
|
||||
private func show(type: SneakContentType, value: CGFloat, icon: String) {
|
||||
guard let screen = NSScreen.main else { return }
|
||||
let screens = NSScreen.screens
|
||||
guard !screens.isEmpty else { return }
|
||||
|
||||
// Close other windows first
|
||||
hideAllWindowsExcept(type: type)
|
||||
|
||||
let window = ensureWindow(for: type)
|
||||
updateContent(window: window, type: type, value: value, icon: icon)
|
||||
|
||||
let targetFrame = calculateFrame(for: screen)
|
||||
|
||||
if window.nsWindow.alphaValue <= 0.01 {
|
||||
// Initial presentation
|
||||
presentWindow(window, targetFrame: targetFrame)
|
||||
} else {
|
||||
// Update existing window
|
||||
window.nsWindow.setFrame(targetFrame, display: true)
|
||||
window.nsWindow.orderFrontRegardless()
|
||||
// Show on all screens
|
||||
for screen in screens {
|
||||
let window = ensureWindow(for: type, screen: screen)
|
||||
updateContent(window: window, type: type, value: value, icon: icon)
|
||||
|
||||
let targetFrame = calculateFrame(for: screen)
|
||||
|
||||
if window.nsWindow.alphaValue <= 0.01 {
|
||||
// Initial presentation
|
||||
presentWindow(window, targetFrame: targetFrame)
|
||||
} else {
|
||||
// Update existing window
|
||||
window.nsWindow.setFrame(targetFrame, display: true)
|
||||
window.nsWindow.orderFrontRegardless()
|
||||
}
|
||||
}
|
||||
|
||||
scheduleHide(for: type)
|
||||
}
|
||||
|
||||
private func ensureWindow(for type: SneakContentType) -> OSDWindow {
|
||||
let window: OSDWindow
|
||||
|
||||
private func ensureWindow(for type: SneakContentType, screen: NSScreen) -> OSDWindow {
|
||||
switch type {
|
||||
case .volume:
|
||||
if let existing = volumeWindow {
|
||||
if let existing = volumeWindows[screen] {
|
||||
return existing
|
||||
}
|
||||
window = createWindow(for: type)
|
||||
volumeWindow = window
|
||||
let window = createWindow(for: type, screen: screen)
|
||||
volumeWindows[screen] = window
|
||||
return window
|
||||
case .brightness:
|
||||
if let existing = brightnessWindow {
|
||||
if let existing = brightnessWindows[screen] {
|
||||
return existing
|
||||
}
|
||||
window = createWindow(for: type)
|
||||
brightnessWindow = window
|
||||
let window = createWindow(for: type, screen: screen)
|
||||
brightnessWindows[screen] = window
|
||||
return window
|
||||
case .backlight:
|
||||
if let existing = backlightWindow {
|
||||
if let existing = backlightWindows[screen] {
|
||||
return existing
|
||||
}
|
||||
window = createWindow(for: type)
|
||||
backlightWindow = window
|
||||
let window = createWindow(for: type, screen: screen)
|
||||
backlightWindows[screen] = window
|
||||
return window
|
||||
default:
|
||||
fatalError("Unsupported OSD type: \(type)")
|
||||
}
|
||||
|
||||
return window
|
||||
}
|
||||
|
||||
private func createWindow(for type: SneakContentType) -> OSDWindow {
|
||||
guard let screen = NSScreen.main else {
|
||||
fatalError("No main screen available")
|
||||
}
|
||||
|
||||
private func createWindow(for type: SneakContentType, screen: NSScreen) -> OSDWindow {
|
||||
let osdView = CustomOSDView(type: .constant(type), value: .constant(0), icon: .constant(""))
|
||||
let hostingView = NSHostingView(rootView: osdView)
|
||||
|
||||
|
|
@ -161,14 +160,20 @@ final class CustomOSDWindowManager {
|
|||
}
|
||||
|
||||
private func hideAllWindowsExcept(type: SneakContentType) {
|
||||
if type != .volume, let volumeWindow = volumeWindow, volumeWindow.nsWindow.alphaValue > 0.01 {
|
||||
hideWindowImmediately(volumeWindow)
|
||||
if type != .volume {
|
||||
for window in volumeWindows.values where window.nsWindow.alphaValue > 0.01 {
|
||||
hideWindowImmediately(window)
|
||||
}
|
||||
}
|
||||
if type != .brightness, let brightnessWindow = brightnessWindow, brightnessWindow.nsWindow.alphaValue > 0.01 {
|
||||
hideWindowImmediately(brightnessWindow)
|
||||
if type != .brightness {
|
||||
for window in brightnessWindows.values where window.nsWindow.alphaValue > 0.01 {
|
||||
hideWindowImmediately(window)
|
||||
}
|
||||
}
|
||||
if type != .backlight, let backlightWindow = backlightWindow, backlightWindow.nsWindow.alphaValue > 0.01 {
|
||||
hideWindowImmediately(backlightWindow)
|
||||
if type != .backlight {
|
||||
for window in backlightWindows.values where window.nsWindow.alphaValue > 0.01 {
|
||||
hideWindowImmediately(window)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -197,20 +202,20 @@ final class CustomOSDWindowManager {
|
|||
}
|
||||
|
||||
private func hideWindow(for type: SneakContentType) {
|
||||
let window: OSDWindow?
|
||||
let windows: [OSDWindow]
|
||||
|
||||
switch type {
|
||||
case .volume:
|
||||
window = volumeWindow
|
||||
windows = Array(volumeWindows.values)
|
||||
case .brightness:
|
||||
window = brightnessWindow
|
||||
windows = Array(brightnessWindows.values)
|
||||
case .backlight:
|
||||
window = backlightWindow
|
||||
windows = Array(backlightWindows.values)
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
guard let window else { return }
|
||||
for window in windows {
|
||||
|
||||
let currentFrame = window.nsWindow.frame
|
||||
let hideFrame = currentFrame.offsetBy(dx: 0, dy: -20)
|
||||
|
|
@ -223,6 +228,7 @@ final class CustomOSDWindowManager {
|
|||
} completionHandler: {
|
||||
window.nsWindow.orderOut(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cleanup
|
||||
|
|
@ -231,13 +237,19 @@ final class CustomOSDWindowManager {
|
|||
hideWorkItem?.cancel()
|
||||
hideWorkItem = nil
|
||||
|
||||
volumeWindow?.nsWindow.orderOut(nil)
|
||||
brightnessWindow?.nsWindow.orderOut(nil)
|
||||
backlightWindow?.nsWindow.orderOut(nil)
|
||||
for window in volumeWindows.values {
|
||||
window.nsWindow.orderOut(nil)
|
||||
}
|
||||
for window in brightnessWindows.values {
|
||||
window.nsWindow.orderOut(nil)
|
||||
}
|
||||
for window in backlightWindows.values {
|
||||
window.nsWindow.orderOut(nil)
|
||||
}
|
||||
|
||||
volumeWindow = nil
|
||||
brightnessWindow = nil
|
||||
backlightWindow = nil
|
||||
volumeWindows.removeAll()
|
||||
brightnessWindows.removeAll()
|
||||
backlightWindows.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,17 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
|
|||
modifiers: NSEvent.ModifierFlags
|
||||
) {
|
||||
guard volumeEnabled else { return }
|
||||
|
||||
// Elastic Limit Detection (Vertical HUD)
|
||||
if Defaults[.enableVerticalHUD] {
|
||||
let volume = volumeController.currentVolume
|
||||
if direction == .up && volume >= 0.99 {
|
||||
Task { @MainActor in VerticalHUDWindowManager.shared.triggerBump(direction: 1) }
|
||||
} else if direction == .down && volume <= 0.01 {
|
||||
Task { @MainActor in VerticalHUDWindowManager.shared.triggerBump(direction: -1) }
|
||||
}
|
||||
}
|
||||
|
||||
let baseStep = stepSize(for: step, base: standardVolumeStep)
|
||||
let delta = direction == .up ? baseStep : -baseStep
|
||||
volumeController.adjust(by: delta)
|
||||
|
|
@ -130,6 +141,16 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
|
|||
isRepeat: Bool,
|
||||
modifiers: NSEvent.ModifierFlags
|
||||
) {
|
||||
// Elastic Limit Detection (Vertical HUD)
|
||||
if Defaults[.enableVerticalHUD] {
|
||||
let brightness = brightnessController.currentBrightness
|
||||
if direction == .up && brightness >= 0.99 {
|
||||
Task { @MainActor in VerticalHUDWindowManager.shared.triggerBump(direction: 1) }
|
||||
} else if direction == .down && brightness <= 0.01 {
|
||||
Task { @MainActor in VerticalHUDWindowManager.shared.triggerBump(direction: -1) }
|
||||
}
|
||||
}
|
||||
|
||||
let baseStep = stepSize(for: step, base: standardBrightnessStep)
|
||||
let delta = direction == .up ? baseStep : -baseStep
|
||||
if modifiers.contains(.command) && keyboardBacklightEnabled {
|
||||
|
|
@ -146,6 +167,22 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
|
|||
return
|
||||
}
|
||||
|
||||
// Send to Circular HUD if enabled
|
||||
if Defaults[.enableCircularHUD] {
|
||||
Task { @MainActor in
|
||||
CircularHUDWindowManager.shared.show(type: .volume, value: CGFloat(value))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Send to Vertical HUD if enabled
|
||||
if Defaults[.enableVerticalHUD] {
|
||||
Task { @MainActor in
|
||||
VerticalHUDWindowManager.shared.show(type: .volume, value: CGFloat(value))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Send to custom OSD if enabled
|
||||
if Defaults[.enableCustomOSD] && Defaults[.enableOSDVolume] {
|
||||
Task { @MainActor in
|
||||
|
|
@ -153,8 +190,8 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
// Send to notch HUD if enabled and OSD is not enabled
|
||||
if Defaults[.enableSystemHUD] && !Defaults[.enableCustomOSD] && Defaults[.enableVolumeHUD] {
|
||||
// Send to notch HUD if enabled and OSD/Vertical/Circular is not enabled
|
||||
if Defaults[.enableSystemHUD] && !Defaults[.enableCustomOSD] && !Defaults[.enableVerticalHUD] && !Defaults[.enableCircularHUD] && Defaults[.enableVolumeHUD] {
|
||||
Task { @MainActor in
|
||||
guard let coordinator else { return }
|
||||
coordinator.toggleSneakPeek(
|
||||
|
|
@ -168,6 +205,22 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
|
|||
}
|
||||
|
||||
private func sendBrightnessNotification(value: Float) {
|
||||
// Send to Circular HUD if enabled
|
||||
if Defaults[.enableCircularHUD] {
|
||||
Task { @MainActor in
|
||||
CircularHUDWindowManager.shared.show(type: .brightness, value: CGFloat(value))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Send to Vertical HUD if enabled
|
||||
if Defaults[.enableVerticalHUD] {
|
||||
Task { @MainActor in
|
||||
VerticalHUDWindowManager.shared.show(type: .brightness, value: CGFloat(value))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Send to custom OSD if enabled
|
||||
if Defaults[.enableCustomOSD] && Defaults[.enableOSDBrightness] {
|
||||
Task { @MainActor in
|
||||
|
|
@ -175,8 +228,8 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
// Send to notch HUD if enabled and OSD is not enabled
|
||||
if Defaults[.enableSystemHUD] && !Defaults[.enableCustomOSD] && Defaults[.enableBrightnessHUD] {
|
||||
// Send to notch HUD if enabled and OSD/Vertical/Circular is not enabled
|
||||
if Defaults[.enableSystemHUD] && !Defaults[.enableCustomOSD] && !Defaults[.enableVerticalHUD] && !Defaults[.enableCircularHUD] && Defaults[.enableBrightnessHUD] {
|
||||
Task { @MainActor in
|
||||
guard let coordinator else { return }
|
||||
coordinator.toggleSneakPeek(
|
||||
|
|
@ -190,6 +243,22 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
|
|||
}
|
||||
|
||||
private func sendKeyboardBacklightNotification(value: Float) {
|
||||
// Send to Circular HUD if enabled
|
||||
if Defaults[.enableCircularHUD] {
|
||||
Task { @MainActor in
|
||||
CircularHUDWindowManager.shared.show(type: .backlight, value: CGFloat(value))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Send to Vertical HUD if enabled
|
||||
if Defaults[.enableVerticalHUD] {
|
||||
Task { @MainActor in
|
||||
VerticalHUDWindowManager.shared.show(type: .backlight, value: CGFloat(value))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Send to custom OSD if enabled
|
||||
if Defaults[.enableCustomOSD] && Defaults[.enableOSDKeyboardBacklight] {
|
||||
Task { @MainActor in
|
||||
|
|
@ -197,8 +266,8 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
// Send to notch HUD if enabled and OSD is not enabled
|
||||
if Defaults[.enableSystemHUD] && !Defaults[.enableCustomOSD] && Defaults[.enableKeyboardBacklightHUD] {
|
||||
// Send to notch HUD if enabled and OSD/Vertical/Circular is not enabled
|
||||
if Defaults[.enableSystemHUD] && !Defaults[.enableCustomOSD] && !Defaults[.enableVerticalHUD] && !Defaults[.enableCircularHUD] && Defaults[.enableKeyboardBacklightHUD] {
|
||||
Task { @MainActor in
|
||||
guard let coordinator else { return }
|
||||
coordinator.toggleSneakPeek(
|
||||
|
|
|
|||
|
|
@ -23,17 +23,39 @@ class SystemHUDManager {
|
|||
}
|
||||
|
||||
private func setupSettingsObserver() {
|
||||
// Observe Vertical HUD toggle
|
||||
Defaults.publisher(.enableVerticalHUD, options: []).sink { [weak self] change in
|
||||
guard let self = self, self.isSetupComplete else {
|
||||
return
|
||||
}
|
||||
Task { @MainActor in
|
||||
if change.newValue {
|
||||
// Always restart to ensure correct flags are applied
|
||||
await self.startSystemObserver()
|
||||
} else if Defaults[.enableSystemHUD] || Defaults[.enableCustomOSD] || Defaults[.enableCircularHUD] {
|
||||
// Restart to apply flags for the remaining active HUD
|
||||
await self.startSystemObserver()
|
||||
} else {
|
||||
await self.stopSystemObserver()
|
||||
}
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
|
||||
// Observe master HUD toggle
|
||||
Defaults.publisher(.enableSystemHUD, options: []).sink { [weak self] change in
|
||||
guard let self = self, self.isSetupComplete else {
|
||||
return
|
||||
}
|
||||
Task { @MainActor in
|
||||
if change.newValue && !Defaults[.enableCustomOSD] {
|
||||
if change.newValue && !Defaults[.enableCustomOSD] && !Defaults[.enableVerticalHUD] && !Defaults[.enableCircularHUD] {
|
||||
// Always restart to ensure correct flags are applied
|
||||
await self.startSystemObserver()
|
||||
} else if !Defaults[.enableCustomOSD] {
|
||||
// Only stop if OSD is also disabled
|
||||
} else if !Defaults[.enableCustomOSD] && !Defaults[.enableVerticalHUD] && !Defaults[.enableCircularHUD] {
|
||||
// Only stop if others are also disabled
|
||||
await self.stopSystemObserver()
|
||||
} else if change.newValue == false && (Defaults[.enableCustomOSD] || Defaults[.enableVerticalHUD] || Defaults[.enableCircularHUD]) {
|
||||
// If disabling system HUD but others are active, restart to update flags
|
||||
await self.startSystemObserver()
|
||||
}
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
|
|
@ -45,17 +67,30 @@ class SystemHUDManager {
|
|||
}
|
||||
Task { @MainActor in
|
||||
if change.newValue {
|
||||
// Start observer for OSD if not already running
|
||||
if self.changesObserver == nil {
|
||||
await self.startSystemObserver()
|
||||
}
|
||||
} else if Defaults[.enableSystemHUD] {
|
||||
// Keep observer running for HUD
|
||||
if self.changesObserver == nil {
|
||||
await self.startSystemObserver()
|
||||
}
|
||||
// Always restart to ensure correct flags are applied
|
||||
await self.startSystemObserver()
|
||||
} else if Defaults[.enableSystemHUD] || Defaults[.enableVerticalHUD] || Defaults[.enableCircularHUD] {
|
||||
// Restart to apply flags for the remaining active HUD
|
||||
await self.startSystemObserver()
|
||||
} else {
|
||||
await self.stopSystemObserver()
|
||||
}
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
|
||||
// Observe Circular HUD toggle
|
||||
Defaults.publisher(.enableCircularHUD, options: []).sink { [weak self] change in
|
||||
guard let self = self, self.isSetupComplete else {
|
||||
return
|
||||
}
|
||||
Task { @MainActor in
|
||||
if change.newValue {
|
||||
// Always restart to ensure correct flags are applied
|
||||
await self.startSystemObserver()
|
||||
} else if Defaults[.enableSystemHUD] || Defaults[.enableCustomOSD] || Defaults[.enableVerticalHUD] {
|
||||
// Restart to apply flags for the remaining active HUD
|
||||
await self.startSystemObserver()
|
||||
} else {
|
||||
// Stop observer if both HUD and OSD are disabled
|
||||
await self.stopSystemObserver()
|
||||
}
|
||||
}
|
||||
|
|
@ -145,8 +180,8 @@ class SystemHUDManager {
|
|||
CustomOSDWindowManager.shared.initialize()
|
||||
}
|
||||
|
||||
// Start observer if either HUD or OSD is enabled
|
||||
if Defaults[.enableSystemHUD] || Defaults[.enableCustomOSD] {
|
||||
// Start observer if any HUD/OSD is enabled
|
||||
if Defaults[.enableSystemHUD] || Defaults[.enableCustomOSD] || Defaults[.enableVerticalHUD] || Defaults[.enableCircularHUD] {
|
||||
Task { @MainActor in
|
||||
await startSystemObserver()
|
||||
self.isSetupComplete = true
|
||||
|
|
@ -170,7 +205,12 @@ class SystemHUDManager {
|
|||
let brightnessEnabled: Bool
|
||||
let keyboardBacklightEnabled: Bool
|
||||
|
||||
if Defaults[.enableCustomOSD] {
|
||||
if Defaults[.enableCircularHUD] || Defaults[.enableVerticalHUD] {
|
||||
// Vertical and Circular HUD support all events
|
||||
volumeEnabled = true
|
||||
brightnessEnabled = true
|
||||
keyboardBacklightEnabled = true
|
||||
} else if Defaults[.enableCustomOSD] {
|
||||
volumeEnabled = Defaults[.enableOSDVolume]
|
||||
brightnessEnabled = Defaults[.enableOSDBrightness]
|
||||
keyboardBacklightEnabled = Defaults[.enableOSDKeyboardBacklight]
|
||||
|
|
@ -186,7 +226,10 @@ class SystemHUDManager {
|
|||
keyboardBacklightEnabled: keyboardBacklightEnabled
|
||||
)
|
||||
|
||||
print("System observer started (HUD: \(Defaults[.enableSystemHUD]), OSD: \(Defaults[.enableCustomOSD]))")
|
||||
// Force disable system HUD to ensure no duplicates
|
||||
SystemOSDManager.disableSystemHUD()
|
||||
|
||||
print("System observer started (HUD: \(Defaults[.enableSystemHUD]), OSD: \(Defaults[.enableCustomOSD]), Vertical: \(Defaults[.enableVerticalHUD]))")
|
||||
isSystemOperationInProgress = false
|
||||
}
|
||||
|
||||
|
|
@ -198,6 +241,9 @@ class SystemHUDManager {
|
|||
changesObserver?.stopObserving()
|
||||
changesObserver = nil
|
||||
|
||||
// Re-enable system HUD when we stop observing
|
||||
SystemOSDManager.enableSystemHUD()
|
||||
|
||||
print("System observer stopped")
|
||||
isSystemOperationInProgress = false
|
||||
}
|
||||
|
|
|
|||
206
DynamicIsland/managers/VerticalHUDWindowManager.swift
Normal file
206
DynamicIsland/managers/VerticalHUDWindowManager.swift
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
#if os(macOS)
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
import SkyLightWindow
|
||||
import QuartzCore
|
||||
import Defaults
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
final class VerticalHUDWindowManager {
|
||||
static let shared = VerticalHUDWindowManager()
|
||||
|
||||
// Properties
|
||||
private var windows: [NSScreen: OSDWindow] = [:]
|
||||
private var hideWorkItem: DispatchWorkItem?
|
||||
private let displayDuration: TimeInterval = 2.0
|
||||
private let animationDuration: TimeInterval = 0.2
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private init() {
|
||||
setupPositionObserver()
|
||||
}
|
||||
|
||||
// Helper Struct
|
||||
private struct OSDWindow {
|
||||
let nsWindow: NSWindow
|
||||
let hostingView: NSHostingView<VerticalHUDView>
|
||||
let state: VerticalHUDState
|
||||
}
|
||||
|
||||
private func setupPositionObserver() {
|
||||
// Observe layout keys individually
|
||||
let updateBlock: (Any) -> Void = { [weak self] _ in
|
||||
Task { @MainActor [weak self] in
|
||||
self?.updateWindowLayout()
|
||||
}
|
||||
}
|
||||
|
||||
Defaults.publisher(.verticalHUDPosition, options: []).sink(receiveValue: updateBlock).store(in: &cancellables)
|
||||
Defaults.publisher(.verticalHUDHeight, options: []).sink(receiveValue: updateBlock).store(in: &cancellables)
|
||||
Defaults.publisher(.verticalHUDWidth, options: []).sink(receiveValue: updateBlock).store(in: &cancellables)
|
||||
Defaults.publisher(.verticalHUDPadding, options: []).sink(receiveValue: updateBlock).store(in: &cancellables)
|
||||
Defaults.publisher(.verticalHUDInteractive, options: []).sink(receiveValue: updateBlock).store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func updateWindowLayout() {
|
||||
guard !windows.isEmpty else { return }
|
||||
|
||||
for (screen, window) in windows {
|
||||
let screenFrame = screen.frame
|
||||
let position = Defaults[.verticalHUDPosition]
|
||||
let width = Defaults[.verticalHUDWidth]
|
||||
let height = Defaults[.verticalHUDHeight]
|
||||
let padding = Defaults[.verticalHUDPadding]
|
||||
let interactive = Defaults[.verticalHUDInteractive]
|
||||
|
||||
// Add padding for shadow and elastic stretch
|
||||
// We use 120px total padding (60px per side) to absolutely guarantee no clipping.
|
||||
let shadowPadding: CGFloat = 120
|
||||
let totalWidth = width + shadowPadding
|
||||
let totalHeight = height + shadowPadding
|
||||
|
||||
let x: CGFloat
|
||||
if position == "left" {
|
||||
// Left edge of HUD is at (minX + padding).
|
||||
// Window edge = HUD Edge - half shadow padding
|
||||
x = screenFrame.minX + padding - (shadowPadding / 2)
|
||||
} else {
|
||||
// Right edge of HUD is at (maxX - padding).
|
||||
// Left edge of HUD = maxX - padding - width.
|
||||
// Window edge = HUD Edge - half shadow padding
|
||||
x = screenFrame.maxX - width - padding - (shadowPadding / 2)
|
||||
}
|
||||
|
||||
let y = screenFrame.midY - (totalHeight / 2)
|
||||
let newFrame = NSRect(x: x, y: y, width: totalWidth, height: totalHeight)
|
||||
|
||||
window.nsWindow.setFrame(newFrame, display: true, animate: true)
|
||||
window.nsWindow.ignoresMouseEvents = !interactive
|
||||
}
|
||||
}
|
||||
|
||||
func show(type: SneakContentType, value: CGFloat, icon: String = "") {
|
||||
guard Defaults[.enableVerticalHUD] else { return }
|
||||
|
||||
let screens = NSScreen.screens
|
||||
guard !screens.isEmpty else { return }
|
||||
|
||||
// Show on all screens
|
||||
for screen in screens {
|
||||
let windowContext = ensureWindow(for: type, screen: screen)
|
||||
updateContent(window: windowContext, type: type, value: value, icon: icon)
|
||||
|
||||
// Ensure visible
|
||||
windowContext.nsWindow.orderFrontRegardless()
|
||||
|
||||
NSAnimationContext.runAnimationGroup { context in
|
||||
context.duration = animationDuration
|
||||
windowContext.nsWindow.animator().alphaValue = 1
|
||||
}
|
||||
}
|
||||
|
||||
scheduleHide()
|
||||
}
|
||||
|
||||
func triggerBump(direction: Int) {
|
||||
guard Defaults[.enableVerticalHUD] else { return }
|
||||
Task { @MainActor in
|
||||
for window in windows.values {
|
||||
window.state.bumpEvent = BumpEvent(direction: direction, timestamp: Date())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func ensureWindow(for type: SneakContentType, screen: NSScreen) -> OSDWindow {
|
||||
if let existing = windows[screen] {
|
||||
return existing
|
||||
}
|
||||
|
||||
let state = VerticalHUDState(type: type, value: 0, icon: "")
|
||||
let osdView = VerticalHUDView(state: state)
|
||||
let hostingView = NSHostingView(rootView: osdView)
|
||||
|
||||
// Calculate frame
|
||||
let screenFrame = screen.frame
|
||||
let position = Defaults[.verticalHUDPosition]
|
||||
let width = Defaults[.verticalHUDWidth]
|
||||
let height = Defaults[.verticalHUDHeight]
|
||||
let padding = Defaults[.verticalHUDPadding]
|
||||
|
||||
// Add padding for shadow and elastic stretch
|
||||
// We use 120px total padding (60px per side) to absolutely guarantee no clipping.
|
||||
let shadowPadding: CGFloat = 120
|
||||
let totalWidth = width + shadowPadding
|
||||
let totalHeight = height + shadowPadding
|
||||
|
||||
let x: CGFloat
|
||||
if position == "left" {
|
||||
x = screenFrame.minX + padding - (shadowPadding / 2)
|
||||
} else {
|
||||
x = screenFrame.maxX - width - padding - (shadowPadding / 2)
|
||||
}
|
||||
|
||||
// Center vertically
|
||||
let y = screenFrame.midY - (totalHeight / 2)
|
||||
let frame = NSRect(x: x, y: y, width: totalWidth, height: totalHeight)
|
||||
|
||||
let win = NSWindow(
|
||||
contentRect: frame,
|
||||
styleMask: [.borderless, .nonactivatingPanel],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
|
||||
win.isOpaque = false
|
||||
win.backgroundColor = NSColor.clear
|
||||
win.level = NSWindow.Level.screenSaver
|
||||
win.collectionBehavior = [NSWindow.CollectionBehavior.canJoinAllSpaces, NSWindow.CollectionBehavior.stationary, NSWindow.CollectionBehavior.ignoresCycle, NSWindow.CollectionBehavior.fullScreenAuxiliary]
|
||||
win.hasShadow = false
|
||||
win.contentView = hostingView
|
||||
win.alphaValue = 0
|
||||
|
||||
// Allow mouse interaction if enabled
|
||||
win.ignoresMouseEvents = !Defaults[.verticalHUDInteractive]
|
||||
|
||||
SkyLightOperator.shared.delegateWindow(win)
|
||||
|
||||
let windowStruct = OSDWindow(nsWindow: win, hostingView: hostingView, state: state)
|
||||
windows[screen] = windowStruct
|
||||
return windowStruct
|
||||
}
|
||||
|
||||
private func updateContent(window: OSDWindow, type: SneakContentType, value: CGFloat, icon: String) {
|
||||
// Update state instead of replacing view
|
||||
window.state.type = type
|
||||
window.state.value = value
|
||||
window.state.icon = icon
|
||||
}
|
||||
|
||||
private func scheduleHide() {
|
||||
hideWorkItem?.cancel()
|
||||
|
||||
let workItem = DispatchWorkItem { [weak self] in
|
||||
guard let self else { return }
|
||||
Task { @MainActor in
|
||||
self.hideWindow()
|
||||
}
|
||||
}
|
||||
|
||||
hideWorkItem = workItem
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + displayDuration, execute: workItem)
|
||||
}
|
||||
|
||||
private func hideWindow() {
|
||||
for window in windows.values {
|
||||
NSAnimationContext.runAnimationGroup { context in
|
||||
context.duration = animationDuration
|
||||
window.nsWindow.animator().alphaValue = 0
|
||||
} completionHandler: {
|
||||
// Keep window around
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Loading…
Add table
Add a link
Reference in a new issue