fix: thumbnails could sometimes be the wrong size
* fixes issues when the user has monitors with different DPI * fixes issues where screenshots after a window is resized could be wrong (because of cached SCWindow having stale frame.size)
This commit is contained in:
1 parent
9ed8a199b0
commit
9d0b947a48
8 files changed
+217
-194
No files matched your search
@@ -73,6 +73,7 @@
|
||||
5F706EC02BA448B1004D542D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5F706EBE2BA448B1004D542D /* Localizable.strings */; };
|
||||
5F706EC32BA448D8004D542D /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5F706EC12BA448D8004D542D /* InfoPlist.strings */; };
|
||||
5F882F382EC3C63400110A9F /* CustomRecorderControlTestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8DC2A355E29516DD2FAF /* CustomRecorderControlTestable.swift */; };
|
||||
5F906A342F16A18500B99924 /* Screens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F906A332F16A18000B99924 /* Screens.swift */; };
|
||||
76D02BB22BFE7C9E0056008D /* Pods_alt_tab_macos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0712B3BEA2B3780398C0999 /* Pods_alt_tab_macos.framework */; };
|
||||
AA974BA929B7D84C0099A29E /* PreviewPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA974BA829B7D84C0099A29E /* PreviewPanel.swift */; };
|
||||
BF0C803A13C409E7F01FBCAB /* SkyLight.framework.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8B94D7994AB9007F6391 /* SkyLight.framework.swift */; };
|
||||
@@ -96,7 +97,6 @@
|
||||
BF0C8224CFD1ED511F3F0063 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C8D94228D90FE99D4A3C8 /* Localizable.strings */; };
|
||||
BF0C823630E3DEAC576FAFE6 /* menubar-0@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = BF0C88BF67CF4D71002A60EB /* menubar-0@2x.png */; };
|
||||
BF0C8249228867A3188E13F3 /* SystemAppearanceEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C869E544F968051715452 /* SystemAppearanceEvents.swift */; };
|
||||
BF0C824E829664B6610C7BAC /* table-of-contents.js in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8EE00994AC6C8323EE12 /* table-of-contents.js */; };
|
||||
BF0C826C7F8C5F68732FD75B /* DebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C80F8FAA00FF3D9682F26 /* DebugMenu.swift */; };
|
||||
BF0C827A169F0DE86C8F12AD /* SystemScrollerStyleEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8B5D1A9658E64F8ECB56 /* SystemScrollerStyleEvents.swift */; };
|
||||
BF0C827C61136E83F43C6436 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C8913DCD0FE4C494055B6 /* InfoPlist.strings */; };
|
||||
@@ -131,7 +131,6 @@
|
||||
BF0C866DE5D740AF7756CDCF /* ImageScalingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C854910CCD4FB493C3A5D /* ImageScalingTests.swift */; };
|
||||
BF0C86B26D16AFC588C88DB4 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C824B8E96CBEA7A757FC3 /* Localizable.strings */; };
|
||||
BF0C86BB79E417163E813179 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C80851C26EDB3DB98556C /* InfoPlist.strings */; };
|
||||
BF0C86FD1DFA49B68F0014CE /* turbolinks.js in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8EC6F81536045569B1CF /* turbolinks.js */; };
|
||||
BF0C875A28B98904F8F4A66A /* ATShortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8BA452332236D972C60E /* ATShortcut.swift */; };
|
||||
BF0C87732A6E660899D13BFD /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C886B1D6E707E9E6BE46C /* InfoPlist.strings */; };
|
||||
BF0C87B5EDCFC573CE220137 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C89231FD53C4D55E3CF9A /* Helpers.swift */; };
|
||||
@@ -241,7 +240,6 @@
|
||||
D04BA737008AA2CD4E230A21 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA10777505D8A67ABD186 /* Application.swift */; };
|
||||
D04BA73E90EFEF8247A5105D /* CGWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAC34CFD42A7F6F1F01C0 /* CGWindow.swift */; };
|
||||
D04BA76A74267B1346D23687 /* GridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA6D57A1456C07318B8EA /* GridView.swift */; };
|
||||
D04BA775CF3F8D9394A1E256 /* NSScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA68C2561D9EE4FD851B8 /* NSScreen.swift */; };
|
||||
D04BA7B8D599E1A7A27FF5AE /* AcknowledgmentsTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA788228BA86D9EFBD1ED /* AcknowledgmentsTab.swift */; };
|
||||
D04BA7B9421871EDBD58F4E3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BABA255CD25D8724A06CD /* Localizable.strings */; };
|
||||
D04BA7BE7F3DD24D58ACE942 /* AppearanceTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA64F1F344007EA13BA05 /* AppearanceTab.swift */; };
|
||||
@@ -374,6 +372,7 @@
|
||||
5F2218B02F156BC6005769F9 /* mascot-squirrel.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "mascot-squirrel.png"; sourceTree = "<group>"; };
|
||||
5F706EBD2BA448B1004D542D /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = is; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
5F706EC22BA448D8004D542D /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = is; path = InfoPlist.strings; sourceTree = "<group>"; };
|
||||
5F906A332F16A18000B99924 /* Screens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screens.swift; sourceTree = "<group>"; };
|
||||
5F9F965C2EAE731200E25498 /* jetbrains.svg */ = {isa = PBXFileReference; lastKnownFileType = text; path = jetbrains.svg; sourceTree = "<group>"; };
|
||||
5FAE000A2F13B49800FE6246 /* update_readme_and_website.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = update_readme_and_website.sh; sourceTree = "<group>"; };
|
||||
5FE2AF752EAE849F004A4AC5 /* xcbeautify */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = xcbeautify; sourceTree = "<group>"; };
|
||||
@@ -620,7 +619,6 @@
|
||||
D04BA653BD073CB58E2CFC93 /* ThumbnailsPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailsPanel.swift; sourceTree = "<group>"; };
|
||||
D04BA662DF59DF9A97FDA598 /* de */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = de; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
D04BA67D904743A293768CB1 /* frontpage.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = frontpage.jpg; sourceTree = "<group>"; };
|
||||
D04BA68C2561D9EE4FD851B8 /* NSScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSScreen.swift; sourceTree = "<group>"; };
|
||||
D04BA6BBFD0E87B8458E91D3 /* feature-suggestion.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = "feature-suggestion.md"; sourceTree = "<group>"; };
|
||||
D04BA6D57A1456C07318B8EA /* GridView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridView.swift; sourceTree = "<group>"; };
|
||||
D04BA6ED38651224DB117A5A /* upload_symbols_to_appcenter.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = upload_symbols_to_appcenter.sh; sourceTree = "<group>"; };
|
||||
@@ -1318,9 +1316,9 @@
|
||||
D04BA0D80B24E72B9B981A1D /* logic */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5F906A332F16A18000B99924 /* Screens.swift */,
|
||||
D04BACD976030676FD0761D5 /* Windows.swift */,
|
||||
D04BA9B93823398A542FF7A0 /* Preferences.swift */,
|
||||
D04BA68C2561D9EE4FD851B8 /* NSScreen.swift */,
|
||||
D04BA7C6F2519091717F4B4E /* SystemPermissions.swift */,
|
||||
D04BA7CF9C2D1BEC7C05AB24 /* Spaces.swift */,
|
||||
D04BA015A45DE7AFDC9794FE /* Window.swift */,
|
||||
@@ -2425,7 +2423,6 @@
|
||||
D04BA8480A8FF466CA89DA5B /* main.swift in Sources */,
|
||||
D04BAFB973C3D28718FAEB87 /* Windows.swift in Sources */,
|
||||
D04BAC011A71E0418154F8CD /* Preferences.swift in Sources */,
|
||||
D04BA775CF3F8D9394A1E256 /* NSScreen.swift in Sources */,
|
||||
D04BADCDA9F9A6C3D6499877 /* SystemPermissions.swift in Sources */,
|
||||
D04BABEECBC6D922298BC93A /* Spaces.swift in Sources */,
|
||||
D04BA6187A91A847844B6ABB /* Window.swift in Sources */,
|
||||
@@ -2462,6 +2459,7 @@
|
||||
5883F0A829A5182C0071DB65 /* TrackpadEvents.swift in Sources */,
|
||||
D04BAAAF5CFA991D3B078DB8 /* KeyboardEvents.swift in Sources */,
|
||||
D04BA1766FBCB2E941D081A5 /* RunningApplicationsEvents.swift in Sources */,
|
||||
5F906A342F16A18500B99924 /* Screens.swift in Sources */,
|
||||
D04BA0AE9865276FF8EF5DEB /* UserDefaultsEvents.swift in Sources */,
|
||||
D04BA4A11F821548EE3C5E95 /* Bash.swift in Sources */,
|
||||
5F21C9E62C6E94920091F72F /* AnimationsSheet.swift in Sources */,
|
||||
@@ -2510,8 +2508,6 @@
|
||||
BF0C8AD478399D2E97AE3FB4 /* CustomRecorderControlTestable.swift in Sources */,
|
||||
BF0C8A5FA46C5E74A071FF4C /* ScrollwheelEvents.swift in Sources */,
|
||||
BF0C82E9AB495AAAA182764A /* CursorEvents.swift in Sources */,
|
||||
BF0C86FD1DFA49B68F0014CE /* turbolinks.js in Sources */,
|
||||
BF0C824E829664B6610C7BAC /* table-of-contents.js in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import Cocoa
|
||||
|
||||
class Screens {
|
||||
static var all = [ScreenUuid: NSScreen]()
|
||||
|
||||
static func refresh() {
|
||||
all.removeAll()
|
||||
for screen in NSScreen.screens {
|
||||
guard let uuid = screen.uuid() else { continue }
|
||||
all[uuid] = screen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NSScreen {
|
||||
static var preferred: NSScreen = detectPreferred() ?? NSScreen.screens.first!
|
||||
|
||||
@@ -22,15 +34,12 @@ extension NSScreen {
|
||||
/// * if NSScreen.screensHaveSeparateSpaces == false, and key window is on another screen than screens[0], it still returns screens[0]
|
||||
/// we find the screen with the key window ourselves manually
|
||||
static func active() -> NSScreen? {
|
||||
if let app = Applications.find(NSWorkspace.shared.frontmostApplication?.processIdentifier) {
|
||||
if let focusedWindow = app.focusedWindow {
|
||||
// on the very first summon, this window may not have its spaces updated, which may land the wrong active screen
|
||||
Windows.updatesWindowSpace(focusedWindow)
|
||||
return NSScreen.screens.first { focusedWindow.isOnScreen($0) }
|
||||
}
|
||||
return NSScreen.withActiveMenubar()
|
||||
}
|
||||
return nil
|
||||
guard let app = Applications.find(NSWorkspace.shared.frontmostApplication?.processIdentifier) else { return nil }
|
||||
guard let focusedWindow = app.focusedWindow else { return NSScreen.withActiveMenubar() }
|
||||
// on the very first summon, this window may not have its spaces updated, which may land the wrong active screen
|
||||
focusedWindow.updateSpacesAndScreen()
|
||||
guard let screenId = focusedWindow.screenId else { return nil }
|
||||
return Screens.all[screenId]
|
||||
}
|
||||
|
||||
/// there is only 1 active menubar. Other screens will show their menubar dimmed
|
||||
@@ -55,13 +64,11 @@ extension NSScreen {
|
||||
}
|
||||
|
||||
func uuid() -> ScreenUuid? {
|
||||
if let screenNumber = number(),
|
||||
// these APIs implicitly unwrap their return values, but it can actually be nil thus we check
|
||||
let screenUuid = CGDisplayCreateUUIDFromDisplayID(screenNumber),
|
||||
let uuid = CFUUIDCreateString(nil, screenUuid.takeRetainedValue()) {
|
||||
return uuid
|
||||
}
|
||||
return nil
|
||||
guard let screenNumber = number(),
|
||||
// these APIs implicitly unwrap their return values, but it can actually be nil thus we check
|
||||
let screenUuid = CGDisplayCreateUUIDFromDisplayID(screenNumber),
|
||||
let uuid = CFUUIDCreateString(nil, screenUuid.takeRetainedValue()) else { return nil }
|
||||
return uuid
|
||||
}
|
||||
|
||||
// periphery:ignore
|
||||
@@ -92,3 +99,4 @@ extension NSScreen {
|
||||
}
|
||||
|
||||
typealias ScreenUuid = CFString
|
||||
|
||||
+22
-1
@@ -29,6 +29,7 @@ class Window {
|
||||
var size: CGSize?
|
||||
var spaceIds = [CGSSpaceID.max]
|
||||
var spaceIndexes = [SpaceIndex.max]
|
||||
var screenId: ScreenUuid?
|
||||
var axUiElement: AXUIElement?
|
||||
var application: Application
|
||||
var axObserver: AXObserver?
|
||||
@@ -42,7 +43,7 @@ class Window {
|
||||
self.axUiElement = axUiElement
|
||||
self.application = application
|
||||
cgWindowId = wid
|
||||
Windows.updatesWindowSpace(self)
|
||||
self.updateSpacesAndScreen()
|
||||
self.isFullscreen = isFullscreen
|
||||
self.isMinimized = isMinimized
|
||||
self.position = position
|
||||
@@ -243,6 +244,26 @@ class Window {
|
||||
return application.localizedName ?? ""
|
||||
}
|
||||
|
||||
func updateSpacesAndScreen() {
|
||||
// macOS bug: if you tab a window, then move the tab group to another space, other tabs from the tab group will stay on the current space
|
||||
// you can use the Dock to focus one of the other tabs and it will teleport that tab in the current space, proving that it's a macOS bug
|
||||
// note: for some reason, it behaves differently if you minimize the tab group after moving it to another space
|
||||
updateSpaces()
|
||||
updateScreenId()
|
||||
}
|
||||
|
||||
private func updateSpaces() {
|
||||
guard let cgWindowId else { return }
|
||||
let spaceIds = cgWindowId.spaces()
|
||||
self.spaceIds = spaceIds
|
||||
self.spaceIndexes = spaceIds.compactMap { spaceId in Spaces.idsAndIndexes.first { $0.0 == spaceId }?.1 }
|
||||
self.isOnAllSpaces = spaceIds.count > 1
|
||||
}
|
||||
|
||||
private func updateScreenId() {
|
||||
screenId = NSScreen.screens.first { isOnScreen($0) }?.uuid()
|
||||
}
|
||||
|
||||
/// window may not be visible on that screen (e.g. the window is not on the current Space)
|
||||
func isOnScreen(_ screen: NSScreen) -> Bool {
|
||||
if NSScreen.screensHaveSeparateSpaces {
|
||||
|
||||
+13
-27
@@ -159,22 +159,20 @@ class Windows {
|
||||
}
|
||||
|
||||
static func updateLastFocus(_ otherWindowAxUiElement: AXUIElement, _ otherWindowWid: CGWindowID) -> [Window]? {
|
||||
if let focusedWindow = (list.first { $0.isEqualRobust(otherWindowAxUiElement, otherWindowWid) }) {
|
||||
let focusedWindowOldFocusOrder = focusedWindow.lastFocusOrder
|
||||
var windowsToRefresh = [focusedWindow]
|
||||
list.forEach {
|
||||
if $0.lastFocusOrder == focusedWindowOldFocusOrder {
|
||||
$0.lastFocusOrder = 0
|
||||
} else if $0.lastFocusOrder < focusedWindowOldFocusOrder {
|
||||
$0.lastFocusOrder += 1
|
||||
}
|
||||
if $0.lastFocusOrder == 0 {
|
||||
windowsToRefresh.append($0)
|
||||
}
|
||||
guard let focusedWindow = (list.first { $0.isEqualRobust(otherWindowAxUiElement, otherWindowWid) }) else { return nil }
|
||||
// no need to update the list is the window is already lastFocusOrder 0
|
||||
guard focusedWindow.lastFocusOrder != 0 && list.count > 1, let previousFocus = (list.first { $0.lastFocusOrder == 0 }) else { return [focusedWindow] }
|
||||
// 2 windows have recently changed: the one which got focused, and the one who just lost focus
|
||||
let windowsToRefresh = [focusedWindow, previousFocus]
|
||||
let focusedWindowOldFocusOrder = focusedWindow.lastFocusOrder
|
||||
list.forEach {
|
||||
if $0.lastFocusOrder == focusedWindowOldFocusOrder {
|
||||
$0.lastFocusOrder = 0
|
||||
} else if $0.lastFocusOrder < focusedWindowOldFocusOrder {
|
||||
$0.lastFocusOrder += 1
|
||||
}
|
||||
return windowsToRefresh
|
||||
}
|
||||
return nil
|
||||
return windowsToRefresh
|
||||
}
|
||||
|
||||
static func updateFocusedAndHoveredWindowIndex(_ newIndex: Int, _ fromMouse: Bool = false) {
|
||||
@@ -306,7 +304,7 @@ class Windows {
|
||||
lazy var visibleCgsWindowIds = Spaces.windowsInSpaces(spaceIdsAndIndexes, false)
|
||||
for window in list {
|
||||
detectTabbedWindows(window, cgsWindowIds, visibleCgsWindowIds)
|
||||
updatesWindowSpace(window)
|
||||
window.updateSpacesAndScreen()
|
||||
refreshIfWindowShouldBeShownToTheUser(window)
|
||||
}
|
||||
refreshWhichWindowsToShowTheUser()
|
||||
@@ -315,18 +313,6 @@ class Windows {
|
||||
return true
|
||||
}
|
||||
|
||||
static func updatesWindowSpace(_ window: Window) {
|
||||
// macOS bug: if you tab a window, then move the tab group to another space, other tabs from the tab group will stay on the current space
|
||||
// you can use the Dock to focus one of the other tabs and it will teleport that tab in the current space, proving that it's a macOS bug
|
||||
// note: for some reason, it behaves differently if you minimize the tab group after moving it to another space
|
||||
if let cgWindowId = window.cgWindowId {
|
||||
let spaceIds = cgWindowId.spaces()
|
||||
window.spaceIds = spaceIds
|
||||
window.spaceIndexes = spaceIds.compactMap { spaceId in Spaces.idsAndIndexes.first { $0.0 == spaceId }?.1 }
|
||||
window.isOnAllSpaces = spaceIds.count > 1
|
||||
}
|
||||
}
|
||||
|
||||
// dispatch screenshot requests off the main-thread, then wait for completion
|
||||
static func refreshThumbnailsAsync(_ windows: [Window], _ source: RefreshCausedBy, windowRemoved: Bool = false) {
|
||||
guard (!windows.isEmpty || windowRemoved) && ScreenRecordingPermission.status == .granted
|
||||
|
||||
@@ -15,18 +15,16 @@ class AccessibilityEvents {
|
||||
let position = try element.position()
|
||||
DispatchQueue.main.async { [weak window] in
|
||||
if let window = (window != nil ? window : (Windows.list.first { $0.isEqualRobust(element, wid) })) {
|
||||
let needsRefresh = window.size != size || window.position != position || window.isMinimized != isMinimized || window.isFullscreen != isFullscreen
|
||||
window.title = window.bestEffortTitle(title)
|
||||
window.size = size
|
||||
window.position = position
|
||||
window.isMinimized = isMinimized
|
||||
window.updateSpacesAndScreen()
|
||||
if window.isFullscreen != isFullscreen {
|
||||
window.isFullscreen = isFullscreen
|
||||
App.app.checkIfShortcutsShouldBeDisabled(window, nil)
|
||||
}
|
||||
if needsRefresh {
|
||||
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
|
||||
}
|
||||
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ class ScreensEvents {
|
||||
debouncerScreenAndSpace.debounce(.screenEvent) {
|
||||
Logger.debug { notification.name.rawValue }
|
||||
Spaces.refresh()
|
||||
Screens.refresh()
|
||||
// a screen added or removed, or screen resolution change can mess up layout; we reset components
|
||||
App.app.resetPreferencesDependentComponents()
|
||||
}
|
||||
|
||||
@@ -56,8 +56,8 @@ class WindowCaptureScreenshots {
|
||||
}
|
||||
|
||||
private static func oneTimeCapture(_ scWindow: SCWindow, _ source: RefreshCausedBy) {
|
||||
guard !App.app.isTerminating else { return }
|
||||
let config = SCStreamConfiguration.forWindow(scWindow, false)
|
||||
guard !App.app.isTerminating, let window = (Windows.list.first { $0.cgWindowId == scWindow.windowID }), window.size != nil else { return }
|
||||
let config = SCStreamConfiguration.forWindow(scWindow, window, false)
|
||||
let filter = SCContentFilter(desktopIndependentWindow: scWindow)
|
||||
ActiveWindowCaptures.increment()
|
||||
SCScreenshotManager.captureImage(contentFilter: filter, configuration: config) { cgImage, error in
|
||||
@@ -100,151 +100,163 @@ class WindowCaptureScreenshotsPrivateApi {
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 12.3, *)
|
||||
class WindowCaptureVideos {
|
||||
private static var streams = [CGWindowID: SCStream]()
|
||||
private static var streamOutputs = [CGWindowID: StreamOutput]()
|
||||
// SCStream.backgroundColor is [unowned], so we must keep own these variables
|
||||
static let scStreamBackgroundColorDark = NSColor(white: 0.23, alpha: 1).cgColor
|
||||
static let scStreamBackgroundColorLight = NSColor.white.cgColor
|
||||
|
||||
static func startCapturing(_ windowsWhichMayHaveChanged: [Window]) {
|
||||
let windowsToShow = Set<CGWindowID>(Windows.list.filter { !$0.isWindowlessApp && $0.shouldShowTheUser }.compactMap { $0.cgWindowId })
|
||||
let windowsAlreadyStreaming = Set<CGWindowID>(streams.keys)
|
||||
let windowsToStop = windowsAlreadyStreaming.subtracting(windowsToShow)
|
||||
stopCaptures(windowsToStop)
|
||||
let windowsToStart = windowsToShow.subtracting(windowsAlreadyStreaming)
|
||||
let windowsWhichMayHaveChanged_ = windowsWhichMayHaveChanged.compactMap { $0.cgWindowId }
|
||||
if !windowsToStart.isEmpty || !windowsWhichMayHaveChanged_.isEmpty {
|
||||
SCShareableContent.getExcludingDesktopWindows(true, onScreenWindowsOnly: false) { shareableContent, error in
|
||||
guard let shareableContent, error == nil else {
|
||||
Logger.error { "\(shareableContent == nil) \(error)" }
|
||||
return
|
||||
}
|
||||
// this callback is executed on an undetermined queue
|
||||
// we move execution to main-thread to avoid races with starting/stopping streams and the app being shown/hidden
|
||||
DispatchQueue.main.async {
|
||||
guard App.app.appIsBeingUsed else { return }
|
||||
startCaptures(windowsToStart, shareableContent)
|
||||
updateCaptures(windowsWhichMayHaveChanged_, shareableContent)
|
||||
Logger.debug { streams.keys }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func stopCapturing() {
|
||||
Logger.debug { streams.keys }
|
||||
for stream in streams.values {
|
||||
stream.stopCapture()
|
||||
}
|
||||
streams.removeAll()
|
||||
streamOutputs.removeAll()
|
||||
}
|
||||
|
||||
private static func updateCaptures(_ windowsWhichMayHaveChanged: [CGWindowID], _ shareableContent: SCShareableContent) {
|
||||
for wid in windowsWhichMayHaveChanged {
|
||||
if let stream = streams[wid],
|
||||
let scWindow = shareableContent.windows.first(where: { $0.windowID == wid }) {
|
||||
stream.updateConfiguration(SCStreamConfiguration.forWindow(scWindow, true)) { error in
|
||||
if let error { Logger.error { error } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func startCaptures(_ windowsToStart: Set<CGWindowID>, _ shareableContent: SCShareableContent) {
|
||||
for wid in windowsToStart {
|
||||
if let scWindow = shareableContent.windows.first(where: { $0.windowID == wid }) {
|
||||
startCapture(scWindow)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static func startCapture(_ window: SCWindow) {
|
||||
let wid = window.windowID
|
||||
let output = StreamOutput(wid)
|
||||
let config = SCStreamConfiguration.forWindow(window, true)
|
||||
let filter = SCContentFilter(desktopIndependentWindow: window)
|
||||
let stream = SCStream(filter: filter, configuration: config, delegate: output)
|
||||
do {
|
||||
try stream.addStreamOutput(output, type: .screen, sampleHandlerQueue: BackgroundWork.screenshotsQueue.strongUnderlyingQueue)
|
||||
stream.startCapture { error in
|
||||
if let error { Logger.error { error } }
|
||||
}
|
||||
streams[wid] = stream
|
||||
streamOutputs[wid] = output
|
||||
} catch {
|
||||
Logger.error { error }
|
||||
}
|
||||
}
|
||||
|
||||
private static func stopCaptures(_ windowsToStop: Set<CGWindowID>) {
|
||||
for wid in windowsToStop {
|
||||
stopCapture(wid)
|
||||
}
|
||||
}
|
||||
|
||||
private static func stopCapture(_ wid: CGWindowID) {
|
||||
if let stream = streams[wid] {
|
||||
stream.stopCapture()
|
||||
streams.removeValue(forKey: wid)
|
||||
streamOutputs.removeValue(forKey: wid)
|
||||
}
|
||||
}
|
||||
|
||||
class StreamOutput: NSObject, SCStreamOutput, SCStreamDelegate {
|
||||
let wid: CGWindowID
|
||||
|
||||
init(_ wid: CGWindowID) {
|
||||
self.wid = wid
|
||||
}
|
||||
|
||||
// from SCStreamOutput; handle captured samples
|
||||
func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of type: SCStreamOutputType) {
|
||||
BackgroundWork.screenshotsQueue.trackCallbacks {
|
||||
if sampleBuffer.isValid,
|
||||
let pixelBuffer = sampleBuffer.pixelBuffer() {
|
||||
DispatchQueue.main.async {
|
||||
if let window = (Windows.list.first { $0.cgWindowId == self.wid }) {
|
||||
window.refreshThumbnail(.pixelBuffer(pixelBuffer))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// from SCStreamDelegate; handle errors when opening a stream
|
||||
func stream(_ stream: SCStream, didStopWithError error: any Error) {
|
||||
BackgroundWork.screenshotsQueue.trackCallbacks {
|
||||
Logger.error { error }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// @available(macOS 12.3, *)
|
||||
// class WindowCaptureVideos {
|
||||
// private static var streams = [CGWindowID: SCStream]()
|
||||
// private static var streamOutputs = [CGWindowID: StreamOutput]()
|
||||
// // SCStream.backgroundColor is [unowned], so we must keep own these variables
|
||||
// static let scStreamBackgroundColorDark = NSColor(white: 0.23, alpha: 1).cgColor
|
||||
// static let scStreamBackgroundColorLight = NSColor.white.cgColor
|
||||
//
|
||||
// static func startCapturing(_ windowsWhichMayHaveChanged: [Window]) {
|
||||
// let windowsToShow = Set<CGWindowID>(Windows.list.filter { !$0.isWindowlessApp && $0.shouldShowTheUser }.compactMap { $0.cgWindowId })
|
||||
// let windowsAlreadyStreaming = Set<CGWindowID>(streams.keys)
|
||||
// let windowsToStop = windowsAlreadyStreaming.subtracting(windowsToShow)
|
||||
// stopCaptures(windowsToStop)
|
||||
// let windowsToStart = windowsToShow.subtracting(windowsAlreadyStreaming)
|
||||
// let windowsWhichMayHaveChanged_ = windowsWhichMayHaveChanged.compactMap { $0.cgWindowId }
|
||||
// if !windowsToStart.isEmpty || !windowsWhichMayHaveChanged_.isEmpty {
|
||||
// SCShareableContent.getExcludingDesktopWindows(true, onScreenWindowsOnly: false) { shareableContent, error in
|
||||
// guard let shareableContent, error == nil else {
|
||||
// Logger.error { "\(shareableContent == nil) \(error)" }
|
||||
// return
|
||||
// }
|
||||
// // this callback is executed on an undetermined queue
|
||||
// // we move execution to main-thread to avoid races with starting/stopping streams and the app being shown/hidden
|
||||
// DispatchQueue.main.async {
|
||||
// guard App.app.appIsBeingUsed else { return }
|
||||
// startCaptures(windowsToStart, shareableContent)
|
||||
// updateCaptures(windowsWhichMayHaveChanged_, shareableContent)
|
||||
// Logger.debug { streams.keys }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// static func stopCapturing() {
|
||||
// Logger.debug { streams.keys }
|
||||
// for stream in streams.values {
|
||||
// stream.stopCapture()
|
||||
// }
|
||||
// streams.removeAll()
|
||||
// streamOutputs.removeAll()
|
||||
// }
|
||||
//
|
||||
// private static func updateCaptures(_ windowsWhichMayHaveChanged: [CGWindowID], _ shareableContent: SCShareableContent) {
|
||||
// for wid in windowsWhichMayHaveChanged {
|
||||
// if let stream = streams[wid],
|
||||
// let scWindow = shareableContent.windows.first(where: { $0.windowID == wid }) {
|
||||
// stream.updateConfiguration(SCStreamConfiguration.forWindow(scWindow, true)) { error in
|
||||
// if let error { Logger.error { error } }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private static func startCaptures(_ windowsToStart: Set<CGWindowID>, _ shareableContent: SCShareableContent) {
|
||||
// for wid in windowsToStart {
|
||||
// if let scWindow = shareableContent.windows.first(where: { $0.windowID == wid }) {
|
||||
// startCapture(scWindow)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// private static func startCapture(_ window: SCWindow) {
|
||||
// let wid = window.windowID
|
||||
// let output = StreamOutput(wid)
|
||||
// let config = SCStreamConfiguration.forWindow(window, true)
|
||||
// let filter = SCContentFilter(desktopIndependentWindow: window)
|
||||
// let stream = SCStream(filter: filter, configuration: config, delegate: output)
|
||||
// do {
|
||||
// try stream.addStreamOutput(output, type: .screen, sampleHandlerQueue: BackgroundWork.screenshotsQueue.strongUnderlyingQueue)
|
||||
// stream.startCapture { error in
|
||||
// if let error { Logger.error { error } }
|
||||
// }
|
||||
// streams[wid] = stream
|
||||
// streamOutputs[wid] = output
|
||||
// } catch {
|
||||
// Logger.error { error }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private static func stopCaptures(_ windowsToStop: Set<CGWindowID>) {
|
||||
// for wid in windowsToStop {
|
||||
// stopCapture(wid)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private static func stopCapture(_ wid: CGWindowID) {
|
||||
// if let stream = streams[wid] {
|
||||
// stream.stopCapture()
|
||||
// streams.removeValue(forKey: wid)
|
||||
// streamOutputs.removeValue(forKey: wid)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// class StreamOutput: NSObject, SCStreamOutput, SCStreamDelegate {
|
||||
// let wid: CGWindowID
|
||||
//
|
||||
// init(_ wid: CGWindowID) {
|
||||
// self.wid = wid
|
||||
// }
|
||||
//
|
||||
// // from SCStreamOutput; handle captured samples
|
||||
// func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of type: SCStreamOutputType) {
|
||||
// BackgroundWork.screenshotsQueue.trackCallbacks {
|
||||
// if sampleBuffer.isValid,
|
||||
// let pixelBuffer = sampleBuffer.pixelBuffer() {
|
||||
// DispatchQueue.main.async {
|
||||
// if let window = (Windows.list.first { $0.cgWindowId == self.wid }) {
|
||||
// window.refreshThumbnail(.pixelBuffer(pixelBuffer))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // from SCStreamDelegate; handle errors when opening a stream
|
||||
// func stream(_ stream: SCStream, didStopWithError error: any Error) {
|
||||
// BackgroundWork.screenshotsQueue.trackCallbacks {
|
||||
// Logger.error { error }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@available(macOS 12.3, *)
|
||||
extension SCStreamConfiguration {
|
||||
static func forWindow(_ window: SCWindow, _ video: Bool) -> SCStreamConfiguration {
|
||||
static func forWindow(_ scWindow: SCWindow, _ window: Window, _ video: Bool) -> SCStreamConfiguration {
|
||||
let config = SCStreamConfiguration()
|
||||
config.setWindowSize(window)
|
||||
config.setWindowSize(scWindow, window)
|
||||
config.showsCursor = false
|
||||
if video {
|
||||
config.minimumFrameInterval = CMTime(value: 1, timescale: CMTimeScale(60))
|
||||
config.queueDepth = 8
|
||||
// ~60% memory reduction compared to kCVPixelFormatType_32BGRA
|
||||
config.pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
|
||||
// kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange doesn't have transparency, so we end up with an opaque background color around the window corners
|
||||
// we use a background color that try and hide these corners as much as possible
|
||||
config.backgroundColor = Appearance.currentTheme == .dark ? WindowCaptureVideos.scStreamBackgroundColorDark : WindowCaptureVideos.scStreamBackgroundColorLight
|
||||
}
|
||||
// if video {
|
||||
// config.minimumFrameInterval = CMTime(value: 1, timescale: CMTimeScale(60))
|
||||
// config.queueDepth = 8
|
||||
// // ~60% memory reduction compared to kCVPixelFormatType_32BGRA
|
||||
// config.pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
|
||||
// // kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange doesn't have transparency, so we end up with an opaque background color around the window corners
|
||||
// // we use a background color that try and hide these corners as much as possible
|
||||
// config.backgroundColor = Appearance.currentTheme == .dark ? WindowCaptureVideos.scStreamBackgroundColorDark : WindowCaptureVideos.scStreamBackgroundColorLight
|
||||
// }
|
||||
// config.scalesToFit = true
|
||||
return config
|
||||
}
|
||||
|
||||
private func setWindowSize(_ window: SCWindow) {
|
||||
let originalSize = NSSize(width: window.frame.width * NSScreen.preferred.backingScaleFactor, height: window.frame.height * NSScreen.preferred.backingScaleFactor)
|
||||
private func windowScaleFactor(_ window: Window) -> CGFloat {
|
||||
if let screenId = window.screenId,
|
||||
let screen = Screens.all[screenId] {
|
||||
return screen.backingScaleFactor
|
||||
}
|
||||
return NSScreen.preferred.backingScaleFactor
|
||||
}
|
||||
|
||||
private func setWindowSize(_ scWindow: SCWindow, _ window: Window) {
|
||||
let scaleFactor = windowScaleFactor(window)
|
||||
// we use window.size and not scWindow.frame, as scWindow is cached thus its size can be stale. window.size is always up-to-date
|
||||
let size = window.size! // we checked non-nil earlier, up the stack
|
||||
// window.size shows logical size. It doesn't change when the scaleFactor changes. We need to correct for this as we need to capture more and less pixels depending on DPI
|
||||
let originalSize = NSSize(width: size.width * scaleFactor, height: size.height * scaleFactor)
|
||||
guard originalSize.width > 0, originalSize.height > 0 else { return }
|
||||
if Preferences.previewFocusedWindow {
|
||||
width = Int(originalSize.width)
|
||||
|
||||
@@ -304,6 +304,7 @@ extension App: NSApplicationDelegate {
|
||||
self.thumbnailsPanel = ThumbnailsPanel()
|
||||
self.previewPanel = PreviewPanel()
|
||||
Spaces.refresh()
|
||||
Screens.refresh()
|
||||
SpacesEvents.observe()
|
||||
ScreensEvents.observe()
|
||||
ThumbnailsPanel.updateMaxPossibleThumbnailSize()
|
||||
|
||||
Reference in new issue
Block a user