-
Notifications
You must be signed in to change notification settings - Fork 110
Expand file tree
/
Copy pathAKPlugin.swift
More file actions
316 lines (280 loc) · 11.3 KB
/
AKPlugin.swift
File metadata and controls
316 lines (280 loc) · 11.3 KB
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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
//
// MacPlugin.swift
// AKInterface
//
// Created by Isaac Marovitz on 13/09/2022.
//
import AppKit
import CoreGraphics
import Foundation
// Add a lightweight struct so we can decode only the flag we care about
private struct AKAppSettingsData: Codable {
var hideTitleBar: Bool?
var floatingWindow: Bool?
var resolution: Int?
var resizableAspectRatioWidth: Int?
var resizableAspectRatioHeight: Int?
}
class AKPlugin: NSObject, Plugin {
required override init() {
super.init()
if let window = NSApplication.shared.windows.first {
window.styleMask.insert([.resizable])
window.collectionBehavior = [.fullScreenPrimary, .managed, .participatesInCycle]
window.isMovable = true
window.isMovableByWindowBackground = true
if self.hideTitleBarSetting == true {
window.styleMask.insert([.fullSizeContentView])
window.titlebarAppearsTransparent = true
window.titleVisibility = .hidden
window.toolbar = nil
window.title = ""
}
if self.floatingWindowSetting == true {
window.level = .floating
}
if let aspectRatio = self.aspectRatioSetting {
window.contentAspectRatio = aspectRatio
}
NSWindow.allowsAutomaticWindowTabbing = true
}
// Apply the same appearance rules to any subsequent windows that may be created
NotificationCenter.default.addObserver(
forName: NSWindow.didBecomeKeyNotification,
object: nil,
queue: .main) { notif in
guard let win = notif.object as? NSWindow else { return }
win.styleMask.insert([.resizable])
if self.hideTitleBarSetting == true {
win.styleMask.insert([.fullSizeContentView])
win.titlebarAppearsTransparent = true
win.titleVisibility = .hidden
win.toolbar = nil
win.title = ""
}
if self.floatingWindowSetting == true {
win.level = .floating
}
if let aspectRatio = self.aspectRatioSetting {
win.contentAspectRatio = aspectRatio
}
}
}
var screenCount: Int {
NSScreen.screens.count
}
var mousePoint: CGPoint {
NSApplication.shared.windows.first?.mouseLocationOutsideOfEventStream ?? CGPoint()
}
var windowFrame: CGRect {
NSApplication.shared.windows.first?.frame ?? CGRect()
}
var isMainScreenEqualToFirst: Bool {
return NSScreen.main == NSScreen.screens.first
}
var mainScreenFrame: CGRect {
return NSScreen.main!.frame as CGRect
}
var isFullscreen: Bool {
NSApplication.shared.windows.first!.styleMask.contains(.fullScreen)
}
var cmdPressed: Bool = false
var cursorHideLevel = 0
func hideCursor() {
NSCursor.hide()
cursorHideLevel += 1
CGAssociateMouseAndMouseCursorPosition(0)
warpCursor()
}
func hideCursorMove() {
NSCursor.setHiddenUntilMouseMoves(true)
}
func warpCursor() {
guard let firstScreen = NSScreen.screens.first else {return}
let frame = windowFrame
// Convert from NS coordinates to CG coordinates
CGWarpMouseCursorPosition(CGPoint(x: frame.midX, y: firstScreen.frame.height - frame.midY))
}
func unhideCursor() {
NSCursor.unhide()
cursorHideLevel -= 1
if cursorHideLevel <= 0 {
CGAssociateMouseAndMouseCursorPosition(1)
}
}
func terminateApplication() {
NSApplication.shared.terminate(self)
}
private var modifierFlag: UInt = 0
// swiftlint:disable:next function_body_length
func setupKeyboard(keyboard: @escaping (UInt16, Bool, Bool, Bool) -> Bool,
swapMode: @escaping () -> Bool) {
func checkCmd(modifier: NSEvent.ModifierFlags) -> Bool {
if modifier.contains(.command) {
self.cmdPressed = true
return true
} else if self.cmdPressed {
self.cmdPressed = false
}
return false
}
NSEvent.addLocalMonitorForEvents(matching: .keyDown, handler: { event in
if checkCmd(modifier: event.modifierFlags) {
return event
}
let consumed = keyboard(event.keyCode, true, event.isARepeat,
event.modifierFlags.contains(.control))
if consumed {
return nil
}
return event
})
NSEvent.addLocalMonitorForEvents(matching: .keyUp, handler: { event in
if checkCmd(modifier: event.modifierFlags) {
return event
}
let consumed = keyboard(event.keyCode, false, false,
event.modifierFlags.contains(.control))
if consumed {
return nil
}
return event
})
NSEvent.addLocalMonitorForEvents(matching: .flagsChanged, handler: { event in
if checkCmd(modifier: event.modifierFlags) {
return event
}
let pressed = self.modifierFlag < event.modifierFlags.rawValue
let changed = self.modifierFlag ^ event.modifierFlags.rawValue
self.modifierFlag = event.modifierFlags.rawValue
let changedFlags = NSEvent.ModifierFlags(rawValue: changed)
if pressed && changedFlags.contains(.option) {
if swapMode() {
return nil
}
return event
}
let consumed = keyboard(event.keyCode, pressed, false,
event.modifierFlags.contains(.control))
if consumed {
return nil
}
return event
})
}
func setupMouseMoved(_ mouseMoved: @escaping (CGFloat, CGFloat) -> Bool) {
let mask: NSEvent.EventTypeMask = [.leftMouseDragged, .otherMouseDragged, .rightMouseDragged]
NSEvent.addLocalMonitorForEvents(matching: mask, handler: { event in
let consumed = mouseMoved(event.deltaX, event.deltaY)
if consumed {
return nil
}
return event
})
// transpass mouse moved event when no button pressed, for traffic light button to light up
NSEvent.addLocalMonitorForEvents(matching: .mouseMoved, handler: { event in
_ = mouseMoved(event.deltaX, event.deltaY)
return event
})
}
func setupMouseButton(left: Bool, right: Bool, _ consumed: @escaping (Int, Bool) -> Bool) {
let downType: NSEvent.EventTypeMask = left ? .leftMouseDown : right ? .rightMouseDown : .otherMouseDown
let upType: NSEvent.EventTypeMask = left ? .leftMouseUp : right ? .rightMouseUp : .otherMouseUp
// Helper to detect whether the event is inside any of the window "traffic-light" buttons
func isInTrafficLightArea(_ event: NSEvent) -> Bool {
if self.hideTitleBarSetting == false {
return false
}
guard let win = event.window else { return false }
let pointInWindow = event.locationInWindow
let buttonTypes: [NSWindow.ButtonType] = [.closeButton, .miniaturizeButton, .zoomButton, .fullScreenButton]
for type in buttonTypes {
if let button = win.standardWindowButton(type) {
let localPoint = button.convert(pointInWindow, from: nil) // convert from window coords
if button.bounds.contains(localPoint) {
return true
}
}
}
return false
}
NSEvent.addLocalMonitorForEvents(matching: downType, handler: { event in
// Always allow clicks on the window traffic-light buttons to pass through
if isInTrafficLightArea(event) {
return event
}
// Detect double-clicks on the title-bar area (respecting system preference)
if left && event.clickCount == 2, self.hideTitleBarSetting, let win = event.window {
let contentRect = win.contentLayoutRect
// Title-bar area is the region above contentLayoutRect
if event.locationInWindow.y > contentRect.maxY {
win.performZoom(nil)
return nil
}
}
// For traffic light buttons when fullscreen
if event.window != NSApplication.shared.windows.first! {
return event
}
if consumed(event.buttonNumber, true) {
return nil
}
return event
})
NSEvent.addLocalMonitorForEvents(matching: upType, handler: { event in
// Always allow releases on the traffic-light buttons to pass through
if isInTrafficLightArea(event) {
return event
}
if consumed(event.buttonNumber, false) {
return nil
}
return event
})
}
func setupScrollWheel(_ onMoved: @escaping (CGFloat, CGFloat) -> Bool) {
NSEvent.addLocalMonitorForEvents(matching: NSEvent.EventTypeMask.scrollWheel, handler: { event in
var deltaX = event.scrollingDeltaX, deltaY = event.scrollingDeltaY
if !event.hasPreciseScrollingDeltas {
deltaX *= 16
deltaY *= 16
}
let consumed = onMoved(deltaX, deltaY)
if consumed {
return nil
}
return event
})
}
func urlForApplicationWithBundleIdentifier(_ value: String) -> URL? {
NSWorkspace.shared.urlForApplication(withBundleIdentifier: value)
}
func setMenuBarVisible(_ visible: Bool) {
NSMenu.setMenuBarVisible(visible)
}
/// Convenience instance property that exposes the cached static preference.
private var hideTitleBarSetting: Bool { Self.akAppSettingsData?.hideTitleBar ?? false }
private var floatingWindowSetting: Bool { Self.akAppSettingsData?.floatingWindow ?? false }
private var aspectRatioSetting: NSSize? {
guard Self.akAppSettingsData?.resolution == 6 else {
return nil
}
let width = Self.akAppSettingsData?.resizableAspectRatioWidth ?? 0
let height = Self.akAppSettingsData?.resizableAspectRatioHeight ?? 0
guard width > 0 && height > 0 else {
return nil
}
return NSSize(width: width, height: height)
}
fileprivate static var akAppSettingsData: AKAppSettingsData? = {
let bundleIdentifier = Bundle.main.bundleIdentifier ?? ""
let settingsURL = URL(fileURLWithPath: "/Users/\(NSUserName())/Library/Containers/io.playcover.PlayCover")
.appendingPathComponent("App Settings")
.appendingPathComponent("\(bundleIdentifier).plist")
guard let data = try? Data(contentsOf: settingsURL),
let decoded = try? PropertyListDecoder().decode(AKAppSettingsData.self, from: data) else {
return nil
}
return decoded
}()
}