BugFix: Hide Notch from Screen Recordings

This commit is contained in:
Hariharan Mudaliar 2025-11-26 15:48:07 +05:30
commit 18f68f56ea
18 changed files with 105 additions and 211 deletions

View file

@ -781,6 +781,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
window.isRestorable = false window.isRestorable = false
window.identifier = NSUserInterfaceItemIdentifier("OnboardingWindow") window.identifier = NSUserInterfaceItemIdentifier("OnboardingWindow")
ScreenCaptureVisibilityManager.shared.register(window, scope: .panelsOnly)
onboardingWindowController = NSWindowController(window: window) onboardingWindowController = NSWindowController(window: window)
} }

View file

@ -7,7 +7,6 @@
import AppKit import AppKit
import SwiftUI import SwiftUI
import Defaults
class ClipboardPanel: NSPanel { class ClipboardPanel: NSPanel {
@ -51,10 +50,8 @@ class ClipboardPanel: NSPanel {
.stationary, .stationary,
.fullScreenAuxiliary // Float above full-screen apps .fullScreenAuxiliary // Float above full-screen apps
] ]
// Apply screenshot protection ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly)
updateScreenshotProtection()
setupScreenshotProtectionObserver()
// Accept mouse moved events for proper hover behavior // Accept mouse moved events for proper hover behavior
acceptsMouseMovedEvents = true acceptsMouseMovedEvents = true
@ -140,28 +137,6 @@ class ClipboardPanel: NSPanel {
setFrameOrigin(NSPoint(x: xPosition, y: yPosition)) 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 { struct ClipboardPanelView: View {

View file

@ -93,9 +93,7 @@ class ColorPickerPanel: NSPanel {
.fullScreenAuxiliary // Float above full-screen apps .fullScreenAuxiliary // Float above full-screen apps
] ]
// Apply screen capture hiding setting ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly)
updateScreenCaptureVisibility()
setupScreenCaptureObserver()
// Accept mouse moved events for proper hover behavior // Accept mouse moved events for proper hover behavior
acceptsMouseMovedEvents = true acceptsMouseMovedEvents = true
@ -161,27 +159,8 @@ class ColorPickerPanel: NSPanel {
saveCurrentPosition() saveCurrentPosition()
} }
private func setupScreenCaptureObserver() { deinit {
// Observe changes to hidePanelsFromScreenCapture setting ScreenCaptureVisibilityManager.shared.unregister(self)
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")
}
} }
} }

View file

@ -38,6 +38,8 @@ class DynamicIslandWindow: NSPanel {
isReleasedWhenClosed = false isReleasedWhenClosed = false
level = .mainMenu + 3 level = .mainMenu + 3
hasShadow = false hasShadow = false
ScreenCaptureVisibilityManager.shared.register(self, scope: .entireInterface)
} }
override var canBecomeKey: Bool { override var canBecomeKey: Bool {

View file

@ -47,9 +47,7 @@ class ChatMessagesPanel: NSPanel {
.fullScreenAuxiliary .fullScreenAuxiliary
] ]
// Apply screen capture hiding setting ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly)
updateScreenCaptureVisibility()
setupScreenCaptureObserver()
acceptsMouseMovedEvents = true acceptsMouseMovedEvents = true
} }
@ -78,27 +76,8 @@ class ChatMessagesPanel: NSPanel {
setFrameOrigin(NSPoint(x: xPosition, y: yPosition)) setFrameOrigin(NSPoint(x: xPosition, y: yPosition))
} }
private func setupScreenCaptureObserver() { deinit {
// Observe changes to hidePanelsFromScreenCapture setting ScreenCaptureVisibilityManager.shared.unregister(self)
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")
}
} }
} }
@ -152,9 +131,7 @@ class ChatInputPanel: NSPanel {
.fullScreenAuxiliary .fullScreenAuxiliary
] ]
// Apply screen capture hiding setting ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly)
updateScreenCaptureVisibility()
setupScreenCaptureObserver()
acceptsMouseMovedEvents = true acceptsMouseMovedEvents = true
} }
@ -183,27 +160,8 @@ class ChatInputPanel: NSPanel {
setFrameOrigin(NSPoint(x: xPosition, y: yPosition)) setFrameOrigin(NSPoint(x: xPosition, y: yPosition))
} }
private func setupScreenCaptureObserver() { deinit {
// Observe changes to hidePanelsFromScreenCapture setting ScreenCaptureVisibilityManager.shared.unregister(self)
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")
}
} }
} }
@ -786,45 +744,23 @@ struct ScreenshotPopoverBackground: NSViewRepresentable {
func makeNSView(context: Context) -> NSView { func makeNSView(context: Context) -> NSView {
let view = NSView() let view = NSView()
// Apply screen capture protection like our other panels
DispatchQueue.main.async { DispatchQueue.main.async {
if let window = view.window { guard let window = view.window else { return }
self.updateScreenCaptureVisibility(window: window) ScreenCaptureVisibilityManager.shared.register(window, scope: .panelsOnly)
self.setupScreenCaptureObserver(window: window)
}
} }
return view return view
} }
func updateNSView(_ nsView: NSView, context: Context) { func updateNSView(_ nsView: NSView, context: Context) {
// Update screen capture protection when view updates
if let window = nsView.window { if let window = nsView.window {
updateScreenCaptureVisibility(window: window) ScreenCaptureVisibilityManager.shared.register(window, scope: .panelsOnly)
} }
} }
private func updateScreenCaptureVisibility(window: NSWindow) { static func dismantleNSView(_ nsView: NSView, coordinator: ()) {
let shouldHide = Defaults[.hidePanelsFromScreenCapture] if let window = nsView.window {
ScreenCaptureVisibilityManager.shared.unregister(window)
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)
}
} }
} }
} }

