import CoreGraphics
|
import Foundation
|
|
public enum FinderTabSpaceAttributionPolicy {
|
private static let finderBundleIdentifier = "com.apple.finder"
|
private static let minimumOverlapRatio: CGFloat = 0.85
|
private static let minimumSizeSimilarityRatio: CGFloat = 0.80
|
private static let minimumBestScoreGap: CGFloat = 0.05
|
|
public static func attributedRecords(_ records: [WindowEnumerationRecord]) -> [WindowEnumerationRecord] {
|
let hosts = records.filter(isFinderFullscreenHost)
|
guard !hosts.isEmpty else { return records }
|
|
return records.map { record in
|
guard isFinderTabCandidate(record),
|
let host = bestHost(for: record, in: hosts)
|
else {
|
return record
|
}
|
|
return copy(
|
record,
|
isFullscreen: true,
|
spaceIDs: host.spaceIDs
|
)
|
}
|
}
|
|
private static func isFinderTabCandidate(_ record: WindowEnumerationRecord) -> Bool {
|
WindowEnumerationPolicy.shouldInclude(record)
|
&& record.identifierSource == .cgWindow
|
&& record.app.bundleIdentifier == finderBundleIdentifier
|
&& record.spaceIDs.isEmpty
|
&& !record.isMinimized
|
&& !record.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
&& record.frame?.isEmpty == false
|
}
|
|
private static func isFinderFullscreenHost(_ record: WindowEnumerationRecord) -> Bool {
|
WindowEnumerationPolicy.shouldInclude(record)
|
&& record.app.bundleIdentifier == finderBundleIdentifier
|
&& record.isFullscreen
|
&& !record.spaceIDs.isEmpty
|
&& record.subrole == .standard
|
&& record.frame?.isEmpty == false
|
}
|
|
private static func bestHost(
|
for candidate: WindowEnumerationRecord,
|
in hosts: [WindowEnumerationRecord]
|
) -> WindowEnumerationRecord? {
|
let compatibleHosts = hosts.filter { host in
|
sameProcess(candidate, host)
|
}
|
let scoredHosts = compatibleHosts.compactMap { host -> (record: WindowEnumerationRecord, score: CGFloat)? in
|
guard let score = frameCompatibilityScore(candidate.frame, host.frame)
|
else {
|
return nil
|
}
|
|
return (host, score)
|
}
|
.sorted { lhs, rhs in
|
if lhs.score != rhs.score {
|
return lhs.score > rhs.score
|
}
|
return lhs.record.id < rhs.record.id
|
}
|
|
guard let best = scoredHosts.first else { return nil }
|
if scoredHosts.count == 1 {
|
return best.record
|
}
|
|
let sameSpaceCompetitors = scoredHosts.filter { $0.record.spaceIDs == best.record.spaceIDs }
|
if sameSpaceCompetitors.count == scoredHosts.count {
|
return best.record
|
}
|
|
let secondBest = scoredHosts[1]
|
return best.score - secondBest.score >= minimumBestScoreGap ? best.record : nil
|
}
|
|
private static func frameCompatibilityScore(_ lhs: CGRect?, _ rhs: CGRect?) -> CGFloat? {
|
guard let overlap = overlapScore(lhs, rhs),
|
overlap >= minimumOverlapRatio,
|
let sizeSimilarity = sizeSimilarityScore(lhs, rhs),
|
sizeSimilarity >= minimumSizeSimilarityRatio
|
else {
|
return nil
|
}
|
|
return min(overlap, sizeSimilarity)
|
}
|
|
private static func sameProcess(
|
_ lhs: WindowEnumerationRecord,
|
_ rhs: WindowEnumerationRecord
|
) -> Bool {
|
guard let lhsPID = lhs.app.processIdentifier,
|
let rhsPID = rhs.app.processIdentifier
|
else {
|
return false
|
}
|
|
return lhsPID == rhsPID
|
}
|
|
private static func overlapScore(_ lhs: CGRect?, _ rhs: CGRect?) -> CGFloat? {
|
guard let lhs,
|
let rhs,
|
!lhs.isEmpty,
|
!rhs.isEmpty
|
else {
|
return nil
|
}
|
|
let intersection = lhs.intersection(rhs)
|
guard !intersection.isNull,
|
!intersection.isEmpty
|
else {
|
return nil
|
}
|
|
let denominator = min(lhs.width * lhs.height, rhs.width * rhs.height)
|
guard denominator > 0 else { return nil }
|
return (intersection.width * intersection.height) / denominator
|
}
|
|
private static func sizeSimilarityScore(_ lhs: CGRect?, _ rhs: CGRect?) -> CGFloat? {
|
guard let lhs,
|
let rhs,
|
lhs.width > 0,
|
lhs.height > 0,
|
rhs.width > 0,
|
rhs.height > 0
|
else {
|
return nil
|
}
|
|
let widthSimilarity = min(lhs.width, rhs.width) / max(lhs.width, rhs.width)
|
let heightSimilarity = min(lhs.height, rhs.height) / max(lhs.height, rhs.height)
|
return min(widthSimilarity, heightSimilarity)
|
}
|
|
private static func copy(
|
_ record: WindowEnumerationRecord,
|
isFullscreen: Bool,
|
spaceIDs: [UInt64]
|
) -> WindowEnumerationRecord {
|
WindowEnumerationRecord(
|
id: record.id,
|
app: record.app,
|
activationPolicy: record.activationPolicy,
|
title: record.title,
|
identifierSource: record.identifierSource,
|
size: record.size,
|
frame: record.frame,
|
subrole: record.subrole,
|
isMinimized: record.isMinimized,
|
isFullscreen: isFullscreen,
|
isGhost: record.isGhost,
|
isDesktopElement: record.isDesktopElement,
|
isSystemCritical: record.isSystemCritical,
|
isInteractable: record.isInteractable,
|
hasAXBacking: record.hasAXBacking,
|
isOnscreen: record.isOnscreen,
|
spaceIDs: spaceIDs
|
)
|
}
|
}
|