Fix UI display issues and improve macOS Tahoe/Sequoia support by arsarsars1 · Pull Request #1827 · MonitorControl/MonitorControl · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .gitignore
4 changes: 4 additions & 0 deletions MonitorControl.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
F06792F6200A745F0066C438 /* MonitorControlHelper.app in [Login] Copy Helper to start at Login */ = {isa = PBXBuildFile; fileRef = F06792E7200A73460066C438 /* MonitorControlHelper.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
F0A489C4279C71B200BEDFD6 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A489C3279C71B200BEDFD6 /* OnboardingViewController.swift */; };
FE4E0896249D584C003A50BB /* OSDUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE4E0895249D584C003A50BB /* OSDUtils.swift */; };
CE12345678901234003A50BB /* CustomHUD.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE12345678901234003A50BC /* CustomHUD.swift */; };
/* End PBXBuildFile section */

/* Begin PBXCopyFilesBuildPhase section */
Expand Down Expand Up @@ -181,6 +182,7 @@
FB5DB28F2AD54C4600306223 /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/InternetAccessPolicy.strings"; sourceTree = "<group>"; };
FB5DB2902AD54C4600306223 /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = "<group>"; };
FE4E0895249D584C003A50BB /* OSDUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSDUtils.swift; sourceTree = "<group>"; };
CE12345678901234003A50BC /* CustomHUD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomHUD.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -317,6 +319,7 @@
AA16139A26BE772E00DCF027 /* Arm64DDC.swift */,
AA4398A826DD55DA00943F16 /* IntelDDC.swift */,
FE4E0895249D584C003A50BB /* OSDUtils.swift */,
CE12345678901234003A50BC /* CustomHUD.swift */,
);
path = Support;
sourceTree = "<group>";
Expand Down Expand Up @@ -629,6 +632,7 @@
AA062E8E26CA7BE5007E628C /* DisplaysPrefsCellView.swift in Sources */,
AA25F6D726E68C160087F3A2 /* MediaKeyTapManager.swift in Sources */,
FE4E0896249D584C003A50BB /* OSDUtils.swift in Sources */,
CE12345678901234003A50BB /* CustomHUD.swift in Sources */,
6CBFE27A23DB266000D1BC41 /* Display.swift in Sources */,
AA44E70727038F7F00E06865 /* KeyboardShortcutsManager.swift in Sources */,
F03A8DF21FFBAA6F0034DC27 /* OtherDisplay.swift in Sources */,
Expand Down
2 changes: 2 additions & 0 deletions MonitorControl/Enums/PrefKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -212,4 +212,6 @@ enum KeyboardVolume: Int {
case custom = 1
case both = 2
case disabled = 3
/// Like `media`, but always captures volume/mute keys even when macOS can control the default output device.
case mediaForce = 4
}
2 changes: 1 addition & 1 deletion MonitorControl/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>7141</string>
<string>7184</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSMinimumSystemVersion</key>
Expand Down
2 changes: 1 addition & 1 deletion MonitorControl/Support/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}

func checkPermissions(firstAsk: Bool = false) {
let permissionsRequired: Bool = [KeyboardVolume.media.rawValue, KeyboardVolume.both.rawValue].contains(prefs.integer(forKey: PrefKey.keyboardVolume.rawValue)) || [KeyboardBrightness.media.rawValue, KeyboardBrightness.both.rawValue].contains(prefs.integer(forKey: PrefKey.keyboardBrightness.rawValue))
let permissionsRequired: Bool = [KeyboardVolume.media.rawValue, KeyboardVolume.both.rawValue, KeyboardVolume.mediaForce.rawValue].contains(prefs.integer(forKey: PrefKey.keyboardVolume.rawValue)) || [KeyboardBrightness.media.rawValue, KeyboardBrightness.both.rawValue].contains(prefs.integer(forKey: PrefKey.keyboardBrightness.rawValue))
if !MediaKeyTapManager.readPrivileges(prompt: false), permissionsRequired {
MediaKeyTapManager.acquirePrivileges(firstAsk: firstAsk)
}
Expand Down
243 changes: 243 additions & 0 deletions MonitorControl/Support/CustomHUD.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
// Copyright © MonitorControl. @JoniVR, @theOneyouseek, @waydabber and others
// CustomHUD.swift - Custom OSD overlay for macOS Tahoe 26+ compatibility

import Cocoa

// MARK: - Custom HUD Manager

/// Manages custom HUD windows for brightness/volume display on macOS 26+
/// where the native OSD API no longer works correctly
class CustomHUDManager {
static let shared = CustomHUDManager()

private var hudWindows: [CGDirectDisplayID: NSWindow] = [:]
private let hudLock = NSLock()

private init() {}

/// Shows a custom HUD on the specified display
func showHUD(displayID: CGDirectDisplayID, type: HUDType, value: Float, maxValue: Float = 1.0) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }

self.hudLock.lock()
defer { self.hudLock.unlock() }

// Get or create HUD window for this display
let window: NSWindow
if let existingWindow = self.hudWindows[displayID] {
window = existingWindow
} else {
window = self.createHUDWindow(displayID: displayID)
self.hudWindows[displayID] = window
}

// Update and show the HUD
self.updateWindowContent(window: window, displayID: displayID, type: type, value: value, maxValue: maxValue)
self.showWindowWithFade(window: window)
}
}

