diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index 04417c5..7d37ba9 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -781,6 +781,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { window.isRestorable = false window.identifier = NSUserInterfaceItemIdentifier("OnboardingWindow") + ScreenCaptureVisibilityManager.shared.register(window, scope: .panelsOnly) + onboardingWindowController = NSWindowController(window: window) } diff --git a/DynamicIsland/components/Clipboard/ClipboardPanel.swift b/DynamicIsland/components/Clipboard/ClipboardPanel.swift index dec51fb..b79f6ba 100644 --- a/DynamicIsland/components/Clipboard/ClipboardPanel.swift +++ b/DynamicIsland/components/Clipboard/ClipboardPanel.swift @@ -7,7 +7,6 @@ import AppKit import SwiftUI -import Defaults class ClipboardPanel: NSPanel { @@ -51,10 +50,8 @@ class ClipboardPanel: NSPanel { .stationary, .fullScreenAuxiliary // Float above full-screen apps ] - - // Apply screenshot protection - updateScreenshotProtection() - setupScreenshotProtectionObserver() + + ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly) // Accept mouse moved events for proper hover behavior acceptsMouseMovedEvents = true @@ -140,28 +137,6 @@ class ClipboardPanel: NSPanel { setFrameOrigin(NSPoint(x: xPosition, y: yPosition)) } - private func setupScreenshotProtectionObserver() { - // Observe changes to hidePanelsFromScreenCapture setting - Defaults.observe(.hidePanelsFromScreenCapture) { [weak self] change in - DispatchQueue.main.async { - self?.updateScreenshotProtection() - } - } - } - - private func updateScreenshotProtection() { - let shouldHide = Defaults[.hidePanelsFromScreenCapture] - - if shouldHide { - // Exclude from screenshots and screen recordings - self.sharingType = .none - print("🙈 ClipboardPanel: Protected from screenshots and screen recordings") - } else { - // Allow normal screenshot inclusion - self.sharingType = .readOnly - print("👁️ ClipboardPanel: Visible in screenshots and screen recordings") - } - } } struct ClipboardPanelView: View { diff --git a/DynamicIsland/components/ColorPicker/ColorPickerPanel.swift b/DynamicIsland/components/ColorPicker/ColorPickerPanel.swift index ad39a0c..1f2d609 100644 --- a/DynamicIsland/components/ColorPicker/ColorPickerPanel.swift +++ b/DynamicIsland/components/ColorPicker/ColorPickerPanel.swift @@ -93,9 +93,7 @@ class ColorPickerPanel: NSPanel { .fullScreenAuxiliary // Float above full-screen apps ] - // Apply screen capture hiding setting - updateScreenCaptureVisibility() - setupScreenCaptureObserver() + ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly) // Accept mouse moved events for proper hover behavior acceptsMouseMovedEvents = true @@ -161,27 +159,8 @@ class ColorPickerPanel: NSPanel { saveCurrentPosition() } - private func setupScreenCaptureObserver() { - // Observe changes to hidePanelsFromScreenCapture setting - Defaults.observe(.hidePanelsFromScreenCapture) { [weak self] change in - DispatchQueue.main.async { - self?.updateScreenCaptureVisibility() - } - } - } - - private func updateScreenCaptureVisibility() { - let shouldHide = Defaults[.hidePanelsFromScreenCapture] - - if shouldHide { - // Hide from screen capture and recording - self.sharingType = .none - print("🙈 ColorPickerPanel: Hidden from screen capture and recordings") - } else { - // Allow normal screen capture - self.sharingType = .readOnly - print("👁️ ColorPickerPanel: Visible in screen capture and recordings") - } + deinit { + ScreenCaptureVisibilityManager.shared.unregister(self) } } diff --git a/DynamicIsland/components/Notch/DynamicIslandWindow.swift b/DynamicIsland/components/Notch/DynamicIslandWindow.swift index 47e9bc9..5118ae7 100644 --- a/DynamicIsland/components/Notch/DynamicIslandWindow.swift +++ b/DynamicIsland/components/Notch/DynamicIslandWindow.swift @@ -38,6 +38,8 @@ class DynamicIslandWindow: NSPanel { isReleasedWhenClosed = false level = .mainMenu + 3 hasShadow = false + + ScreenCaptureVisibilityManager.shared.register(self, scope: .entireInterface) } override var canBecomeKey: Bool { diff --git a/DynamicIsland/components/ScreenAssistant/ChatPanels.swift b/DynamicIsland/components/ScreenAssistant/ChatPanels.swift index 3932db5..41dffde 100644 --- a/DynamicIsland/components/ScreenAssistant/ChatPanels.swift +++ b/DynamicIsland/components/ScreenAssistant/ChatPanels.swift @@ -47,9 +47,7 @@ class ChatMessagesPanel: NSPanel { .fullScreenAuxiliary ] - // Apply screen capture hiding setting - updateScreenCaptureVisibility() - setupScreenCaptureObserver() + ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly) acceptsMouseMovedEvents = true } @@ -78,27 +76,8 @@ class ChatMessagesPanel: NSPanel { setFrameOrigin(NSPoint(x: xPosition, y: yPosition)) } - private func setupScreenCaptureObserver() { - // Observe changes to hidePanelsFromScreenCapture setting - Defaults.observe(.hidePanelsFromScreenCapture) { [weak self] change in - DispatchQueue.main.async { - self?.updateScreenCaptureVisibility() - } - } - } - - private func updateScreenCaptureVisibility() { - let shouldHide = Defaults[.hidePanelsFromScreenCapture] - - if shouldHide { - // Hide from screen capture and recording - self.sharingType = .none - print("🙈 ChatMessagesPanel: Hidden from screen capture and recordings") - } else { - // Allow normal screen capture - self.sharingType = .readOnly - print("👁️ ChatMessagesPanel: Visible in screen capture and recordings") - } + deinit { + ScreenCaptureVisibilityManager.shared.unregister(self) } } @@ -152,9 +131,7 @@ class ChatInputPanel: NSPanel { .fullScreenAuxiliary ] - // Apply screen capture hiding setting - updateScreenCaptureVisibility() - setupScreenCaptureObserver() + ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly) acceptsMouseMovedEvents = true } @@ -183,27 +160,8 @@ class ChatInputPanel: NSPanel { setFrameOrigin(NSPoint(x: xPosition, y: yPosition)) } - private func setupScreenCaptureObserver() { - // Observe changes to hidePanelsFromScreenCapture setting - Defaults.observe(.hidePanelsFromScreenCapture) { [weak self] change in - DispatchQueue.main.async { - self?.updateScreenCaptureVisibility() - } - } - } - - private func updateScreenCaptureVisibility() { - let shouldHide = Defaults[.hidePanelsFromScreenCapture] - - if shouldHide { - // Hide from screen capture and recording - self.sharingType = .none - print("🙈 ChatInputPanel: Hidden from screen capture and recordings") - } else { - // Allow normal screen capture - self.sharingType = .readOnly - print("👁️ ChatInputPanel: Visible in screen capture and recordings") - } + deinit { + ScreenCaptureVisibilityManager.shared.unregister(self) } } @@ -786,45 +744,23 @@ struct ScreenshotPopoverBackground: NSViewRepresentable { func makeNSView(context: Context) -> NSView { let view = NSView() - // Apply screen capture protection like our other panels DispatchQueue.main.async { - if let window = view.window { - self.updateScreenCaptureVisibility(window: window) - self.setupScreenCaptureObserver(window: window) - } + guard let window = view.window else { return } + ScreenCaptureVisibilityManager.shared.register(window, scope: .panelsOnly) } return view } func updateNSView(_ nsView: NSView, context: Context) { - // Update screen capture protection when view updates if let window = nsView.window { - updateScreenCaptureVisibility(window: window) + ScreenCaptureVisibilityManager.shared.register(window, scope: .panelsOnly) } } - private func updateScreenCaptureVisibility(window: NSWindow) { - let shouldHide = Defaults[.hidePanelsFromScreenCapture] - - if shouldHide { - // Hide from screen capture and recording - window.sharingType = .none - print("🙈 ScreenshotPopover: Hidden from screen capture and recordings") - } else { - // Allow normal screen capture - window.sharingType = .readOnly - print("👁️ ScreenshotPopover: Visible in screen capture and recordings") - } - } - - private func setupScreenCaptureObserver(window: NSWindow) { - // Observe changes to hidePanelsFromScreenCapture setting - Defaults.observe(.hidePanelsFromScreenCapture) { [weak window] change in - DispatchQueue.main.async { - guard let window = window else { return } - self.updateScreenCaptureVisibility(window: window) - } + static func dismantleNSView(_ nsView: NSView, coordinator: ()) { + if let window = nsView.window { + ScreenCaptureVisibilityManager.shared.unregister(window) } } } \ No newline at end of file diff --git a/DynamicIsland/components/ScreenAssistant/ModelSelectionPanel.swift b/DynamicIsland/components/ScreenAssistant/ModelSelectionPanel.swift index 65e82ff..5995251 100644 --- a/DynamicIsland/components/ScreenAssistant/ModelSelectionPanel.swift +++ b/DynamicIsland/components/ScreenAssistant/ModelSelectionPanel.swift @@ -58,9 +58,7 @@ class ModelSelectionPanel: NSPanel { .fullScreenAuxiliary ] - // Apply screen capture hiding setting - updateScreenCaptureVisibility() - setupScreenCaptureObserver() + ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly) acceptsMouseMovedEvents = true } @@ -89,27 +87,8 @@ class ModelSelectionPanel: NSPanel { setFrameOrigin(NSPoint(x: xPosition, y: yPosition)) } - private func setupScreenCaptureObserver() { - // Observe changes to hidePanelsFromScreenCapture setting - Defaults.observe(.hidePanelsFromScreenCapture) { [weak self] change in - DispatchQueue.main.async { - self?.updateScreenCaptureVisibility() - } - } - } - - private func updateScreenCaptureVisibility() { - let shouldHide = Defaults[.hidePanelsFromScreenCapture] - - if shouldHide { - // Hide from screen capture and recording - self.sharingType = .none - print("🙈 ModelSelectionPanel: Hidden from screen capture and recordings") - } else { - // Allow normal screen capture - self.sharingType = .readOnly - print("👁️ ModelSelectionPanel: Visible in screen capture and recordings") - } + deinit { + ScreenCaptureVisibilityManager.shared.unregister(self) } } diff --git a/DynamicIsland/components/ScreenAssistant/ScreenAssistantPanel.swift b/DynamicIsland/components/ScreenAssistant/ScreenAssistantPanel.swift index 5c36558..064b153 100644 --- a/DynamicIsland/components/ScreenAssistant/ScreenAssistantPanel.swift +++ b/DynamicIsland/components/ScreenAssistant/ScreenAssistantPanel.swift @@ -57,9 +57,7 @@ class ScreenAssistantPanel: NSPanel { .fullScreenAuxiliary ] - // Apply screen capture hiding setting - updateScreenCaptureVisibility() - setupScreenCaptureObserver() + ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly) // Set minimal size since this is just a wrapper setContentSize(CGSize(width: 1, height: 1)) @@ -77,27 +75,8 @@ class ScreenAssistantPanel: NSPanel { super.close() } - private func setupScreenCaptureObserver() { - // Observe changes to hidePanelsFromScreenCapture setting - Defaults.observe(.hidePanelsFromScreenCapture) { [weak self] change in - DispatchQueue.main.async { - self?.updateScreenCaptureVisibility() - } - } - } - - private func updateScreenCaptureVisibility() { - let shouldHide = Defaults[.hidePanelsFromScreenCapture] - - if shouldHide { - // Hide from screen capture and recording - self.sharingType = .none - print("🙈 ScreenAssistantPanel: Hidden from screen capture and recordings") - } else { - // Allow normal screen capture - self.sharingType = .readOnly - print("👁️ ScreenAssistantPanel: Visible in screen capture and recordings") - } + deinit { + ScreenCaptureVisibilityManager.shared.unregister(self) } } diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 805b8f0..4e55b25 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -232,7 +232,7 @@ struct GeneralSettings: View { NotificationCenter.default.post(name: Notification.Name.automaticallySwitchDisplayChanged, object: nil) } .disabled(showOnAllDisplays) - Defaults.Toggle("Hide panels from screenshots & screen recordings", key: .hidePanelsFromScreenCapture) + Defaults.Toggle("Hide Dynamic Island during screenshots & recordings", key: .hideDynamicIslandFromScreenCapture) } header: { Text("System features") } diff --git a/DynamicIsland/components/Settings/SettingsWindowController.swift b/DynamicIsland/components/Settings/SettingsWindowController.swift index 4d17eec..2a40ddc 100644 --- a/DynamicIsland/components/Settings/SettingsWindowController.swift +++ b/DynamicIsland/components/Settings/SettingsWindowController.swift @@ -7,7 +7,6 @@ import AppKit import SwiftUI -import Defaults import Sparkle class SettingsWindowController: NSWindowController { @@ -65,9 +64,7 @@ class SettingsWindowController: NSWindowController { // Handle window closing window.delegate = self - // Apply screen capture hiding setting - updateScreenCaptureVisibility() - setupScreenCaptureObserver() + ScreenCaptureVisibilityManager.shared.register(window, scope: .panelsOnly) } func showWindow() { @@ -108,26 +105,9 @@ class SettingsWindowController: NSWindowController { NSApp.setActivationPolicy(.accessory) } - private func setupScreenCaptureObserver() { - // Observe changes to hidePanelsFromScreenCapture setting - Defaults.observe(.hidePanelsFromScreenCapture) { [weak self] change in - DispatchQueue.main.async { - self?.updateScreenCaptureVisibility() - } - } - } - - private func updateScreenCaptureVisibility() { - let shouldHide = Defaults[.hidePanelsFromScreenCapture] - - if shouldHide { - // Hide from screen capture and recording - window?.sharingType = .none - print("🙈 SettingsWindow: Hidden from screen capture and recordings") - } else { - // Allow normal screen capture - window?.sharingType = .readOnly - print("👁️ SettingsWindow: Visible in screen capture and recordings") + deinit { + if let window = window { + ScreenCaptureVisibilityManager.shared.unregister(window) } } } diff --git a/DynamicIsland/helpers/ScreenCaptureVisibilityManager.swift b/DynamicIsland/helpers/ScreenCaptureVisibilityManager.swift new file mode 100644 index 0000000..71993ca --- /dev/null +++ b/DynamicIsland/helpers/ScreenCaptureVisibilityManager.swift @@ -0,0 +1,51 @@ +import AppKit +import Combine +import Defaults + +enum ScreenCaptureScope: Int { + case panelsOnly + case entireInterface +} + +final class ScreenCaptureVisibilityManager { + static let shared = ScreenCaptureVisibilityManager() + + private let scopedWindows = NSMapTable(keyOptions: .weakMemory, valueOptions: .strongMemory) + private var cancellables = Set() + + private init() { + let interfacePublisher = Defaults.publisher(.hideDynamicIslandFromScreenCapture) + .map { _ in () } + .eraseToAnyPublisher() + + interfacePublisher + .receive(on: RunLoop.main) + .sink { [weak self] in + self?.updateAllWindows() + } + .store(in: &cancellables) + } + + func register(_ window: NSWindow, scope: ScreenCaptureScope) { + scopedWindows.setObject(NSNumber(value: scope.rawValue), forKey: window) + applyVisibility(to: window, scope: scope) + } + + func unregister(_ window: NSWindow) { + scopedWindows.removeObject(forKey: window) + } + + private func updateAllWindows() { + guard let windows = scopedWindows.keyEnumerator().allObjects as? [NSWindow] else { return } + for window in windows { + guard let raw = scopedWindows.object(forKey: window)?.intValue, + let scope = ScreenCaptureScope(rawValue: raw) else { continue } + applyVisibility(to: window, scope: scope) + } + } + + private func applyVisibility(to window: NSWindow, scope _: ScreenCaptureScope) { + let shouldHide = Defaults[.hideDynamicIslandFromScreenCapture] + window.sharingType = shouldHide ? .none : .readOnly + } +} diff --git a/DynamicIsland/managers/ClipboardWindowManager.swift b/DynamicIsland/managers/ClipboardWindowManager.swift index b76941e..727f663 100644 --- a/DynamicIsland/managers/ClipboardWindowManager.swift +++ b/DynamicIsland/managers/ClipboardWindowManager.swift @@ -60,9 +60,12 @@ class ClipboardWindowManager: ObservableObject { window.contentView = hostingView // Handle window closing - window.delegate = WindowDelegate { [weak self] in + window.delegate = WindowDelegate { [weak self] window in + ScreenCaptureVisibilityManager.shared.unregister(window) self?.clipboardWindow = nil } + + ScreenCaptureVisibilityManager.shared.register(window, scope: .panelsOnly) self.clipboardWindow = window window.makeKeyAndOrderFront(nil) @@ -88,21 +91,17 @@ class ClipboardWindowManager: ObservableObject { } private class WindowDelegate: NSObject, NSWindowDelegate { - private let onClose: () -> Void - - init(onClose: @escaping () -> Void) { + private let onClose: (NSWindow) -> Void + + init(onClose: @escaping (NSWindow) -> Void) { self.onClose = onClose super.init() } - - func windowWillClose(_ notification: Notification) { - onClose() - } - + func windowShouldClose(_ sender: NSWindow) -> Bool { // Hide instead of close when user clicks close button sender.orderOut(nil) - onClose() + onClose(sender) return false } } diff --git a/DynamicIsland/managers/LockScreenLiveActivityWindowManager.swift b/DynamicIsland/managers/LockScreenLiveActivityWindowManager.swift index 2a50070..ef35ac4 100644 --- a/DynamicIsland/managers/LockScreenLiveActivityWindowManager.swift +++ b/DynamicIsland/managers/LockScreenLiveActivityWindowManager.swift @@ -70,6 +70,8 @@ class LockScreenLiveActivityWindowManager { window.alphaValue = 0 window.animationBehavior = .none + ScreenCaptureVisibilityManager.shared.register(window, scope: .entireInterface) + self.window = window self.hasDelegated = false return window diff --git a/DynamicIsland/managers/LockScreenPanelManager.swift b/DynamicIsland/managers/LockScreenPanelManager.swift index 3e6eda8..e30d8bd 100644 --- a/DynamicIsland/managers/LockScreenPanelManager.swift +++ b/DynamicIsland/managers/LockScreenPanelManager.swift @@ -81,6 +81,8 @@ class LockScreenPanelManager { newWindow.isMovable = false newWindow.hasShadow = false + ScreenCaptureVisibilityManager.shared.register(newWindow, scope: .entireInterface) + panelWindow = newWindow window = newWindow hasDelegated = false diff --git a/DynamicIsland/managers/LockScreenReminderWidgetPanelManager.swift b/DynamicIsland/managers/LockScreenReminderWidgetPanelManager.swift index 5c66f75..4a82398 100644 --- a/DynamicIsland/managers/LockScreenReminderWidgetPanelManager.swift +++ b/DynamicIsland/managers/LockScreenReminderWidgetPanelManager.swift @@ -70,6 +70,8 @@ final class LockScreenReminderWidgetPanelManager { newWindow.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel())) newWindow.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary] + ScreenCaptureVisibilityManager.shared.register(newWindow, scope: .entireInterface) + window = newWindow if !hasDelegated { SkyLightOperator.shared.delegateWindow(newWindow) diff --git a/DynamicIsland/managers/LockScreenWeatherPanelManager.swift b/DynamicIsland/managers/LockScreenWeatherPanelManager.swift index f95df89..f4112f9 100644 --- a/DynamicIsland/managers/LockScreenWeatherPanelManager.swift +++ b/DynamicIsland/managers/LockScreenWeatherPanelManager.swift @@ -78,6 +78,8 @@ final class LockScreenWeatherPanelManager { newWindow.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel())) newWindow.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary] + ScreenCaptureVisibilityManager.shared.register(newWindow, scope: .entireInterface) + window = newWindow if !hasDelegated { SkyLightOperator.shared.delegateWindow(newWindow) diff --git a/DynamicIsland/managers/MusicControlWindowManager.swift b/DynamicIsland/managers/MusicControlWindowManager.swift index a7857b7..f72d59e 100644 --- a/DynamicIsland/managers/MusicControlWindowManager.swift +++ b/DynamicIsland/managers/MusicControlWindowManager.swift @@ -166,6 +166,8 @@ final class MusicControlWindowManager { window.isMovable = false window.alphaValue = 0 + ScreenCaptureVisibilityManager.shared.register(window, scope: .entireInterface) + self.window = window self.hasDelegated = false return window diff --git a/DynamicIsland/managers/TimerControlWindowManager.swift b/DynamicIsland/managers/TimerControlWindowManager.swift index 5eed75f..eeb2378 100644 --- a/DynamicIsland/managers/TimerControlWindowManager.swift +++ b/DynamicIsland/managers/TimerControlWindowManager.swift @@ -145,6 +145,8 @@ final class TimerControlWindowManager { window.isMovable = false window.alphaValue = 0 + ScreenCaptureVisibilityManager.shared.register(window, scope: .entireInterface) + self.window = window self.hasDelegated = false return window diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 5ca47b7..c2677ef 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -383,7 +383,7 @@ extension Defaults.Keys { static let showOnAllDisplays = Key("showOnAllDisplays", default: false) static let automaticallySwitchDisplay = Key("automaticallySwitchDisplay", default: true) static let releaseName = Key("releaseName", default: "alpha 0.0.1") - static let hidePanelsFromScreenCapture = Key("hideFromScreenCapture", default: false) + static let hideDynamicIslandFromScreenCapture = Key("hideDynamicIslandFromScreenCapture", default: false) // MARK: Behavior static let minimumHoverDuration = Key("minimumHoverDuration", default: 0.3)