mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Something in the way
This commit is contained in:
parent
018050081d
commit
9bd445986d
19 changed files with 2395 additions and 1 deletions
|
|
@ -0,0 +1,248 @@
|
|||
import SwiftUI
|
||||
import Combine
|
||||
import Defaults
|
||||
import AppKit
|
||||
import SkyLightWindow
|
||||
import AtollExtensionKit
|
||||
|
||||
@MainActor
|
||||
final class ExtensionLockScreenWidgetPresentationController {
|
||||
private unowned let manager: ExtensionLockScreenWidgetManager
|
||||
private var cancellables: Set<AnyCancellable> = []
|
||||
private let windowPool = ExtensionLockScreenWidgetWindowPool()
|
||||
private var cachedPayloads: [ExtensionLockScreenWidgetPayload] = []
|
||||
private var isLocked: Bool = false
|
||||
|
||||
init(manager: ExtensionLockScreenWidgetManager) {
|
||||
self.manager = manager
|
||||
}
|
||||
|
||||
func activate() {
|
||||
observeLockState()
|
||||
observePayloads()
|
||||
observeDefaults()
|
||||
}
|
||||
|
||||
private func observeLockState() {
|
||||
LockScreenManager.shared.$isLocked
|
||||
.removeDuplicates()
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] locked in
|
||||
guard let self else { return }
|
||||
self.isLocked = locked
|
||||
self.refreshPresentation()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func observePayloads() {
|
||||
manager.$activeWidgets
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] payloads in
|
||||
guard let self else { return }
|
||||
self.cachedPayloads = payloads
|
||||
self.refreshPresentation()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func observeDefaults() {
|
||||
Defaults.publisher(.enableExtensionLockScreenWidgets, options: [])
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] change in
|
||||
guard let self else { return }
|
||||
if change.newValue {
|
||||
self.refreshPresentation()
|
||||
} else {
|
||||
self.windowPool.hideAll()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func refreshPresentation() {
|
||||
guard isLocked, LockScreenManager.shared.currentLockStatus, Defaults[.enableExtensionLockScreenWidgets] else {
|
||||
windowPool.hideAll()
|
||||
return
|
||||
}
|
||||
guard cachedPayloads.isEmpty == false else {
|
||||
windowPool.hideAll()
|
||||
return
|
||||
}
|
||||
windowPool.sync(with: cachedPayloads)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class ExtensionLockScreenWidgetWindowPool {
|
||||
private struct WindowRecord {
|
||||
let window: NSWindow
|
||||
}
|
||||
|
||||
private var windows: [String: WindowRecord] = [:]
|
||||
private var payloadsByID: [String: ExtensionLockScreenWidgetPayload] = [:]
|
||||
private var screenChangeObserver: NSObjectProtocol?
|
||||
private var workspaceObservers: [NSObjectProtocol] = []
|
||||
|
||||
init() {
|
||||
registerScreenChangeObservers()
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let screenChangeObserver {
|
||||
NotificationCenter.default.removeObserver(screenChangeObserver)
|
||||
}
|
||||
let workspaceCenter = NSWorkspace.shared.notificationCenter
|
||||
workspaceObservers.forEach { workspaceCenter.removeObserver($0) }
|
||||
}
|
||||
|
||||
func sync(with payloads: [ExtensionLockScreenWidgetPayload]) {
|
||||
payloadsByID = Dictionary(uniqueKeysWithValues: payloads.map { ($0.id, $0) })
|
||||
let activeIDs = Set(payloadsByID.keys)
|
||||
let staleIDs = Set(windows.keys).subtracting(activeIDs)
|
||||
staleIDs.forEach { hideWindow(withID: $0) }
|
||||
payloads.forEach { renderWindow(for: $0) }
|
||||
}
|
||||
|
||||
func hideAll() {
|
||||
payloadsByID.removeAll()
|
||||
windows.values.forEach { record in
|
||||
record.window.orderOut(nil)
|
||||
record.window.contentView = nil
|
||||
}
|
||||
windows.removeAll()
|
||||
}
|
||||
|
||||
private func renderWindow(for payload: ExtensionLockScreenWidgetPayload) {
|
||||
guard let screen = currentScreen() else { return }
|
||||
let descriptor = payload.descriptor
|
||||
let window = ensureWindow(for: payload.id)
|
||||
let hosting = NSHostingView(rootView: ExtensionLockScreenWidgetView(payload: payload))
|
||||
hosting.frame = NSRect(origin: .zero, size: descriptor.size)
|
||||
hosting.wantsLayer = true
|
||||
hosting.layer?.masksToBounds = true
|
||||
hosting.layer?.cornerRadius = descriptor.cornerRadius
|
||||
window.contentView = hosting
|
||||
window.setFrame(frame(for: descriptor, on: screen), display: true)
|
||||
window.alphaValue = 1
|
||||
window.orderFrontRegardless()
|
||||
}
|
||||
|
||||
private func ensureWindow(for id: String) -> NSWindow {
|
||||
if let record = windows[id] {
|
||||
return record.window
|
||||
}
|
||||
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(origin: .zero, size: CGSize(width: 200, height: 80)),
|
||||
styleMask: [.borderless, .nonactivatingPanel],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
|
||||
window.isReleasedWhenClosed = false
|
||||
window.isOpaque = false
|
||||
window.backgroundColor = .clear
|
||||
window.hasShadow = false
|
||||
window.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()))
|
||||
window.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
|
||||
window.ignoresMouseEvents = true
|
||||
window.isMovable = false
|
||||
|
||||
ScreenCaptureVisibilityManager.shared.register(window, scope: .entireInterface)
|
||||
SkyLightOperator.shared.delegateWindow(window)
|
||||
|
||||
let record = WindowRecord(window: window)
|
||||
windows[id] = record
|
||||
return window
|
||||
}
|
||||
|
||||
private func hideWindow(withID id: String) {
|
||||
guard let record = windows[id] else { return }
|
||||
record.window.orderOut(nil)
|
||||
record.window.contentView = nil
|
||||
windows.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
private func registerScreenChangeObservers() {
|
||||
screenChangeObserver = NotificationCenter.default.addObserver(
|
||||
forName: NSApplication.didChangeScreenParametersNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
self?.handleScreenGeometryChange(reason: "screen-parameters")
|
||||
}
|
||||
|
||||
let workspaceCenter = NSWorkspace.shared.notificationCenter
|
||||
let wakeObserver = workspaceCenter.addObserver(
|
||||
forName: NSWorkspace.screensDidWakeNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
self?.handleScreenGeometryChange(reason: "screens-did-wake")
|
||||
}
|
||||
|
||||
workspaceObservers = [wakeObserver]
|
||||
}
|
||||
|
||||
private func handleScreenGeometryChange(reason: String) {
|
||||
guard windows.isEmpty == false else { return }
|
||||
repositionAll(animated: false)
|
||||
print("ExtensionLockScreenWidgetWindowPool: realigned windows due to \(reason)")
|
||||
}
|
||||
|
||||
private func repositionAll(animated: Bool) {
|
||||
guard let screen = currentScreen() else { return }
|
||||
for (id, record) in windows {
|
||||
guard let payload = payloadsByID[id] else { continue }
|
||||
let targetFrame = frame(for: payload.descriptor, on: screen)
|
||||
if animated {
|
||||
NSAnimationContext.runAnimationGroup { context in
|
||||
context.duration = 0.22
|
||||
context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||
record.window.animator().setFrame(targetFrame, display: true)
|
||||
}
|
||||
} else {
|
||||
record.window.setFrame(targetFrame, display: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func frame(for descriptor: AtollLockScreenWidgetDescriptor, on screen: NSScreen) -> NSRect {
|
||||
let size = descriptor.size
|
||||
let safeInsets = NSEdgeInsets(top: 140, left: 48, bottom: 100, right: 48)
|
||||
let screenFrame = screen.frame
|
||||
|
||||
let minX = screenFrame.minX + safeInsets.left
|
||||
let maxX = screenFrame.maxX - safeInsets.right - size.width
|
||||
let minY = screenFrame.minY + safeInsets.bottom
|
||||
let maxY = screenFrame.maxY - safeInsets.top - size.height
|
||||
|
||||
var originX: CGFloat
|
||||
switch descriptor.position.alignment {
|
||||
case .leading:
|
||||
originX = minX
|
||||
case .center:
|
||||
originX = screenFrame.midX - (size.width / 2)
|
||||
case .trailing:
|
||||
originX = maxX
|
||||
}
|
||||
|
||||
originX += descriptor.position.horizontalOffset
|
||||
originX = clamp(originX, lower: min(minX, maxX), upper: max(minX, maxX))
|
||||
|
||||
var originY = screenFrame.midY - (size.height / 2) + descriptor.position.verticalOffset
|
||||
originY = clamp(originY, lower: min(minY, maxY), upper: max(minY, maxY))
|
||||
|
||||
return NSRect(x: originX, y: originY, width: size.width, height: size.height)
|
||||
}
|
||||
|
||||
private func clamp(_ value: CGFloat, lower: CGFloat, upper: CGFloat) -> CGFloat {
|
||||
guard lower <= upper else { return value }
|
||||
return min(max(value, lower), upper)
|
||||
}
|
||||
|
||||
private func currentScreen() -> NSScreen? {
|
||||
LockScreenDisplayContextProvider.shared.contextSnapshot()?.screen ?? NSScreen.main
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue