mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-28 15:44:32 +00:00
Update BetterDisplay
This commit is contained in:
parent
dd890bd5e5
commit
d8ca8ceb02
3 changed files with 111 additions and 22 deletions
|
|
@ -4795,6 +4795,9 @@
|
||||||
"BetterDisplay Integration" : {
|
"BetterDisplay Integration" : {
|
||||||
"comment" : "The header text for the \"BetterDisplay Integration\" section in the settings view.",
|
"comment" : "The header text for the \"BetterDisplay Integration\" section in the settings view.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"BetterDisplay is installed but not currently running. Launch BetterDisplay to enable integration." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"BetterDisplay OSD events will be routed through Atoll's active HUD style. Make sure BetterDisplay's OSD integration is enabled in its Settings › Application › Integration." : {
|
"BetterDisplay OSD events will be routed through Atoll's active HUD style. Make sure BetterDisplay's OSD integration is enabled in its Settings › Application › Integration." : {
|
||||||
|
|
||||||
|
|
@ -8517,10 +8520,6 @@
|
||||||
"comment" : "Name of a user profile for a designer.",
|
"comment" : "Name of a user profile for a designer.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Detected" : {
|
|
||||||
"comment" : "A label indicating that BetterDisplay is detected on the system.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Detection Status" : {
|
"Detection Status" : {
|
||||||
"comment" : "The header for a section that displays whether the screen recording detection feature is currently active or inactive.",
|
"comment" : "The header for a section that displays whether the screen recording detection feature is currently active or inactive.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
|
|
@ -17410,9 +17409,6 @@
|
||||||
"Not charging" : {
|
"Not charging" : {
|
||||||
"comment" : "Text displayed in the status text when the battery is not charging.",
|
"comment" : "Text displayed in the status text when the battery is not charging.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
|
||||||
"Not detected" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Not Now" : {
|
"Not Now" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
|
||||||
|
|
@ -1934,6 +1934,26 @@ private struct BetterDisplayIntegrationSection: View {
|
||||||
SettingsTab.hudAndOSD.highlightID(for: title)
|
SettingsTab.hudAndOSD.highlightID(for: title)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var statusText: String {
|
||||||
|
if betterDisplayManager.isRunning {
|
||||||
|
return "Running"
|
||||||
|
} else if betterDisplayManager.isDetected {
|
||||||
|
return "Not running"
|
||||||
|
} else {
|
||||||
|
return "Not detected"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var statusColor: Color {
|
||||||
|
if betterDisplayManager.isRunning {
|
||||||
|
return .green
|
||||||
|
} else if betterDisplayManager.isDetected {
|
||||||
|
return .orange
|
||||||
|
} else {
|
||||||
|
return .secondary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
|
|
@ -1941,9 +1961,9 @@ private struct BetterDisplayIntegrationSection: View {
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text("BetterDisplay")
|
Text("BetterDisplay")
|
||||||
.font(.system(size: 13, weight: .medium))
|
.font(.system(size: 13, weight: .medium))
|
||||||
Text(betterDisplayManager.isDetected ? "Detected" : "Not detected")
|
Text(statusText)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(betterDisplayManager.isDetected ? .green : .secondary)
|
.foregroundStyle(statusColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
@ -1959,6 +1979,10 @@ private struct BetterDisplayIntegrationSection: View {
|
||||||
Text("Install [BetterDisplay](https://betterdisplay.pro) to control external display brightness and volume through Atoll's HUD.")
|
Text("Install [BetterDisplay](https://betterdisplay.pro) to control external display brightness and volume through Atoll's HUD.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
} else if !betterDisplayManager.isRunning {
|
||||||
|
Text("BetterDisplay is installed but not currently running. Launch BetterDisplay to enable integration.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
} else if enableBetterDisplayIntegration {
|
} else if enableBetterDisplayIntegration {
|
||||||
Text("BetterDisplay OSD events will be routed through Atoll's active HUD style. Make sure BetterDisplay's OSD integration is enabled in its Settings › Application › Integration.")
|
Text("BetterDisplay OSD events will be routed through Atoll's active HUD style. Make sure BetterDisplay's OSD integration is enabled in its Settings › Application › Integration.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
|
|
|
||||||
|
|
@ -93,23 +93,33 @@ final class BetterDisplayManager: ObservableObject {
|
||||||
private static let osdNotificationName = NSNotification.Name("com.betterdisplay.BetterDisplay.osd")
|
private static let osdNotificationName = NSNotification.Name("com.betterdisplay.BetterDisplay.osd")
|
||||||
private static let requestNotificationName = NSNotification.Name("com.betterdisplay.BetterDisplay.request")
|
private static let requestNotificationName = NSNotification.Name("com.betterdisplay.BetterDisplay.request")
|
||||||
private static let responseNotificationName = NSNotification.Name("com.betterdisplay.BetterDisplay.response")
|
private static let responseNotificationName = NSNotification.Name("com.betterdisplay.BetterDisplay.response")
|
||||||
|
private static let launchedNotificationName = NSNotification.Name("pro.betterdisplay.BetterDisplay.launched")
|
||||||
|
private static let terminatedNotificationName = NSNotification.Name("pro.betterdisplay.BetterDisplay.terminated")
|
||||||
|
|
||||||
// MARK: Published state
|
// MARK: Published state
|
||||||
|
|
||||||
/// Whether BetterDisplay is currently detected on this machine.
|
/// Whether BetterDisplay is currently detected (installed) on this machine.
|
||||||
@Published private(set) var isDetected: Bool = false
|
@Published private(set) var isDetected: Bool = false
|
||||||
|
|
||||||
|
/// Whether BetterDisplay is currently running and ready for communication.
|
||||||
|
@Published private(set) var isRunning: Bool = false
|
||||||
|
|
||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
private var osdObserver: NSObjectProtocol?
|
private var osdObserver: NSObjectProtocol?
|
||||||
private var responseObserver: NSObjectProtocol?
|
private var responseObserver: NSObjectProtocol?
|
||||||
private var workspaceObserver: NSObjectProtocol?
|
private var workspaceObserver: NSObjectProtocol?
|
||||||
|
private var workspaceTermObserver: NSObjectProtocol?
|
||||||
|
private var launchedObserver: NSObjectProtocol?
|
||||||
|
private var terminatedObserver: NSObjectProtocol?
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
private weak var coordinator: DynamicIslandViewCoordinator?
|
private weak var coordinator: DynamicIslandViewCoordinator?
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
isDetected = Self.checkInstallation()
|
isDetected = Self.checkInstallation()
|
||||||
|
isRunning = Self.checkRunning()
|
||||||
setupWorkspaceObserver()
|
setupWorkspaceObserver()
|
||||||
|
setupLifecycleObservers()
|
||||||
setupSettingsObserver()
|
setupSettingsObserver()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,8 +129,8 @@ final class BetterDisplayManager: ObservableObject {
|
||||||
func configure(coordinator: DynamicIslandViewCoordinator) {
|
func configure(coordinator: DynamicIslandViewCoordinator) {
|
||||||
self.coordinator = coordinator
|
self.coordinator = coordinator
|
||||||
|
|
||||||
// Start listening if integration is enabled and BetterDisplay is detected
|
// Start listening if integration is enabled and BetterDisplay is running
|
||||||
if Defaults[.enableBetterDisplayIntegration] && isDetected {
|
if Defaults[.enableBetterDisplayIntegration] && isRunning {
|
||||||
startListening()
|
startListening()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -128,18 +138,19 @@ final class BetterDisplayManager: ObservableObject {
|
||||||
/// Refresh detection status (e.g. after app install/uninstall).
|
/// Refresh detection status (e.g. after app install/uninstall).
|
||||||
func refreshDetectionStatus() {
|
func refreshDetectionStatus() {
|
||||||
let wasDetected = isDetected
|
let wasDetected = isDetected
|
||||||
|
let wasRunning = isRunning
|
||||||
isDetected = Self.checkInstallation()
|
isDetected = Self.checkInstallation()
|
||||||
if wasDetected && !isDetected {
|
isRunning = Self.checkRunning()
|
||||||
|
if wasRunning && !isRunning {
|
||||||
stopListening()
|
stopListening()
|
||||||
} else if !wasDetected && isDetected && Defaults[.enableBetterDisplayIntegration] {
|
} else if !wasRunning && isRunning && Defaults[.enableBetterDisplayIntegration] {
|
||||||
startListening()
|
startListening()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Detection
|
// MARK: - Detection
|
||||||
|
|
||||||
/// Check if BetterDisplay is installed by looking for its bundle ID in running apps
|
/// Check if BetterDisplay is installed by looking for its bundle ID.
|
||||||
/// and falling back to NSWorkspace URL lookup.
|
|
||||||
static func checkInstallation() -> Bool {
|
static func checkInstallation() -> Bool {
|
||||||
// Check running apps first (fast path)
|
// Check running apps first (fast path)
|
||||||
if NSWorkspace.shared.runningApplications.contains(where: { $0.bundleIdentifier == bundleID }) {
|
if NSWorkspace.shared.runningApplications.contains(where: { $0.bundleIdentifier == bundleID }) {
|
||||||
|
|
@ -154,6 +165,11 @@ final class BetterDisplayManager: ObservableObject {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if BetterDisplay is currently running.
|
||||||
|
static func checkRunning() -> Bool {
|
||||||
|
NSWorkspace.shared.runningApplications.contains(where: { $0.bundleIdentifier == bundleID })
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - OSD Listening
|
// MARK: - OSD Listening
|
||||||
|
|
||||||
private func startListening() {
|
private func startListening() {
|
||||||
|
|
@ -183,7 +199,7 @@ final class BetterDisplayManager: ObservableObject {
|
||||||
// MARK: - OSD Handling
|
// MARK: - OSD Handling
|
||||||
|
|
||||||
private func handleOSDNotification(_ notification: Notification) {
|
private func handleOSDNotification(_ notification: Notification) {
|
||||||
guard Defaults[.enableBetterDisplayIntegration] else { return }
|
guard Defaults[.enableBetterDisplayIntegration], isRunning else { return }
|
||||||
|
|
||||||
guard let notificationString = notification.object as? String else {
|
guard let notificationString = notification.object as? String else {
|
||||||
NSLog("⚠️ BetterDisplay OSD: unexpected notification format")
|
NSLog("⚠️ BetterDisplay OSD: unexpected notification format")
|
||||||
|
|
@ -345,6 +361,12 @@ final class BetterDisplayManager: ObservableObject {
|
||||||
parameters: [String: String?],
|
parameters: [String: String?],
|
||||||
completion: (@MainActor @Sendable (BetterDisplayResponseData?) -> Void)? = nil
|
completion: (@MainActor @Sendable (BetterDisplayResponseData?) -> Void)? = nil
|
||||||
) {
|
) {
|
||||||
|
guard isRunning else {
|
||||||
|
NSLog("⚠️ BetterDisplay sendRequest skipped — app is not running")
|
||||||
|
completion?(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let uuid = UUID().uuidString
|
let uuid = UUID().uuidString
|
||||||
let request = BetterDisplayRequestData(uuid: uuid, commands: commands, parameters: parameters)
|
let request = BetterDisplayRequestData(uuid: uuid, commands: commands, parameters: parameters)
|
||||||
|
|
||||||
|
|
@ -429,16 +451,54 @@ final class BetterDisplayManager: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also observe app termination
|
// Also observe app termination — handles crashes and force-quits
|
||||||
NSWorkspace.shared.notificationCenter.addObserver(
|
// (orderly quits are caught by the lifecycle observer below)
|
||||||
|
workspaceTermObserver = NSWorkspace.shared.notificationCenter.addObserver(
|
||||||
forName: NSWorkspace.didTerminateApplicationNotification,
|
forName: NSWorkspace.didTerminateApplicationNotification,
|
||||||
object: nil,
|
object: nil,
|
||||||
queue: .main
|
queue: .main
|
||||||
) { notification in
|
) { [weak self] notification in
|
||||||
guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication,
|
guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication,
|
||||||
app.bundleIdentifier == betterDisplayBundleID
|
app.bundleIdentifier == betterDisplayBundleID
|
||||||
else { return }
|
else { return }
|
||||||
// Don't clear isDetected on termination — app is still installed, just not running.
|
Task { @MainActor in
|
||||||
|
NSLog("🔴 BetterDisplay terminated (workspace notification)")
|
||||||
|
self?.isRunning = false
|
||||||
|
self?.stopListening()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Lifecycle Observers (BetterDisplay launched/terminated notifications)
|
||||||
|
|
||||||
|
/// Listen for distributed notifications sent by BetterDisplay itself
|
||||||
|
/// to know when it becomes ready and when it shuts down cleanly.
|
||||||
|
private func setupLifecycleObservers() {
|
||||||
|
launchedObserver = DistributedNotificationCenter.default().addObserver(
|
||||||
|
forName: Self.launchedNotificationName,
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { [weak self] _ in
|
||||||
|
Task { @MainActor in
|
||||||
|
NSLog("🟢 BetterDisplay launched notification received")
|
||||||
|
self?.isDetected = true
|
||||||
|
self?.isRunning = true
|
||||||
|
if Defaults[.enableBetterDisplayIntegration] {
|
||||||
|
self?.startListening()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
terminatedObserver = DistributedNotificationCenter.default().addObserver(
|
||||||
|
forName: Self.terminatedNotificationName,
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { [weak self] _ in
|
||||||
|
Task { @MainActor in
|
||||||
|
NSLog("🔴 BetterDisplay terminated notification received")
|
||||||
|
self?.isRunning = false
|
||||||
|
self?.stopListening()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -449,7 +509,7 @@ final class BetterDisplayManager: ObservableObject {
|
||||||
.sink { [weak self] change in
|
.sink { [weak self] change in
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
if change.newValue && self.isDetected {
|
if change.newValue && self.isRunning {
|
||||||
self.startListening()
|
self.startListening()
|
||||||
} else {
|
} else {
|
||||||
self.stopListening()
|
self.stopListening()
|
||||||
|
|
@ -463,9 +523,18 @@ final class BetterDisplayManager: ObservableObject {
|
||||||
if let osdObserver {
|
if let osdObserver {
|
||||||
DistributedNotificationCenter.default().removeObserver(osdObserver)
|
DistributedNotificationCenter.default().removeObserver(osdObserver)
|
||||||
}
|
}
|
||||||
|
if let launchedObserver {
|
||||||
|
DistributedNotificationCenter.default().removeObserver(launchedObserver)
|
||||||
|
}
|
||||||
|
if let terminatedObserver {
|
||||||
|
DistributedNotificationCenter.default().removeObserver(terminatedObserver)
|
||||||
|
}
|
||||||
if let workspaceObserver {
|
if let workspaceObserver {
|
||||||
NSWorkspace.shared.notificationCenter.removeObserver(workspaceObserver)
|
NSWorkspace.shared.notificationCenter.removeObserver(workspaceObserver)
|
||||||
}
|
}
|
||||||
|
if let workspaceTermObserver {
|
||||||
|
NSWorkspace.shared.notificationCenter.removeObserver(workspaceTermObserver)
|
||||||
|
}
|
||||||
cancellables.removeAll()
|
cancellables.removeAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue