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.identifier = NSUserInterfaceItemIdentifier("OnboardingWindow")
ScreenCaptureVisibilityManager.shared.register(window, scope: .panelsOnly)
onboardingWindowController = NSWindowController(window: window)
}

View file

@ -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 {

View file

@ -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)
}
}

View file

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

View file

@ -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)
}
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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")
}

View file

@ -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)
}
}
}

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
// 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
}
}

View file

@ -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

View file

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

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -383,7 +383,7 @@ extension Defaults.Keys {
static let showOnAllDisplays = Key<Bool>("showOnAllDisplays", default: false)
static let automaticallySwitchDisplay = Key<Bool>("automaticallySwitchDisplay", default: true)
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
static let minimumHoverDuration = Key<TimeInterval>("minimumHoverDuration", default: 0.3)