View file

@ -58,9 +58,7 @@ class ModelSelectionPanel: NSPanel {
.fullScreenAuxiliary .fullScreenAuxiliary
] ]
// Apply screen capture hiding setting ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly)
updateScreenCaptureVisibility()
setupScreenCaptureObserver()
acceptsMouseMovedEvents = true acceptsMouseMovedEvents = true
} }
@ -89,27 +87,8 @@ class ModelSelectionPanel: NSPanel {
setFrameOrigin(NSPoint(x: xPosition, y: yPosition)) setFrameOrigin(NSPoint(x: xPosition, y: yPosition))
} }
private func setupScreenCaptureObserver() { deinit {
// Observe changes to hidePanelsFromScreenCapture setting ScreenCaptureVisibilityManager.shared.unregister(self)
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")
}
} }
} }

View file

@ -57,9 +57,7 @@ class ScreenAssistantPanel: NSPanel {
.fullScreenAuxiliary .fullScreenAuxiliary
] ]
// Apply screen capture hiding setting ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly)
updateScreenCaptureVisibility()
setupScreenCaptureObserver()
// Set minimal size since this is just a wrapper // Set minimal size since this is just a wrapper
setContentSize(CGSize(width: 1, height: 1)) setContentSize(CGSize(width: 1, height: 1))
@ -77,27 +75,8 @@ class ScreenAssistantPanel: NSPanel {
super.close() super.close()
} }
private func setupScreenCaptureObserver() { deinit {
// Observe changes to hidePanelsFromScreenCapture setting ScreenCaptureVisibilityManager.shared.unregister(self)
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")
}
} }
} }

View file

@ -232,7 +232,7 @@ struct GeneralSettings: View {
NotificationCenter.default.post(name: Notification.Name.automaticallySwitchDisplayChanged, object: nil) NotificationCenter.default.post(name: Notification.Name.automaticallySwitchDisplayChanged, object: nil)
} }
.disabled(showOnAllDisplays) .disabled(showOnAllDisplays)
Defaults.Toggle("Hide panels from screenshots & screen recordings", key: .hidePanelsFromScreenCapture) Defaults.Toggle("Hide Dynamic Island during screenshots & recordings", key: .hideDynamicIslandFromScreenCapture)
} header: { } header: {
Text("System features") Text("System features")
} }

View file

@ -7,7 +7,6 @@
import AppKit import AppKit
import SwiftUI import SwiftUI
import Defaults
import Sparkle import Sparkle
class SettingsWindowController: NSWindowController { class SettingsWindowController: NSWindowController {
@ -65,9 +64,7 @@ class SettingsWindowController: NSWindowController {
// Handle window closing // Handle window closing
window.delegate = self window.delegate = self
// Apply screen capture hiding setting ScreenCaptureVisibilityManager.shared.register(window, scope: .panelsOnly)
updateScreenCaptureVisibility()
setupScreenCaptureObserver()
} }
func showWindow() { func showWindow() {
@ -108,26 +105,9 @@ class SettingsWindowController: NSWindowController {
NSApp.setActivationPolicy(.accessory) NSApp.setActivationPolicy(.accessory)
} }
private func setupScreenCaptureObserver() { deinit {
// Observe changes to hidePanelsFromScreenCapture setting if let window = window {
Defaults.observe(.hidePanelsFromScreenCapture) { [weak self] change in ScreenCaptureVisibilityManager.shared.unregister(window)
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")
} }
} }
} }

View file

@ -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<NSWindow, NSNumber>(keyOptions: .weakMemory, valueOptions: .strongMemory)
private var cancellables = Set<AnyCancellable>()
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
}
}

View file

