/
Screen.swift
228 lines (184 loc) · 7.85 KB
/
Screen.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
import PromiseKit
// MARK: - Screen
/// A physical display.
public final class Screen: Equatable, CustomDebugStringConvertible {
internal let delegate: ScreenDelegate
internal init(delegate: ScreenDelegate) {
self.delegate = delegate
}
public var debugDescription: String { return delegate.debugDescription }
/// The frame defining the screen boundaries in global coordinates.
/// -Note: x and y may be negative.
public var frame: CGRect { return delegate.frame }
/// The frame defining the screen boundaries in global coordinates, excluding the menu bar and
/// dock.
public var applicationFrame: CGRect { return delegate.applicationFrame }
}
public func ==(lhs: Screen, rhs: Screen) -> Bool {
return lhs.delegate.equalTo(rhs.delegate)
}
protocol SystemScreenDelegate {
associatedtype Delegate: ScreenDelegate
func createForAll() -> [Delegate]
}
protocol ScreenDelegate: class, CustomDebugStringConvertible {
var frame: CGRect { get }
var applicationFrame: CGRect { get }
func equalTo(_ other: ScreenDelegate) -> Bool
}
struct FakeSystemScreenDelegate: SystemScreenDelegate {
typealias Delegate = FakeScreenDelegate
var screens: [Delegate]
func createForAll() -> [Delegate] {
return screens
}
}
final class FakeScreenDelegate: ScreenDelegate {
let frame: CGRect
let applicationFrame: CGRect
init(frame: CGRect, applicationFrame: CGRect) {
self.frame = frame
self.applicationFrame = applicationFrame
}
func equalTo(_ other: ScreenDelegate) -> Bool { return false }
var debugDescription: String {
return "FakeScreen(frame: \(frame), applicationFrame: \(applicationFrame))"
}
}
// MARK: - OSXScreenDelegate
protocol NSScreenType {
var frame: CGRect { get }
var visibleFrame: CGRect { get }
var deviceDescription: [NSDeviceDescriptionKey: Any] { get }
var displayName: String { get }
}
extension NSScreen: NSScreenType {
/// The name for the display (usually, the manufacturer and model number).
/// -Note: This is expensive to get, and should be cached in a stored property.
var displayName: String {
guard let info = infoForCGDisplay(numberForScreen(self as NSScreen),
options: kIODisplayOnlyPreferredName) else {
return "Unknown screen"
}
guard let localizedNames = info[kDisplayProductName] as! NSDictionary? as Dictionary?,
let name = localizedNames.values.first as! NSString? as String? else {
return "Unnamed screen"
}
return name
}
}
struct OSXSystemScreenDelegate: SystemScreenDelegate {
typealias ScreenDelegate = OSXScreenDelegate<NSScreen>
func createForAll() -> [ScreenDelegate] {
return NSScreen.screens.map{ OSXScreenDelegate(nsScreen: $0) }
}
}
private let kNSScreenNumber = "NSScreenNumber"
final class OSXScreenDelegate<NSScreenT: NSScreenType>: ScreenDelegate {
fileprivate let nsScreen: NSScreenT
// This ID is guaranteed to stay the same for any given display. NSScreen equality checks can
// fail if the display switches graphics cards.
fileprivate let directDisplayID: CGDirectDisplayID
init(nsScreen: NSScreenT) {
self.nsScreen = nsScreen
frame = nsScreen.frame
directDisplayID = numberForScreen(nsScreen)
}
func equalTo(_ other: ScreenDelegate) -> Bool {
guard let other = other as? OSXScreenDelegate else {
return false
}
return other.directDisplayID == directDisplayID
}
lazy var displayName: String = { self.nsScreen.displayName }()
var debugDescription: String {
return "\"\(displayName)\" \(frame)"
}
// The frame won't change during the delegate's lifetime because it gets recreated every time
// there is a screen configuration change.
let frame: CGRect
var applicationFrame: CGRect { return nsScreen.visibleFrame }
}
extension OSXScreenDelegate {
static func handleScreenChange(
newScreens nsScreens: [NSScreenT], oldScreens: [OSXScreenDelegate], state: State
) -> ([OSXScreenDelegate], ScreenLayoutChangedEvent) {
// Make a new screen delegate for every screen, because NSScreen objects can become stale.
let newScreens = nsScreens.map { OSXScreenDelegate(nsScreen: $0) }
var oldScreensById: [CGDirectDisplayID: OSXScreenDelegate] = [:]
for oldScreen in oldScreens {
oldScreensById[oldScreen.directDisplayID] = oldScreen
}
var addedScreens: [Screen] = []
var changedScreens: [Screen] = []
var unchangedScreens: [Screen] = []
for newScreen in newScreens {
let newScreenWrapped = Screen(delegate: newScreen)
guard let oldScreen = oldScreensById[newScreen.directDisplayID] else {
addedScreens.append(newScreenWrapped)
continue
}
// Remove from dict to signifiy that we've seen it.
oldScreensById[newScreen.directDisplayID] = nil
if newScreen.frame != oldScreen.frame
|| newScreen.applicationFrame != oldScreen.applicationFrame {
changedScreens.append(newScreenWrapped)
} else {
unchangedScreens.append(newScreenWrapped)
}
}
// All old screens that match a new screen were removed from oldScreensById.
let removedScreens = Array(oldScreensById.values.map { Screen(delegate: $0) })
let event = ScreenLayoutChangedEvent(
external: false,
addedScreens: addedScreens,
removedScreens: removedScreens,
changedScreens: changedScreens,
unchangedScreens: unchangedScreens
)
return (newScreens, event)
}
}
private func numberForScreen<NSScreenT: NSScreenType>(_ nsScreen: NSScreenT) -> CGDirectDisplayID {
// Get the direct display ID. This is documented to always exist.
let screenNumber = nsScreen.deviceDescription[NSDeviceDescriptionKey(kNSScreenNumber)]!
return CGDirectDisplayID((screenNumber as! NSNumber).intValue)
}
/// Returns the IODisplay info dictionary for the given displayID.
///
/// -Returns: The info dictionary for the first screen with the same vendor and model number as the
/// specified screen.
private func infoForCGDisplay(_ displayID: CGDirectDisplayID, options: Int) -> [AnyHashable: Any]? {
var iter: io_iterator_t = 0
// Initialize iterator.
let services = IOServiceMatching("IODisplayConnect")
let err = IOServiceGetMatchingServices(kIOMasterPortDefault, services, &iter)
guard err == KERN_SUCCESS else {
log.warn("Could not find services for IODisplayConnect, error code \(err)")
return nil
}
// Loop through all screens, looking for a vendor and model ID match.
var service = IOIteratorNext(iter)
while service != 0 {
let info = IODisplayCreateInfoDictionary(service, IOOptionBits(options)).takeRetainedValue()
as Dictionary as [AnyHashable: Any]
guard let cfVendorID = info[kDisplayVendorID] as! CFNumber?,
let cfProductID = info[kDisplayProductID] as! CFNumber? else {
log.warn("Missing vendor or product ID encountered when looping through screens")
continue
}
var vendorID: CFIndex = 0, productID: CFIndex = 0
guard CFNumberGetValue(cfVendorID, .cfIndexType, &vendorID) &&
CFNumberGetValue(cfProductID, .cfIndexType, &productID) else {
log.warn("Unexpected failure unwrapping vendor or product ID while looping through "
+ "screens")
continue
}
if UInt32(vendorID) == CGDisplayVendorNumber(displayID) &&
UInt32(productID) == CGDisplayModelNumber(displayID) {
return info
}
service = IOIteratorNext(iter)
}
return nil
}