private func createHUDWindow(displayID: CGDirectDisplayID) -> NSWindow {
let window = NSPanel(
contentRect: NSRect(x: 0, y: 0, width: 280, height: 64),
styleMask: [.nonactivatingPanel, .fullSizeContentView],
backing: .buffered,
defer: false
)

window.level = .floating
window.isFloatingPanel = true
window.hidesOnDeactivate = false
window.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle]
window.isOpaque = false
window.backgroundColor = .clear
window.hasShadow = true
window.isMovable = false
window.isMovableByWindowBackground = false
window.ignoresMouseEvents = true
window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true

positionWindow(window, onDisplay: displayID)

return window
}

private func positionWindow(_ window: NSWindow, onDisplay displayID: CGDirectDisplayID) {
guard let screen = getScreen(for: displayID) else { return }

let screenFrame = screen.visibleFrame
let windowSize = window.frame.size

// Position at center-bottom of screen, above the dock
let x = screenFrame.origin.x + (screenFrame.width - windowSize.width) / 2
let y = screenFrame.origin.y + 80 // 80 points from bottom for better visibility

window.setFrameOrigin(NSPoint(x: x, y: y))
}

private func getScreen(for displayID: CGDirectDisplayID) -> NSScreen? {
return NSScreen.screens.first { screen in
guard let screenNumber = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber else {
return false
}
return CGDirectDisplayID(screenNumber.uint32Value) == displayID
}
}

private func updateWindowContent(window: NSWindow, displayID: CGDirectDisplayID, type: HUDType, value: Float, maxValue: Float) {
// Create content view using AppKit for maximum compatibility
let contentView = createHUDContentView(type: type, value: value, maxValue: maxValue)
window.contentView = contentView

// Re-position in case screen changed
positionWindow(window, onDisplay: displayID)
}

private func createHUDContentView(type: HUDType, value: Float, maxValue: Float) -> NSView {
let w: CGFloat = 280
let h: CGFloat = 64

// Wrap the visual effect view in a transparent root view.
let rootView = NSView(frame: NSRect(x: 0, y: 0, width: w, height: h))
rootView.wantsLayer = true
rootView.layer?.backgroundColor = NSColor.clear.cgColor

let containerView = NSVisualEffectView(frame: NSRect(x: 0, y: 0, width: w, height: h))
containerView.material = .hudWindow
containerView.blendingMode = .behindWindow
containerView.state = .active
containerView.appearance = NSAppearance(named: .vibrantDark)
containerView.wantsLayer = true
containerView.layer?.cornerRadius = h / 2
containerView.layer?.masksToBounds = true

// Add premium liquid glass edge highlight
containerView.layer?.borderWidth = 0.5
containerView.layer?.borderColor = NSColor.white.withAlphaComponent(0.25).cgColor

rootView.addSubview(containerView)

let iconSize: CGFloat = 24
let iconView = NSImageView(frame: NSRect(x: 20, y: (h - iconSize) / 2, width: iconSize, height: iconSize))
iconView.imageScaling = .scaleProportionallyDown
iconView.wantsLayer = true
iconView.layer?.isOpaque = false
iconView.layer?.backgroundColor = NSColor.clear.cgColor

if #available(macOS 11.0, *) {
if let icon = NSImage(systemSymbolName: type.iconSystemName, accessibilityDescription: nil) {
let config = NSImage.SymbolConfiguration(pointSize: iconSize * 0.9, weight: .bold)
iconView.image = icon.withSymbolConfiguration(config)
iconView.contentTintColor = .white
}
} else {
let fallbackIcon: String
switch type {
case .brightness: fallbackIcon = NSImage.touchBarComposeTemplateName
case .volume: fallbackIcon = NSImage.touchBarAudioOutputVolumeHighTemplateName
case .volumeMuted: fallbackIcon = NSImage.touchBarAudioOutputMuteTemplateName
case .contrast: fallbackIcon = NSImage.touchBarColorPickerFillName
}
iconView.image = NSImage(named: fallbackIcon)
iconView.contentTintColor = .white
}
containerView.addSubview(iconView)