@ -60,9 +60,12 @@ class ClipboardWindowManager: ObservableObject {
window.contentView = hostingView window.contentView = hostingView
// Handle window closing // Handle window closing
window.delegate = WindowDelegate { [weak self] in window.delegate = WindowDelegate { [weak self] window in
ScreenCaptureVisibilityManager.shared.unregister(window)
self?.clipboardWindow = nil self?.clipboardWindow = nil
} }
ScreenCaptureVisibilityManager.shared.register(window, scope: .panelsOnly)
self.clipboardWindow = window self.clipboardWindow = window
window.makeKeyAndOrderFront(nil) window.makeKeyAndOrderFront(nil)
@ -88,21 +91,17 @@ class ClipboardWindowManager: ObservableObject {
} }
private class WindowDelegate: NSObject, NSWindowDelegate { private class WindowDelegate: NSObject, NSWindowDelegate {
private let onClose: () -> Void private let onClose: (NSWindow) -> Void
init(onClose: @escaping () -> Void) { init(onClose: @escaping (NSWindow) -> Void) {
self.onClose = onClose self.onClose = onClose
super.init() super.init()
} }
func windowWillClose(_ notification: Notification) {
onClose()
}
func windowShouldClose(_ sender: NSWindow) -> Bool { func windowShouldClose(_ sender: NSWindow) -> Bool {
// Hide instead of close when user clicks close button // Hide instead of close when user clicks close button
sender.orderOut(nil) sender.orderOut(nil)
onClose() onClose(sender)
return false return false
} }
} }

View file

@ -70,6 +70,8 @@ class LockScreenLiveActivityWindowManager {
window.alphaValue = 0 window.alphaValue = 0
window.animationBehavior = .none window.animationBehavior = .none
ScreenCaptureVisibilityManager.shared.register(window, scope: .entireInterface)
self.window = window self.window = window
self.hasDelegated = false self.hasDelegated = false
return window return window

View file

@ -81,6 +81,8 @@ class LockScreenPanelManager {
newWindow.isMovable = false newWindow.isMovable = false
newWindow.hasShadow = false newWindow.hasShadow = false
ScreenCaptureVisibilityManager.shared.register(newWindow, scope: .entireInterface)
panelWindow = newWindow panelWindow = newWindow
window = newWindow window = newWindow
hasDelegated = false hasDelegated = false

View file

@ -70,6 +70,8 @@ final class LockScreenReminderWidgetPanelManager {
newWindow.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel())) newWindow.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()))
newWindow.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary] newWindow.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
ScreenCaptureVisibilityManager.shared.register(newWindow, scope: .entireInterface)
window = newWindow window = newWindow
if !hasDelegated { if !hasDelegated {
SkyLightOperator.shared.delegateWindow(newWindow) SkyLightOperator.shared.delegateWindow(newWindow)

View file

@ -78,6 +78,8 @@ final class LockScreenWeatherPanelManager {
newWindow.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel())) newWindow.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()))
newWindow.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary] newWindow.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
ScreenCaptureVisibilityManager.shared.register(newWindow, scope: .entireInterface)
window = newWindow window = newWindow
if !hasDelegated { if !hasDelegated {
SkyLightOperator.shared.delegateWindow(newWindow) SkyLightOperator.shared.delegateWindow(newWindow)

View file

@ -166,6 +166,8 @@ final class MusicControlWindowManager {
window.isMovable = false window.isMovable = false
window.alphaValue = 0 window.alphaValue = 0
ScreenCaptureVisibilityManager.shared.register(window, scope: .entireInterface)
self.window = window self.window = window
self.hasDelegated = false self.hasDelegated = false
return window return window

View file

@ -145,6 +145,8 @@ final class TimerControlWindowManager {
window.isMovable = false window.isMovable = false
window.alphaValue = 0 window.alphaValue = 0
ScreenCaptureVisibilityManager.shared.register(window, scope: .entireInterface)
self.window = window self.window = window
self.hasDelegated = false self.hasDelegated = false
return window return window

View file

@ -383,7 +383,7 @@ extension Defaults.Keys {
static let showOnAllDisplays = Key<Bool>("showOnAllDisplays", default: false) static let showOnAllDisplays = Key<Bool>("showOnAllDisplays", default: false)
static let automaticallySwitchDisplay = Key<Bool>("automaticallySwitchDisplay", default: true) static let automaticallySwitchDisplay = Key<Bool>("automaticallySwitchDisplay", default: true)
static let releaseName = Key<String>("releaseName", default: "alpha 0.0.1") static let releaseName = Key<String>("releaseName", default: "alpha 0.0.1")
static let hidePanelsFromScreenCapture = Key<Bool>("hideFromScreenCapture", default: false) static let hideDynamicIslandFromScreenCapture = Key<Bool>("hideDynamicIslandFromScreenCapture", default: false)
// MARK: Behavior // MARK: Behavior
static let minimumHoverDuration = Key<TimeInterval>("minimumHoverDuration", default: 0.3) static let minimumHoverDuration = Key<TimeInterval>("minimumHoverDuration", default: 0.3)