Atoll/DynamicIsland/DynamicIslandApp.swift
Hariharan Mudaliar eb5d0d42d9
Remove License BS
2026-01-01 13:37:14 +05:30

887 lines
34 KiB
Swift

//
// DynamicIslandApp.swift
// DynamicIslandApp
//
// Modified by Hariharan Mudaliar on 20/09/25.
//
import AVFoundation
import Combine
import Defaults
import KeyboardShortcuts
import Sparkle
import SwiftUI
import SkyLightWindow
@main
struct DynamicNotchApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@Default(.menubarIcon) var showMenuBarIcon
@Environment(\.openWindow) var openWindow
let updaterController: SPUStandardUpdaterController
init() {
updaterController = SPUStandardUpdaterController(
startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil)
// Initialize the settings window controller with the updater controller
SettingsWindowController.shared.setUpdaterController(updaterController)
}
var body: some Scene {
MenuBarExtra("dynamic.island", systemImage: "mountain.2.fill", isInserted: $showMenuBarIcon) {
Button("Settings") {
SettingsWindowController.shared.showWindow()
}
CheckForUpdatesView(updater: updaterController.updater)
Divider()
Button("Restart Atoll") {
guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return }
let workspace = NSWorkspace.shared
if let appURL = workspace.urlForApplication(withBundleIdentifier: bundleIdentifier)
{
let configuration = NSWorkspace.OpenConfiguration()
configuration.createsNewApplicationInstance = true
workspace.openApplication(at: appURL, configuration: configuration)
}
NSApplication.shared.terminate(self)
}
Button("Quit", role: .destructive) {
NSApplication.shared.terminate(self)
}
.keyboardShortcut(KeyEquivalent("Q"), modifiers: .command)
}
}
var commands: some Commands {
CommandGroup(replacing: .appSettings) {
Button("Settings…") {
SettingsWindowController.shared.showWindow()
}
}
}
}
extension AppDelegate {
static var shared: AppDelegate? {
NSApplication.shared.delegate as? AppDelegate
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
var statusItem: NSStatusItem?
var windows: [NSScreen: NSWindow] = [:]
var viewModels: [NSScreen: DynamicIslandViewModel] = [:]
var window: NSWindow?
let vm: DynamicIslandViewModel = .init()
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
var whatsNewWindow: NSWindow?
var timer: Timer?
let calendarManager = CalendarManager.shared
let webcamManager = WebcamManager.shared
let dndManager = DoNotDisturbManager.shared // NEW: DND detection
let bluetoothAudioManager = BluetoothAudioManager.shared // NEW: Bluetooth audio detection
let idleAnimationManager = IdleAnimationManager.shared // NEW: Custom idle animations
let downloadManager = DownloadManager.shared // NEW: Chromium downloads detection
let lockScreenPanelManager = LockScreenPanelManager.shared // NEW: Lock screen music panel
let systemTimerBridge = SystemTimerBridge.shared
var closeNotchWorkItem: DispatchWorkItem?
private var previousScreens: [NSScreen]?
private var onboardingWindowController: NSWindowController?
private var cancellables = Set<AnyCancellable>()
private var windowsHiddenForLock = false
private var optionalShortcutHandlersRegistered = false
// Debouncing mechanism for window size updates
private var windowSizeUpdateWorkItem: DispatchWorkItem?
// let calendarManager = CalendarManager.shared
// let webcamManager = WebcamManager.shared
// var closeNotchWorkItem: DispatchWorkItem?
// private var previousScreens: [NSScreen]?
// private var onboardingWindowController: NSWindowController?
// private var cancellables = Set<AnyCancellable>()
//
// // Debouncing mechanism for window size updates
// private var windowSizeUpdateWorkItem: DispatchWorkItem?
private func debouncedUpdateWindowSize() {
// Cancel any existing work item
windowSizeUpdateWorkItem?.cancel()
// Create new work item with delay
let workItem = DispatchWorkItem { [weak self] in
self?.updateWindowSizeIfNeeded()
}
// Store reference and schedule
windowSizeUpdateWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15, execute: workItem)
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return false
}
func applicationWillTerminate(_ notification: Notification) {
// Cancel any pending window size updates
windowSizeUpdateWorkItem?.cancel()
NotificationCenter.default.removeObserver(self)
}
@objc func onScreenLocked(_: Notification) {
print("Screen locked")
hideWindowsForLock()
}
@objc func onScreenUnlocked(_: Notification) {
print("Screen unlocked")
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
guard let self = self else { return }
self.restoreWindowsAfterLock()
self.adjustWindowPosition(changeAlpha: true)
}
}
private func hideWindowsForLock() {
guard !windowsHiddenForLock else { return }
windowsHiddenForLock = true
if Defaults[.showOnAllDisplays] {
for window in windows.values {
window.alphaValue = 0
window.orderOut(nil)
}
} else if let window = window {
window.alphaValue = 0
window.orderOut(nil)
}
}
private func restoreWindowsAfterLock() {
guard windowsHiddenForLock else { return }
windowsHiddenForLock = false
if Defaults[.showOnAllDisplays] {
for window in windows.values {
window.orderFrontRegardless()
window.alphaValue = 1
}
} else if let window = window {
window.orderFrontRegardless()
window.alphaValue = 1
}
}
private func cleanupWindows(shouldInvert: Bool = false) {
if shouldInvert ? !Defaults[.showOnAllDisplays] : Defaults[.showOnAllDisplays] {
for window in windows.values {
window.close()
NotchSpaceManager.shared.notchSpace.windows.remove(window)
}
windows.removeAll()
viewModels.removeAll()
} else if let window = window {
window.close()
NotchSpaceManager.shared.notchSpace.windows.remove(window)
self.window = nil
}
}
private func createDynamicIslandWindow(for screen: NSScreen, with viewModel: DynamicIslandViewModel)
-> NSWindow
{
// Use the current required size instead of always using openNotchSize
let requiredSize = calculateRequiredNotchSize()
let window = DynamicIslandWindow(
contentRect: NSRect(
x: 0, y: 0, width: requiredSize.width, height: requiredSize.height),
styleMask: [.borderless, .nonactivatingPanel, .utilityWindow, .hudWindow],
backing: .buffered,
defer: false
)
window.contentView = NSHostingView(
rootView: ContentView()
.environmentObject(viewModel)
.environmentObject(webcamManager)
//.moveToSky()
)
window.orderFrontRegardless()
NotchSpaceManager.shared.notchSpace.windows.insert(window)
//SkyLightOperator.shared.delegateWindow(window)
return window
}
private func positionWindow(_ window: NSWindow, on screen: NSScreen, changeAlpha: Bool = false)
{
if changeAlpha {
window.alphaValue = 0
}
// Use the same centering logic as updateWindowSizeIfNeeded()
let screenFrame = screen.frame
let centerX = screenFrame.origin.x + (screenFrame.width / 2)
let newX = centerX - (window.frame.width / 2)
let newY = screenFrame.origin.y + screenFrame.height - window.frame.height
window.setFrame(NSRect(
x: newX,
y: newY,
width: window.frame.width,
height: window.frame.height
), display: false)
if changeAlpha {
window.alphaValue = 1
}
}
private func updateWindowSizeIfNeeded() {
// Calculate required size based on current state
let requiredSize = calculateRequiredNotchSize()
let animateResize = shouldAnimateResize(for: requiredSize)
resizeWindows(to: requiredSize, animated: animateResize, force: false)
}
private func calculateRequiredNotchSize() -> CGSize {
// Check if inline sneak peek is showing and notch is closed
let isInlineSneakPeekActive = vm.notchState == .closed &&
coordinator.expandingView.show &&
(coordinator.expandingView.type == .music || coordinator.expandingView.type == .timer) &&
Defaults[.enableSneakPeek] &&
Defaults[.sneakPeekStyles] == .inline
// If inline sneak peek is active, use a wider width to accommodate the expanded content
if isInlineSneakPeekActive {
// Calculate required width for inline sneak peek:
// Album art (~32) + Middle section (380) + Visualizer (~32) + padding = ~450
let inlineSneakPeekWidth: CGFloat = 450
return CGSize(width: inlineSneakPeekWidth, height: vm.effectiveClosedNotchHeight)
}
// Use minimalistic or normal size based on settings
var baseSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize
// Use a consistent height for different view types
if coordinator.currentView == .timer {
baseSize.height = 250 // Extra space for timer presets
} else if coordinator.currentView == .notes || coordinator.currentView == .clipboard {
let preferredHeight = coordinator.notesLayoutState.preferredHeight
baseSize.height = max(baseSize.height, preferredHeight)
}
let adjustedContentSize = statsAdjustedNotchSize(
from: baseSize,
isStatsTabActive: coordinator.currentView == .stats,
secondRowProgress: coordinator.statsSecondRowExpansion
)
return addShadowPadding(
to: adjustedContentSize,
isMinimalistic: Defaults[.enableMinimalisticUI]
)
}
func ensureWindowSize(_ size: CGSize, animated: Bool, force: Bool = false) {
resizeWindows(to: size, animated: animated, force: force)
}
private func resizeWindows(to size: CGSize, animated: Bool, force: Bool) {
guard size.width > 0, size.height > 0 else { return }
if Defaults[.showOnAllDisplays] {
for (screen, window) in windows {
if force || window.frame.size != size {
resizeWindow(window, on: screen, to: size, animated: animated)
}
}
} else if let window {
let screen = window.screen ?? NSScreen.screens.first { $0.frame.intersects(window.frame) } ?? NSScreen.main ?? NSScreen.screens.first
guard let screen else { return }
if force || window.frame.size != size {
resizeWindow(window, on: screen, to: size, animated: animated)
}
}
}
private func resizeWindow(_ window: NSWindow, on screen: NSScreen, to size: CGSize, animated: Bool) {
let screenFrame = screen.frame
let centerX = screenFrame.midX
let newX = centerX - (size.width / 2)
let newY = screenFrame.origin.y + screenFrame.height - size.height
let targetFrame = NSRect(x: newX, y: newY, width: size.width, height: size.height)
if animated {
NSAnimationContext.runAnimationGroup { context in
context.duration = 0.25
context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
context.allowsImplicitAnimation = true
window.animator().setFrame(targetFrame, display: true)
}
} else {
window.setFrame(targetFrame, display: true)
}
}
private func shouldAnimateResize(for newSize: CGSize) -> Bool {
if Defaults[.enableMinimalisticUI] && !ReminderLiveActivityManager.shared.activeWindowReminders.isEmpty {
return false
}
return true
}
func applicationDidFinishLaunching(_ notification: Notification) {
LockScreenLiveActivityWindowManager.shared.configure(viewModel: vm)
LockScreenManager.shared.configure(viewModel: vm)
// Migrate legacy progress bar settings
Defaults.Keys.migrateProgressBarStyle()
Defaults.Keys.migrateMusicAuxControls()
Defaults.Keys.migrateMusicControlSlots()
// Initialize idle animations (load bundled + built-in face)
idleAnimationManager.initializeDefaultAnimations()
// Setup SystemHUD Manager
SystemHUDManager.shared.setup(coordinator: coordinator)
// Setup ScreenRecording Manager
if Defaults[.enableScreenRecordingDetection] {
ScreenRecordingManager.shared.startMonitoring()
}
// Setup Do Not Disturb Manager
if Defaults[.enableDoNotDisturbDetection] {
dndManager.startMonitoring()
}
// Setup Privacy Indicator Manager (camera and microphone monitoring)
PrivacyIndicatorManager.shared.startMonitoring()
// Observe tab changes - use immediate updates to prevent clipping
coordinator.$currentView.sink { [weak self] newView in
self?.updateWindowSizeIfNeeded()
}.store(in: &cancellables)
coordinator.$notesLayoutState
.removeDuplicates()
.sink { [weak self] _ in
self?.updateWindowSizeIfNeeded()
}
.store(in: &cancellables)
// Observe stats settings changes - use debounced updates
Defaults.publisher(.enableStatsFeature, options: []).sink { [weak self] _ in
self?.debouncedUpdateWindowSize()
}.store(in: &cancellables)
Defaults.publisher(.showCpuGraph, options: []).sink { [weak self] _ in
self?.debouncedUpdateWindowSize()
}.store(in: &cancellables)
Defaults.publisher(.showMemoryGraph, options: []).sink { [weak self] _ in
self?.debouncedUpdateWindowSize()
}.store(in: &cancellables)
Defaults.publisher(.showGpuGraph, options: []).sink { [weak self] _ in
self?.debouncedUpdateWindowSize()
}.store(in: &cancellables)
Defaults.publisher(.showNetworkGraph, options: []).sink { [weak self] _ in
self?.debouncedUpdateWindowSize()
}.store(in: &cancellables)
Defaults.publisher(.showDiskGraph, options: []).sink { [weak self] _ in
self?.debouncedUpdateWindowSize()
}.store(in: &cancellables)
Defaults.publisher(.openNotchWidth, options: []).sink { [weak self] _ in
self?.debouncedUpdateWindowSize()
}.store(in: &cancellables)
MemoryUsageMonitor.shared.startMonitoring()
ReminderLiveActivityManager.shared.$activeWindowReminders
.receive(on: RunLoop.main)
.sink { [weak self] _ in
self?.debouncedUpdateWindowSize()
}
.store(in: &cancellables)
TimerManager.shared.$activeSource
.combineLatest(TimerManager.shared.$isTimerActive)
.receive(on: RunLoop.main)
.sink { [weak self] _ in
self?.debouncedUpdateWindowSize()
}
.store(in: &cancellables)
Defaults.publisher(.enableShortcuts, options: []).sink { [weak self] change in
Task { @MainActor [weak self] in
guard let self else { return }
KeyboardShortcuts.isEnabled = change.newValue
self.updateFeatureShortcutAvailability()
}
}.store(in: &cancellables)
Defaults.publisher(.enableTimerFeature, options: []).sink { [weak self] _ in
Task { @MainActor [weak self] in
self?.updateFeatureShortcutAvailability()
}
}.store(in: &cancellables)
Defaults.publisher(.enableClipboardManager, options: []).sink { [weak self] _ in
Task { @MainActor [weak self] in
self?.updateFeatureShortcutAvailability()
}
}.store(in: &cancellables)
Defaults.publisher(.enableColorPickerFeature, options: []).sink { [weak self] _ in
Task { @MainActor [weak self] in
self?.updateFeatureShortcutAvailability()
}
}.store(in: &cancellables)
Defaults.publisher(.enableScreenAssistant, options: []).sink { [weak self] _ in
Task { @MainActor [weak self] in
self?.updateFeatureShortcutAvailability()
}
}.store(in: &cancellables)
// Observe minimalistic UI setting changes - trigger window resize
Defaults.publisher(.enableMinimalisticUI, options: []).sink { [weak self] change in
// Force sneak peek to standard mode when minimalistic UI is enabled
if change.newValue == true && Defaults[.sneakPeekStyles] != .standard {
Defaults[.sneakPeekStyles] = .standard
}
// Update window size IMMEDIATELY (no debouncing) to prevent position shift
self?.updateWindowSizeIfNeeded()
}.store(in: &cancellables)
// Observe screen recording settings changes
Defaults.publisher(.enableScreenRecordingDetection, options: []).sink { _ in
if Defaults[.enableScreenRecordingDetection] {
ScreenRecordingManager.shared.startMonitoring()
} else {
ScreenRecordingManager.shared.stopMonitoring()
}
}.store(in: &cancellables)
Defaults.publisher(.enableDoNotDisturbDetection, options: []).sink { [weak self] _ in
guard let self else { return }
if Defaults[.enableDoNotDisturbDetection] {
self.dndManager.startMonitoring()
} else {
self.dndManager.stopMonitoring()
}
}.store(in: &cancellables)
// Note: Polling setting removed - now uses event-driven private API detection only
NotificationCenter.default.addObserver(
self,
selector: #selector(screenConfigurationDidChange),
name: NSApplication.didChangeScreenParametersNotification,
object: nil
)
NotificationCenter.default.addObserver(
forName: Notification.Name.selectedScreenChanged, object: nil, queue: nil
) { [weak self] _ in
self?.adjustWindowPosition(changeAlpha: true)
}
NotificationCenter.default.addObserver(
forName: Notification.Name.notchHeightChanged, object: nil, queue: nil
) { [weak self] _ in
self?.adjustWindowPosition()
}
NotificationCenter.default.addObserver(
forName: Notification.Name.automaticallySwitchDisplayChanged, object: nil, queue: nil
) { [weak self] _ in
guard let self = self, let window = self.window else { return }
window.alphaValue =
self.coordinator.selectedScreen == self.coordinator.preferredScreen ? 1 : 0
}
NotificationCenter.default.addObserver(
forName: Notification.Name.showOnAllDisplaysChanged, object: nil, queue: nil
) { [weak self] _ in
guard let self = self else { return }
self.cleanupWindows(shouldInvert: true)
if !Defaults[.showOnAllDisplays] {
let viewModel = self.vm
let window = self.createDynamicIslandWindow(
for: NSScreen.main ?? NSScreen.screens.first!, with: viewModel)
self.window = window
self.adjustWindowPosition(changeAlpha: true)
} else {
self.adjustWindowPosition()
}
}
DistributedNotificationCenter.default().addObserver(
self, selector: #selector(onScreenLocked(_:)),
name: NSNotification.Name(rawValue: "com.apple.screenIsLocked"), object: nil)
DistributedNotificationCenter.default().addObserver(
self, selector: #selector(onScreenUnlocked(_:)),
name: NSNotification.Name(rawValue: "com.apple.screenIsUnlocked"), object: nil)
KeyboardShortcuts.onKeyDown(for: .toggleSneakPeek) { [weak self] in
guard let self = self else { return }
guard Defaults[.enableShortcuts] else { return }
self.coordinator.toggleSneakPeek(
status: !self.coordinator.sneakPeek.show,
type: .music,
duration: 3.0
)
}
KeyboardShortcuts.onKeyDown(for: .toggleNotchOpen) { [weak self] in
guard let self = self else { return }
guard Defaults[.enableShortcuts] else { return }
let mouseLocation = NSEvent.mouseLocation
var viewModel = self.vm
if Defaults[.showOnAllDisplays] {
for screen in NSScreen.screens {
if screen.frame.contains(mouseLocation) {
if let screenViewModel = self.viewModels[screen] {
viewModel = screenViewModel
break
}
}
}
}
self.closeNotchWorkItem?.cancel()
self.closeNotchWorkItem = nil
switch viewModel.notchState {
case .closed:
viewModel.open()
let workItem = DispatchWorkItem { [weak viewModel] in
viewModel?.close()
}
self.closeNotchWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0, execute: workItem)
case .open:
viewModel.close()
}
}
KeyboardShortcuts.isEnabled = Defaults[.enableShortcuts]
registerOptionalShortcutHandlers()
updateFeatureShortcutAvailability()
if !Defaults[.showOnAllDisplays] {
let viewModel = self.vm
let window = createDynamicIslandWindow(
for: NSScreen.main ?? NSScreen.screens.first!, with: viewModel)
self.window = window
adjustWindowPosition(changeAlpha: true)
} else {
adjustWindowPosition(changeAlpha: true)
}
if coordinator.firstLaunch {
DispatchQueue.main.async {
self.showOnboardingWindow()
}
playWelcomeSound()
}
previousScreens = NSScreen.screens
if Defaults[.enableLockScreenWeatherWidget] {
LockScreenWeatherManager.shared.prepareLocationAccess()
Task { @MainActor in
await LockScreenWeatherManager.shared.refresh(force: true)
}
}
// Warm up the lock screen timer widget manager so it can observe timer/default
// changes immediately instead of waiting for the first lock event.
let timerWidgetManager = LockScreenTimerWidgetManager.shared
timerWidgetManager.handleLockStateChange(isLocked: LockScreenManager.shared.currentLockStatus)
// Ensure the reminder widget mirrors live activity snapshots as soon as they exist.
_ = LockScreenReminderWidgetManager.shared
}
private func registerOptionalShortcutHandlers() {
guard !optionalShortcutHandlersRegistered else { return }
optionalShortcutHandlersRegistered = true
KeyboardShortcuts.onKeyDown(for: .startDemoTimer) {
guard Defaults[.enableShortcuts], Defaults[.enableTimerFeature] else { return }
TimerManager.shared.startDemoTimer(duration: 300)
}
KeyboardShortcuts.onKeyDown(for: .clipboardHistoryPanel) { [weak self] in
guard let self else { return }
guard Defaults[.enableShortcuts], Defaults[.enableClipboardManager] else { return }
if !ClipboardManager.shared.isMonitoring {
ClipboardManager.shared.startMonitoring()
}
switch Defaults[.clipboardDisplayMode] {
case .panel:
ClipboardPanelManager.shared.toggleClipboardPanel()
case .popover:
if vm.notchState == .closed {
vm.open()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
NotificationCenter.default.post(name: NSNotification.Name("ToggleClipboardPopover"), object: nil)
}
} else {
NotificationCenter.default.post(name: NSNotification.Name("ToggleClipboardPopover"), object: nil)
}
case .separateTab:
if vm.notchState == .closed {
vm.open()
coordinator.currentView = .notes
} else {
if coordinator.currentView == .notes {
vm.close()
} else {
coordinator.currentView = .notes
}
}
}
}
KeyboardShortcuts.onKeyDown(for: .colorPickerPanel) {
guard Defaults[.enableShortcuts], Defaults[.enableColorPickerFeature] else { return }
ColorPickerPanelManager.shared.toggleColorPickerPanel()
}
KeyboardShortcuts.onKeyDown(for: .screenAssistantPanel) { [weak self] in
guard let self else { return }
guard Defaults[.enableShortcuts], Defaults[.enableScreenAssistant] else { return }
switch Defaults[.screenAssistantDisplayMode] {
case .panel:
ScreenAssistantPanelManager.shared.toggleScreenAssistantPanel()
case .popover:
if vm.notchState == .closed {
vm.open()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
NotificationCenter.default.post(name: NSNotification.Name("ToggleScreenAssistantPopover"), object: nil)
}
} else {
NotificationCenter.default.post(name: NSNotification.Name("ToggleScreenAssistantPopover"), object: nil)
}
}
}
}
@MainActor
private func updateFeatureShortcutAvailability() {
updateShortcut(.startDemoTimer, isEnabled: Defaults[.enableShortcuts] && Defaults[.enableTimerFeature])
updateShortcut(.clipboardHistoryPanel, isEnabled: Defaults[.enableShortcuts] && Defaults[.enableClipboardManager])
updateShortcut(.colorPickerPanel, isEnabled: Defaults[.enableShortcuts] && Defaults[.enableColorPickerFeature])
updateShortcut(.screenAssistantPanel, isEnabled: Defaults[.enableShortcuts] && Defaults[.enableScreenAssistant])
}
@MainActor
private func updateShortcut(_ name: KeyboardShortcuts.Name, isEnabled: Bool) {
if isEnabled {
KeyboardShortcuts.enable(name)
} else {
KeyboardShortcuts.disable(name)
}
}
func playWelcomeSound() {
let audioPlayer = AudioPlayer()
audioPlayer.play(fileName: "dynamic", fileExtension: "m4a")
}
func deviceHasNotch() -> Bool {
if #available(macOS 12.0, *) {
for screen in NSScreen.screens {
if screen.safeAreaInsets.top > 0 {
return true
}
}
}
return false
}
@objc func screenConfigurationDidChange() {
let currentScreens = NSScreen.screens
let screensChanged =
currentScreens.count != previousScreens?.count
|| Set(currentScreens.map { $0.localizedName })
!= Set(previousScreens?.map { $0.localizedName } ?? [])
|| Set(currentScreens.map { $0.frame }) != Set(previousScreens?.map { $0.frame } ?? [])
previousScreens = currentScreens
if screensChanged {
DispatchQueue.main.async { [weak self] in
self?.cleanupWindows()
self?.adjustWindowPosition()
}
}
}
@objc func adjustWindowPosition(changeAlpha: Bool = false) {
if Defaults[.showOnAllDisplays] {
let currentScreens = Set(NSScreen.screens)
for screen in windows.keys where !currentScreens.contains(screen) {
if let window = windows[screen] {
window.close()
NotchSpaceManager.shared.notchSpace.windows.remove(window)
windows.removeValue(forKey: screen)
viewModels.removeValue(forKey: screen)
}
}
for screen in currentScreens {
if windows[screen] == nil {
let viewModel = DynamicIslandViewModel(screen: screen.localizedName)
let window = createDynamicIslandWindow(for: screen, with: viewModel)
windows[screen] = window
viewModels[screen] = viewModel
}
if let window = windows[screen], let viewModel = viewModels[screen] {
positionWindow(window, on: screen, changeAlpha: changeAlpha)
if viewModel.notchState == .closed {
viewModel.close()
}
}
}
} else {
let selectedScreen: NSScreen
if let preferredScreen = NSScreen.screens.first(where: {
$0.localizedName == coordinator.preferredScreen
}) {
coordinator.selectedScreen = coordinator.preferredScreen
selectedScreen = preferredScreen
} else if Defaults[.automaticallySwitchDisplay], let mainScreen = NSScreen.main {
coordinator.selectedScreen = mainScreen.localizedName
selectedScreen = mainScreen
} else {
if let window = window {
window.alphaValue = 0
}
return
}
vm.screen = selectedScreen.localizedName
vm.notchSize = getClosedNotchSize(screen: selectedScreen.localizedName)
if window == nil {
window = createDynamicIslandWindow(for: selectedScreen, with: vm)
}
if let window = window {
positionWindow(window, on: selectedScreen, changeAlpha: changeAlpha)
if vm.notchState == .closed {
vm.close()
}
}
}
}
@objc func togglePopover(_ sender: Any?) {
if window?.isVisible == true {
window?.orderOut(nil)
} else {
window?.orderFrontRegardless()
}
}
@objc func showMenu() {
statusItem?.menu?.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil)
}
@objc func quitAction() {
NSApplication.shared.terminate(nil)
}
private func showOnboardingWindow() {
if onboardingWindowController == nil {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 400, height: 600),
styleMask: [.titled, .fullSizeContentView],
backing: .buffered,
defer: false
)
window.center()
window.title = "Onboarding"
window.titlebarAppearsTransparent = true
window.titleVisibility = .hidden
window.contentView = NSHostingView(rootView: OnboardingView(
onFinish: {
window.orderOut(nil)
NSApp.setActivationPolicy(.accessory)
window.close()
NSApp.deactivate()
},
onOpenSettings: {
window.close()
SettingsWindowController.shared.showWindow()
}
))
window.isRestorable = false
window.identifier = NSUserInterfaceItemIdentifier("OnboardingWindow")
ScreenCaptureVisibilityManager.shared.register(window, scope: .panelsOnly)
onboardingWindowController = NSWindowController(window: window)
}
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
onboardingWindowController?.window?.makeKeyAndOrderFront(nil)
onboardingWindowController?.window?.orderFrontRegardless()
}
}
extension Notification.Name {
static let selectedScreenChanged = Notification.Name("SelectedScreenChanged")
static let notchHeightChanged = Notification.Name("NotchHeightChanged")
static let showOnAllDisplaysChanged = Notification.Name("showOnAllDisplaysChanged")
static let automaticallySwitchDisplayChanged = Notification.Name("automaticallySwitchDisplayChanged")
}
extension CGRect: @retroactive Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(origin.x)
hasher.combine(origin.y)
hasher.combine(size.width)
hasher.combine(size.height)
}
public static func == (lhs: CGRect, rhs: CGRect) -> Bool {
return lhs.origin == rhs.origin && lhs.size == rhs.size
}
}