// Thick Liquid Slider
let barX: CGFloat = 20 + iconSize + 16
let barW: CGFloat = w - barX - 24 // More compact, no trailing percentage label by default
let barH: CGFloat = 32 // Thicker, liquid-style bar
let barY = (h - barH) / 2

let progressBg = NSBox(frame: NSRect(x: barX, y: barY, width: barW, height: barH))
progressBg.boxType = .custom
progressBg.borderType = .noBorder
progressBg.fillColor = NSColor.white.withAlphaComponent(0.15)
progressBg.cornerRadius = barH / 2
containerView.addSubview(progressBg)

let normalizedValue = CGFloat(min(max(value / maxValue, 0), 1))
let fillWidth = max(barH, barW * normalizedValue)
let progressFill = NSBox(frame: NSRect(x: barX, y: barY, width: fillWidth, height: barH))
progressFill.boxType = .custom
progressFill.borderType = .noBorder
progressFill.fillColor = NSColor.white.withAlphaComponent(0.9)
progressFill.cornerRadius = barH / 2
containerView.addSubview(progressFill)

return rootView
}

private var fadeTimers: [CGDirectDisplayID: Timer] = [:]

private func showWindowWithFade(window: NSWindow) {
// Find the displayID for this window
guard let displayID = hudWindows.first(where: { $0.value === window })?.key else { return }

// Cancel any existing fade timer
fadeTimers[displayID]?.invalidate()

// Make window fully visible
window.alphaValue = 1.0
window.orderFrontRegardless()

// Schedule fade out after 1.5 seconds (common mode so it still fires during nested run-loop work)
let timer = Timer(timeInterval: 1.5, repeats: false) { [weak self, weak window] _ in
guard let window = window else { return }
self?.fadeOut(window: window)
}
fadeTimers[displayID] = timer
RunLoop.main.add(timer, forMode: .common)
}

private func fadeOut(window: NSWindow) {
NSAnimationContext.runAnimationGroup { context in
context.duration = 0.3
window.animator().alphaValue = 0
} completionHandler: {
window.orderOut(nil)
}
}

/// Cleans up HUD windows for removed displays
func cleanupDisplay(_ displayID: CGDirectDisplayID) {
hudLock.lock()
defer { hudLock.unlock() }

fadeTimers[displayID]?.invalidate()
fadeTimers.removeValue(forKey: displayID)

if let window = hudWindows[displayID] {
window.close()
hudWindows.removeValue(forKey: displayID)
}
}
}

// MARK: - HUD Type

enum HUDType {
case brightness
case volume
case volumeMuted
case contrast

var iconSystemName: String {
switch self {
case .brightness: return "sun.max.fill"
case .volume: return "speaker.wave.2.fill"
case .volumeMuted: return "speaker.slash.fill"
case .contrast: return "circle.lefthalf.filled"
}
}

var iconNSColor: NSColor {
switch self {
case .brightness: return .white.withAlphaComponent(0.9)
case .volume, .volumeMuted: return .white.withAlphaComponent(0.9)
case .contrast: return .white.withAlphaComponent(0.8)
}
}
}
3 changes: 3 additions & 0 deletions MonitorControl/Support/DisplayManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,9 @@ class DisplayManager {
}

func clearDisplays() {
for display in self.displays {
CustomHUDManager.shared.cleanupDisplay(display.identifier)
}
self.displays = []
}

Expand Down
6 changes: 3 additions & 3 deletions MonitorControl/Support/MediaKeyTapManager.swift
Loading