From cbe2fa7e85047c7ae593af0d267fe7255b76570c Mon Sep 17 00:00:00 2001 From: Abdul Rehman Date: Sat, 27 Dec 2025 10:53:02 +0500 Subject: [PATCH 01/12] Fix UI display issues and improve macOS Tahoe support - **Menu Bar Slider**: - Redesigned layout to column style (icon above slider). - Aligned icon to left-start. - Increased vertical spacing and view height for better touch target and aesthetics. - Fixed icon vs slider overlap issues. - **Preferences UI**: - **Displays**: Fixed truncated friendly name text field, increased height, and improved vertical alignment. - **About**: Fixed truncated app name text. - **Keyboard**: Improved layout of accessibility troubleshooting buttons (Help/Reset), added container/label for better grid alignment. - **macOS Tahoe Support**: - Added accessibility permission reset functionality. - Implemented Custom HUD fallback for cases where native OSD is broken. --- MonitorControl.xcodeproj/project.pbxproj | 4 + MonitorControl/Info.plist | 2 +- MonitorControl/Support/CustomHUD.swift | 225 ++++++++++++++++++ MonitorControl/Support/OSDUtils.swift | 47 ++++ MonitorControl/Support/SliderHandler.swift | 10 +- MonitorControl/UI/Base.lproj/Main.storyboard | 10 +- .../UI/cs.lproj/Localizable.strings | 2 +- .../KeyboardPrefsViewController.swift | 139 +++++++++++ MonitorControlHelper/Info.plist | 2 +- 9 files changed, 429 insertions(+), 12 deletions(-) create mode 100644 MonitorControl/Support/CustomHUD.swift diff --git a/MonitorControl.xcodeproj/project.pbxproj b/MonitorControl.xcodeproj/project.pbxproj index 0583586..47e741f 100644 --- a/MonitorControl.xcodeproj/project.pbxproj +++ b/MonitorControl.xcodeproj/project.pbxproj @@ -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 */ @@ -181,6 +182,7 @@ FB5DB28F2AD54C4600306223 /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/InternetAccessPolicy.strings"; sourceTree = ""; }; FB5DB2902AD54C4600306223 /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = ""; }; FE4E0895249D584C003A50BB /* OSDUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSDUtils.swift; sourceTree = ""; }; + CE12345678901234003A50BC /* CustomHUD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomHUD.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -317,6 +319,7 @@ AA16139A26BE772E00DCF027 /* Arm64DDC.swift */, AA4398A826DD55DA00943F16 /* IntelDDC.swift */, FE4E0895249D584C003A50BB /* OSDUtils.swift */, + CE12345678901234003A50BC /* CustomHUD.swift */, ); path = Support; sourceTree = ""; @@ -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 */, diff --git a/MonitorControl/Info.plist b/MonitorControl/Info.plist index 69810af..a992cf1 100644 --- a/MonitorControl/Info.plist +++ b/MonitorControl/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 7141 + 7168 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion diff --git a/MonitorControl/Support/CustomHUD.swift b/MonitorControl/Support/CustomHUD.swift new file mode 100644 index 0000000..8136c33 --- /dev/null +++ b/MonitorControl/Support/CustomHUD.swift @@ -0,0 +1,225 @@ +// Copyright © MonitorControl. @JoniVR, @theOneyouseek, @waydabber and others +// CustomHUD.swift - Custom OSD overlay for macOS Tahoe 26+ compatibility + +import Cocoa + +#if swift(>=5.3) +import SwiftUI +#endif + +// 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: 220, height: 60), + styleMask: [.nonactivatingPanel, .hudWindow], + 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 + + 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 + 100 // 100 points from bottom + + 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 containerView = NSVisualEffectView(frame: NSRect(x: 0, y: 0, width: 220, height: 60)) + containerView.material = .hudWindow + containerView.blendingMode = .behindWindow + containerView.state = .active + containerView.wantsLayer = true + containerView.layer?.cornerRadius = 12 + containerView.layer?.masksToBounds = true + + // Icon + let iconView = NSImageView(frame: NSRect(x: 16, y: 18, width: 24, height: 24)) + if #available(macOS 11.0, *) { + if let icon = NSImage(systemSymbolName: type.iconSystemName, accessibilityDescription: nil) { + iconView.image = icon + iconView.contentTintColor = type.iconNSColor + } + } else { + // Fallback for older macOS: use bundled or system icons + 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 = type.iconNSColor + } + containerView.addSubview(iconView) + + // Progress bar background + let progressBg = NSView(frame: NSRect(x: 52, y: 26, width: 108, height: 8)) + progressBg.wantsLayer = true + progressBg.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.2).cgColor + progressBg.layer?.cornerRadius = 4 + containerView.addSubview(progressBg) + + // Progress bar fill + let normalizedValue = CGFloat(min(max(value / maxValue, 0), 1)) + let progressFill = NSView(frame: NSRect(x: 52, y: 26, width: 108 * normalizedValue, height: 8)) + progressFill.wantsLayer = true + progressFill.layer?.backgroundColor = NSColor.white.cgColor + progressFill.layer?.cornerRadius = 4 + containerView.addSubview(progressFill) + + // Percentage label + let percentage = Int(normalizedValue * 100) + let label = NSTextField(labelWithString: "\(percentage)%") + label.frame = NSRect(x: 168, y: 20, width: 42, height: 20) + label.font = NSFont.monospacedDigitSystemFont(ofSize: 14, weight: .medium) + label.textColor = .white + label.alignment = .right + containerView.addSubview(label) + + return containerView + } + + 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 + fadeTimers[displayID] = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { [weak self, weak window] _ in + guard let window = window else { return } + self?.fadeOut(window: window) + } + } + + 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 .systemYellow + case .volume, .volumeMuted: return .systemBlue + case .contrast: return .systemGray + } + } +} diff --git a/MonitorControl/Support/OSDUtils.swift b/MonitorControl/Support/OSDUtils.swift index a250273..60623d6 100644 --- a/MonitorControl/Support/OSDUtils.swift +++ b/MonitorControl/Support/OSDUtils.swift @@ -21,7 +21,37 @@ class OSDUtils: NSObject { return osdImage } + /// Check if we're running on macOS 26 (Tahoe) or later where native OSD is broken + private static var shouldUseCustomHUD: Bool { + if #available(macOS 26, *) { + return true + } + return false + } + + /// Convert Command to HUDType for custom HUD + private static func getHUDType(command: Command, value: Float) -> HUDType { + switch command { + case .audioSpeakerVolume: + return value > 0 ? .volume : .volumeMuted + case .audioMuteScreenBlank: + return .volumeMuted + case .contrast: + return .contrast + default: + return .brightness + } + } + static func showOsd(displayID: CGDirectDisplayID, command: Command, value: Float, maxValue: Float = 1, roundChiclet: Bool = false, lock: Bool = false) { + // Use custom HUD on macOS 26+ where native OSD is broken + if shouldUseCustomHUD { + let hudType = getHUDType(command: command, value: value) + CustomHUDManager.shared.showHUD(displayID: displayID, type: hudType, value: value, maxValue: maxValue) + return + } + + // Fallback to native OSD for older macOS versions guard let manager = OSDManager.sharedManager() as? OSDManager else { return } @@ -40,6 +70,11 @@ class OSDUtils: NSObject { } static func showOsdVolumeDisabled(displayID: CGDirectDisplayID) { + if shouldUseCustomHUD { + CustomHUDManager.shared.showHUD(displayID: displayID, type: .volumeMuted, value: 0, maxValue: 1) + return + } + guard let manager = OSDManager.sharedManager() as? OSDManager else { return } @@ -47,6 +82,11 @@ class OSDUtils: NSObject { } static func showOsdMuteDisabled(displayID: CGDirectDisplayID) { + if shouldUseCustomHUD { + CustomHUDManager.shared.showHUD(displayID: displayID, type: .volumeMuted, value: 0, maxValue: 1) + return + } + guard let manager = OSDManager.sharedManager() as? OSDManager else { return } @@ -54,6 +94,12 @@ class OSDUtils: NSObject { } static func popEmptyOsd(displayID: CGDirectDisplayID, command: Command) { + if shouldUseCustomHUD { + let hudType = getHUDType(command: command, value: 0) + CustomHUDManager.shared.showHUD(displayID: displayID, type: hudType, value: 0, maxValue: 1) + return + } + guard let manager = OSDManager.sharedManager() as? OSDManager else { return } @@ -75,3 +121,4 @@ class OSDUtils: NSObject { abs(chiclet.rounded(.towardZero) - chiclet) } } + diff --git a/MonitorControl/Support/SliderHandler.swift b/MonitorControl/Support/SliderHandler.swift index 1d7d20e..ff98e78 100644 --- a/MonitorControl/Support/SliderHandler.swift +++ b/MonitorControl/Support/SliderHandler.swift @@ -219,8 +219,8 @@ class SliderHandler { self.slider = slider if !DEBUG_MACOS10, #available(macOS 11.0, *) { slider.frame.size.width = 180 - slider.frame.origin = NSPoint(x: 15, y: 5) - let view = NSView(frame: NSRect(x: 0, y: 0, width: slider.frame.width + 30 + (showPercent ? 38 : 0), height: slider.frame.height + 14)) + slider.frame.origin = NSPoint(x: 15, y: 8) + let view = NSView(frame: NSRect(x: 0, y: 0, width: slider.frame.width + 30 + (showPercent ? 38 : 0), height: slider.frame.height + 42)) view.frame.origin = NSPoint(x: 12, y: 0) var iconName = "circle.dashed" switch command { @@ -232,13 +232,15 @@ class SliderHandler { let icon = SliderHandler.ClickThroughImageView() icon.image = NSImage(systemSymbolName: iconName, accessibilityDescription: title) icon.contentTintColor = NSColor.black.withAlphaComponent(0.6) - icon.frame = NSRect(x: view.frame.origin.x + 6.5, y: view.frame.origin.y + 13, width: 15, height: 15) + // Position icon at horizontal left (start), 8px above slider + let iconSize: CGFloat = 18 + icon.frame = NSRect(x: slider.frame.origin.x, y: slider.frame.origin.y + slider.frame.height + 8, width: iconSize, height: iconSize) icon.imageAlignment = .alignCenter view.addSubview(slider) view.addSubview(icon) self.icon = icon if showPercent { - let percentageBox = NSTextField(frame: NSRect(x: 15 + slider.frame.size.width - 2, y: 17, width: 40, height: 12)) + let percentageBox = NSTextField(frame: NSRect(x: 15 + slider.frame.size.width - 2, y: slider.frame.origin.y + (slider.frame.height - 12) / 2, width: 40, height: 12)) self.setupPercentageBox(percentageBox) self.percentageBox = percentageBox view.addSubview(percentageBox) diff --git a/MonitorControl/UI/Base.lproj/Main.storyboard b/MonitorControl/UI/Base.lproj/Main.storyboard index f5a9bb4..c269c53 100644 --- a/MonitorControl/UI/Base.lproj/Main.storyboard +++ b/MonitorControl/UI/Base.lproj/Main.storyboard @@ -709,7 +709,7 @@ - + @@ -1164,7 +1164,7 @@ - + @@ -1280,7 +1280,7 @@ - + @@ -1340,7 +1340,7 @@ - + @@ -1987,7 +1987,7 @@ - + diff --git a/MonitorControl/UI/cs.lproj/Localizable.strings b/MonitorControl/UI/cs.lproj/Localizable.strings index 1256215..6ef2563 100644 --- a/MonitorControl/UI/cs.lproj/Localizable.strings +++ b/MonitorControl/UI/cs.lproj/Localizable.strings @@ -8,7 +8,7 @@ "App menu" = "Nabídka"; /* Shown in the alert dialog */ -"Are you sure you want to enable a longer delay? Doing so may freeze your system and require a restart. Start at login will be disabled as a safety measure." = "Opravdu chcete zapnout delší prodlevu? Může dojít k zamrznutí systému, což by vyžadovalo restart. Volba "Spustit po přihlášení" se z bezpečnostních důvodů vypne."; +"Are you sure you want to enable a longer delay? Doing so may freeze your system and require a restart. Start at login will be disabled as a safety measure." = "Opravdu chcete zapnout delší prodlevu? Může dojít k zamrznutí systému, což by vyžadovalo restart. Volba \\\"Spustit po přihlášení\\\" se z bezpečnostních důvodů vypne."; /* Shown in the alert dialog */ "Are you sure you want to reset all settings?" = "Opravdu chcete obnovit všechna nastavení?"; diff --git a/MonitorControl/View Controllers/Preferences/KeyboardPrefsViewController.swift b/MonitorControl/View Controllers/Preferences/KeyboardPrefsViewController.swift index 8bccf3a..74eddfc 100644 --- a/MonitorControl/View Controllers/Preferences/KeyboardPrefsViewController.swift +++ b/MonitorControl/View Controllers/Preferences/KeyboardPrefsViewController.swift @@ -4,6 +4,7 @@ import Cocoa import KeyboardShortcuts import ServiceManagement import Settings +import os.log class KeyboardPrefsViewController: NSViewController, SettingsPane { let paneIdentifier = Settings.PaneIdentifier.keyboard @@ -46,6 +47,10 @@ class KeyboardPrefsViewController: NSViewController, SettingsPane { @IBOutlet var rowUseAudioMouseText: NSGridRow! @IBOutlet var rowUseAudioNameText: NSGridRow! + // Accessibility troubleshooting UI + var accessibilityHelpButton: NSButton? + var resetAccessibilityButton: NSButton? + func updateGridLayout() { if self.keyboardBrightness.selectedTag() == KeyboardBrightness.media.rawValue { self.rowKeyboardBrightnessPopUp.bottomPadding = -13 @@ -141,9 +146,142 @@ class KeyboardPrefsViewController: NSViewController, SettingsPane { self.customVolumeDown.addSubview(customVolumeDownRecorder) self.customMute.addSubview(customMuteRecorder) + self.setupAccessibilityTroubleshootingUI() self.populateSettings() } + // MARK: - Accessibility Troubleshooting UI + + private func setupAccessibilityTroubleshootingUI() { + // Create container view - height matches other grid rows + let containerHeight: CGFloat = 25 + let containerView = NSView() + containerView.translatesAutoresizingMaskIntoConstraints = false + + // Create "Troubleshooting:" label to match the existing UI style + let label = NSTextField(labelWithString: NSLocalizedString("Troubleshooting:", comment: "Label for troubleshooting section")) + label.font = NSFont.systemFont(ofSize: 13) + label.textColor = NSColor.labelColor + label.alignment = .right + label.translatesAutoresizingMaskIntoConstraints = false + + // Create help button with system help style + let helpButton = NSButton() + helpButton.bezelStyle = .helpButton + helpButton.title = "" + helpButton.toolTip = NSLocalizedString("Keyboard shortcuts troubleshooting", comment: "Tooltip for help button") + helpButton.target = self + helpButton.action = #selector(showAccessibilityHelp(_:)) + helpButton.translatesAutoresizingMaskIntoConstraints = false + self.accessibilityHelpButton = helpButton + + // Create Reset Accessibility button + let resetButton = NSButton() + resetButton.title = NSLocalizedString("Reset Accessibility Permission", comment: "Button to reset accessibility") + resetButton.bezelStyle = .rounded + resetButton.toolTip = NSLocalizedString("Reset and re-request accessibility permission (fixes keyboard shortcuts on macOS Tahoe)", comment: "Tooltip for reset button") + resetButton.target = self + resetButton.action = #selector(resetAccessibilityPermission(_:)) + resetButton.translatesAutoresizingMaskIntoConstraints = false + self.resetAccessibilityButton = resetButton + + // Add all elements to container + containerView.addSubview(label) + containerView.addSubview(helpButton) + containerView.addSubview(resetButton) + + // Add container to the main view + self.view.addSubview(containerView) + + // Layout constraints - positioning to match grid spacing (10px from grid bottom) + NSLayoutConstraint.activate([ + // Container positioning - bottom of view with grid-like padding + containerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 20), + containerView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -20), + containerView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -18), + containerView.heightAnchor.constraint(equalToConstant: containerHeight), + + // Label - right aligned at 212 points width to match storyboard column + label.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + label.widthAnchor.constraint(equalToConstant: 210), + label.centerYAnchor.constraint(equalTo: containerView.centerYAnchor), + + // Help button - after label + helpButton.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: 8), + helpButton.centerYAnchor.constraint(equalTo: containerView.centerYAnchor), + + // Reset button - after help button + resetButton.leadingAnchor.constraint(equalTo: helpButton.trailingAnchor, constant: 8), + resetButton.centerYAnchor.constraint(equalTo: containerView.centerYAnchor), + ]) + } + + @objc func showAccessibilityHelp(_ sender: NSButton) { + let alert = NSAlert() + alert.messageText = NSLocalizedString("Keyboard Shortcuts Not Working?", comment: "Alert title") + alert.informativeText = NSLocalizedString(""" +On macOS Tahoe (26+), you may need to reset accessibility permissions for keyboard shortcuts to work. + +To fix this: +1. Click "Reset Accessibility Permission" below +2. When prompted, click "Open System Settings" +3. Enable MonitorControl in the Accessibility list +4. Restart MonitorControl if needed + +Alternatively, go to: +System Settings → Privacy & Security → Accessibility +Remove MonitorControl, then add it back. +""", comment: "Accessibility troubleshooting guide") + alert.alertStyle = .informational + if #available(macOS 11.0, *) { + alert.icon = NSImage(systemSymbolName: "keyboard", accessibilityDescription: nil) + } + alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK button")) + alert.addButton(withTitle: NSLocalizedString("Open Accessibility Settings", comment: "Button to open settings")) + + let response = alert.runModal() + if response == .alertSecondButtonReturn { + self.openAccessibilitySettings() + } + } + + @objc func resetAccessibilityPermission(_ sender: NSButton) { + let alert = NSAlert() + alert.messageText = NSLocalizedString("Reset Accessibility Permission?", comment: "Confirmation alert title") + alert.informativeText = NSLocalizedString("This will reset MonitorControl's accessibility permission. You will need to grant permission again for keyboard shortcuts to work.", comment: "Confirmation message") + alert.alertStyle = .warning + alert.addButton(withTitle: NSLocalizedString("Reset & Re-request", comment: "Reset button")) + alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel button")) + + let response = alert.runModal() + guard response == .alertFirstButtonReturn else { return } + + // Run tccutil to reset accessibility for this app + let bundleId = Bundle.main.bundleIdentifier ?? "me.guillaumeb.MonitorControl" + let process = Process() + process.executableURL = URL(fileURLWithPath: "/usr/bin/tccutil") + process.arguments = ["reset", "Accessibility", bundleId] + + do { + try process.run() + process.waitUntilExit() + os_log("Reset accessibility permission for %{public}@, exit code: %{public}d", type: .info, bundleId, process.terminationStatus) + } catch { + os_log("Failed to reset accessibility: %{public}@", type: .error, error.localizedDescription) + } + + // Re-prompt for accessibility permission + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + MediaKeyTapManager.acquirePrivileges() + } + } + + private func openAccessibilitySettings() { + if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") { + NSWorkspace.shared.open(url) + } + } + func populateSettings() { self.keyboardBrightness.selectItem(withTag: prefs.integer(forKey: PrefKey.keyboardBrightness.rawValue)) self.keyboardVolume.selectItem(withTag: prefs.integer(forKey: PrefKey.keyboardVolume.rawValue)) @@ -224,3 +362,4 @@ class KeyboardPrefsViewController: NSViewController, SettingsPane { self.updateGridLayout() } } + diff --git a/MonitorControlHelper/Info.plist b/MonitorControlHelper/Info.plist index e6cc0a2..de3f068 100644 --- a/MonitorControlHelper/Info.plist +++ b/MonitorControlHelper/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 7141 + 7168 LSApplicationCategoryType public.app-category.utilities LSBackgroundOnly From 40f9b022aa8d1168e238125265bd570e38361405 Mon Sep 17 00:00:00 2001 From: Abdul Rehman Date: Sat, 27 Dec 2025 21:58:29 +0500 Subject: [PATCH 02/12] Bump build version --- MonitorControl/Info.plist | 2 +- MonitorControlHelper/Info.plist | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MonitorControl/Info.plist b/MonitorControl/Info.plist index a992cf1..94309c9 100644 --- a/MonitorControl/Info.plist +++ b/MonitorControl/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 7168 + 7171 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion diff --git a/MonitorControlHelper/Info.plist b/MonitorControlHelper/Info.plist index de3f068..7ae7926 100644 --- a/MonitorControlHelper/Info.plist +++ b/MonitorControlHelper/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 7168 + 7171 LSApplicationCategoryType public.app-category.utilities LSBackgroundOnly From 8bbef398e10c3a7608b9e1491067f025316dcaeb Mon Sep 17 00:00:00 2001 From: Abdul Rehman Date: Mon, 6 Apr 2026 13:06:12 +0500 Subject: [PATCH 03/12] chore: ignore Xcode and SwiftPM local build directories Made-with: Cursor --- .gitignore | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 639728b..3e3cf4b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,16 @@ Carthage .DS_Store ### Xcode ### -xcuserdata/ \ No newline at end of file +xcuserdata/ +*.xcuserstate +*.xccheckout +*.xcscmblueprint + +### Xcode / SwiftPM (local build outputs beside the project) ### +.build/ +.derivedData/ +build/ +DerivedData/ + +### SwiftPM — user-specific; keep Package.resolved tracked ### +.swiftpm/xcode/xcuserdata/ From 6cf4eba1fc534f59d44639219bbdf92cddacae66 Mon Sep 17 00:00:00 2001 From: Abdul Rehman Date: Mon, 6 Apr 2026 13:06:12 +0500 Subject: [PATCH 04/12] chore: ignore Xcode and SwiftPM local build directories Made-with: Cursor --- .gitignore | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 639728b..3e3cf4b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,16 @@ Carthage .DS_Store ### Xcode ### -xcuserdata/ \ No newline at end of file +xcuserdata/ +*.xcuserstate +*.xccheckout +*.xcscmblueprint + +### Xcode / SwiftPM (local build outputs beside the project) ### +.build/ +.derivedData/ +build/ +DerivedData/ + +### SwiftPM — user-specific; keep Package.resolved tracked ### +.swiftpm/xcode/xcuserdata/ From c6a8fe97b66fdec152479b624b124e218a3d4961 Mon Sep 17 00:00:00 2001 From: Abdul Rehman Date: Mon, 6 Apr 2026 13:43:15 +0500 Subject: [PATCH 05/12] chore: bump version to 7175 and enhance CustomHUD and SliderHandler UI - Updated CFBundleVersion in Info.plist files for both MonitorControl and MonitorControlHelper. - Adjusted HUD dimensions and layout in CustomHUD.swift for improved aesthetics. - Enhanced SliderHandler to support accent fill for better visual distinction in menu symbols. --- MonitorControl/Info.plist | 2 +- MonitorControl/Support/CustomHUD.swift | 60 +++++++++++++--------- MonitorControl/Support/MenuHandler.swift | 37 +++++++------ MonitorControl/Support/SliderHandler.swift | 48 +++++++++++++++-- MonitorControlHelper/Info.plist | 2 +- 5 files changed, 100 insertions(+), 49 deletions(-) diff --git a/MonitorControl/Info.plist b/MonitorControl/Info.plist index 94309c9..39d5cf8 100644 --- a/MonitorControl/Info.plist +++ b/MonitorControl/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 7171 + 7175 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion diff --git a/MonitorControl/Support/CustomHUD.swift b/MonitorControl/Support/CustomHUD.swift index 8136c33..9b69c7f 100644 --- a/MonitorControl/Support/CustomHUD.swift +++ b/MonitorControl/Support/CustomHUD.swift @@ -44,7 +44,7 @@ class CustomHUDManager { private func createHUDWindow(displayID: CGDirectDisplayID) -> NSWindow { let window = NSPanel( - contentRect: NSRect(x: 0, y: 0, width: 220, height: 60), + contentRect: NSRect(x: 0, y: 0, width: 260, height: 56), styleMask: [.nonactivatingPanel, .hudWindow], backing: .buffered, defer: false @@ -98,23 +98,35 @@ class CustomHUDManager { } private func createHUDContentView(type: HUDType, value: Float, maxValue: Float) -> NSView { - let containerView = NSVisualEffectView(frame: NSRect(x: 0, y: 0, width: 220, height: 60)) + let w: CGFloat = 260 + let h: CGFloat = 56 + let containerView = NSVisualEffectView(frame: NSRect(x: 0, y: 0, width: w, height: h)) containerView.material = .hudWindow containerView.blendingMode = .behindWindow containerView.state = .active containerView.wantsLayer = true - containerView.layer?.cornerRadius = 12 + containerView.layer?.cornerRadius = h / 2 containerView.layer?.masksToBounds = true - - // Icon - let iconView = NSImageView(frame: NSRect(x: 16, y: 18, width: 24, height: 24)) + + let iconSize: CGFloat = 26 + let iconView = NSImageView(frame: NSRect(x: 18, 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) { - iconView.image = icon - iconView.contentTintColor = type.iconNSColor + if #available(macOS 12.0, *) { + let base = NSImage.SymbolConfiguration(pointSize: iconSize * 0.85, weight: .semibold) + let palette = NSImage.SymbolConfiguration(paletteColors: [type.iconNSColor]) + iconView.image = icon.withSymbolConfiguration(base.applying(palette)) + iconView.contentTintColor = nil + } else { + iconView.image = icon + iconView.contentTintColor = type.iconNSColor + } } } else { - // Fallback for older macOS: use bundled or system icons let fallbackIcon: String switch type { case .brightness: fallbackIcon = NSImage.touchBarComposeTemplateName @@ -126,31 +138,33 @@ class CustomHUDManager { iconView.contentTintColor = type.iconNSColor } containerView.addSubview(iconView) - - // Progress bar background - let progressBg = NSView(frame: NSRect(x: 52, y: 26, width: 108, height: 8)) + + let barX: CGFloat = 18 + iconSize + 12 + let barW: CGFloat = w - barX - 56 + let barH: CGFloat = 10 + let barY = (h - barH) / 2 + + let progressBg = NSView(frame: NSRect(x: barX, y: barY, width: barW, height: barH)) progressBg.wantsLayer = true - progressBg.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.2).cgColor - progressBg.layer?.cornerRadius = 4 + progressBg.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.22).cgColor + progressBg.layer?.cornerRadius = barH / 2 containerView.addSubview(progressBg) - - // Progress bar fill + let normalizedValue = CGFloat(min(max(value / maxValue, 0), 1)) - let progressFill = NSView(frame: NSRect(x: 52, y: 26, width: 108 * normalizedValue, height: 8)) + let progressFill = NSView(frame: NSRect(x: barX, y: barY, width: max(barH, barW * normalizedValue), height: barH)) progressFill.wantsLayer = true progressFill.layer?.backgroundColor = NSColor.white.cgColor - progressFill.layer?.cornerRadius = 4 + progressFill.layer?.cornerRadius = barH / 2 containerView.addSubview(progressFill) - - // Percentage label + let percentage = Int(normalizedValue * 100) let label = NSTextField(labelWithString: "\(percentage)%") - label.frame = NSRect(x: 168, y: 20, width: 42, height: 20) - label.font = NSFont.monospacedDigitSystemFont(ofSize: 14, weight: .medium) + label.frame = NSRect(x: barX + barW + 8, y: (h - 22) / 2, width: 44, height: 22) + label.font = NSFont.monospacedDigitSystemFont(ofSize: 15, weight: .semibold) label.textColor = .white label.alignment = .right containerView.addSubview(label) - + return containerView } diff --git a/MonitorControl/Support/MenuHandler.swift b/MonitorControl/Support/MenuHandler.swift index 6fcd486..baebdc1 100644 --- a/MonitorControl/Support/MenuHandler.swift +++ b/MonitorControl/Support/MenuHandler.swift @@ -103,25 +103,24 @@ class MenuHandler: NSMenu, NSMenuDelegate { func addDisplayMenuBlock(addedSliderHandlers: [SliderHandler], blockName: String, monitorSubMenu: NSMenu, numOfDisplays: Int, asSubMenu: Bool) { if numOfDisplays > 1, prefs.integer(forKey: PrefKey.multiSliders.rawValue) != MultiSliders.relevant.rawValue, !DEBUG_MACOS10, #available(macOS 11.0, *) { - class BlockView: NSView { - override func draw(_: NSRect) { - let radius = prefs.bool(forKey: PrefKey.showTickMarks.rawValue) ? CGFloat(4) : CGFloat(11) - let outerMargin = CGFloat(15) - let blockRect = self.frame.insetBy(dx: outerMargin, dy: outerMargin / 2 + 2).offsetBy(dx: 0, dy: outerMargin / 2 * -1 + 7) - for i in 1 ... 5 { - let blockPath = NSBezierPath(roundedRect: blockRect.insetBy(dx: CGFloat(i) * -1, dy: CGFloat(i) * -1), xRadius: radius + CGFloat(i) * 0.5, yRadius: radius + CGFloat(i) * 0.5) - NSColor.black.withAlphaComponent(0.1 / CGFloat(i)).setStroke() - blockPath.stroke() - } - let blockPath = NSBezierPath(roundedRect: blockRect, xRadius: radius, yRadius: radius) - if [NSAppearance.Name.darkAqua, NSAppearance.Name.vibrantDark].contains(effectiveAppearance.name) { - NSColor.systemGray.withAlphaComponent(0.3).setStroke() - blockPath.stroke() - } - if ![NSAppearance.Name.darkAqua, NSAppearance.Name.vibrantDark].contains(effectiveAppearance.name) { - NSColor.white.withAlphaComponent(0.5).setFill() - blockPath.fill() - } + class BlockView: NSVisualEffectView { + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + self.material = .popover + self.blendingMode = .behindWindow + self.state = .active + self.isEmphasized = false + self.wantsLayer = true + let tickMarks = prefs.bool(forKey: PrefKey.showTickMarks.rawValue) + self.layer?.cornerRadius = tickMarks ? CGFloat(10) : CGFloat(14) + self.layer?.masksToBounds = true + self.layer?.borderWidth = 0.5 + self.layer?.borderColor = NSColor.separatorColor.withAlphaComponent(0.45).cgColor + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") } } var contentWidth: CGFloat = 0 diff --git a/MonitorControl/Support/SliderHandler.swift b/MonitorControl/Support/SliderHandler.swift index ff98e78..320623f 100644 --- a/MonitorControl/Support/SliderHandler.swift +++ b/MonitorControl/Support/SliderHandler.swift @@ -34,6 +34,8 @@ class SliderHandler { var numOfTickmarks: Int = 0 var isHighlightDisplayItems: Bool = false var displayHighlightItems: [CGDirectDisplayID: Float] = [:] + /// Matches Control Center–style volume (accent) vs. brightness (neutral fill). + var useAccentFill: Bool = false var isTracking: Bool = false @@ -90,7 +92,7 @@ class SliderHandler { let barFilledWidth = (aRect.width - aRect.height) * CGFloat(maxValue) + aRect.height let barFilledRect = NSRect(x: aRect.origin.x, y: aRect.origin.y, width: barFilledWidth, height: aRect.height) let barFilled = NSBezierPath(roundedRect: barFilledRect, xRadius: barRadius, yRadius: barRadius) - self.barFilledFillColor.setFill() + (self.useAccentFill ? NSColor.controlAccentColor : self.barFilledFillColor).setFill() barFilled.fill() let knobMinX = aRect.origin.x + (aRect.width - aRect.height) * CGFloat(minValue) @@ -207,6 +209,30 @@ class SliderHandler { subviews.first { subview in subview.hitTest(point) != nil } } + + func configureForMenuSymbol() { + self.wantsLayer = true + self.layer?.isOpaque = false + self.layer?.backgroundColor = NSColor.clear.cgColor + } + } + + /// Renders menu SF Symbols without multicolor white “matting” around fills (e.g. sun.max.fill). + @available(macOS 11.0, *) + private static func menuSymbolImage(named name: String, pointSize: CGFloat, command: Command) -> NSImage? { + guard let base = NSImage(systemSymbolName: name, accessibilityDescription: nil) else { return nil } + let weight: NSFont.Weight = .medium + let baseConfig = NSImage.SymbolConfiguration(pointSize: pointSize, weight: weight) + if #available(macOS 12.0, *) { + let paletteColor: NSColor + switch command { + case .brightness: paletteColor = .systemYellow + default: paletteColor = .labelColor.withAlphaComponent(0.72) + } + let palette = NSImage.SymbolConfiguration(paletteColors: [paletteColor]) + return base.withSymbolConfiguration(baseConfig.applying(palette)) + } + return base.withSymbolConfiguration(baseConfig) } public init(display: Display?, command: Command, title: String = "", position _: Int = 0) { @@ -216,9 +242,15 @@ class SliderHandler { let showPercent = prefs.bool(forKey: PrefKey.enableSliderPercent.rawValue) slider.isEnabled = true slider.setNumOfCustomTickmarks(prefs.bool(forKey: PrefKey.showTickMarks.rawValue) ? 5 : 0) + if let cell = slider.cell as? MCSliderCell { + cell.useAccentFill = command == .audioSpeakerVolume + } self.slider = slider if !DEBUG_MACOS10, #available(macOS 11.0, *) { - slider.frame.size.width = 180 + slider.frame.size.width = 196 + var sliderFrame = slider.frame + sliderFrame.size.height = max(sliderFrame.size.height, 22) + slider.frame = sliderFrame slider.frame.origin = NSPoint(x: 15, y: 8) let view = NSView(frame: NSRect(x: 0, y: 0, width: slider.frame.width + 30 + (showPercent ? 38 : 0), height: slider.frame.height + 42)) view.frame.origin = NSPoint(x: 12, y: 0) @@ -230,12 +262,18 @@ class SliderHandler { default: break } let icon = SliderHandler.ClickThroughImageView() - icon.image = NSImage(systemSymbolName: iconName, accessibilityDescription: title) - icon.contentTintColor = NSColor.black.withAlphaComponent(0.6) - // Position icon at horizontal left (start), 8px above slider let iconSize: CGFloat = 18 + icon.image = SliderHandler.menuSymbolImage(named: iconName, pointSize: iconSize, command: command) + icon.imageScaling = .scaleProportionallyDown + if #available(macOS 12.0, *) { + icon.contentTintColor = nil + } else { + icon.contentTintColor = command == .brightness ? NSColor.systemYellow : NSColor.labelColor.withAlphaComponent(0.72) + } + // Position icon at horizontal left (start), 8px above slider icon.frame = NSRect(x: slider.frame.origin.x, y: slider.frame.origin.y + slider.frame.height + 8, width: iconSize, height: iconSize) icon.imageAlignment = .alignCenter + icon.configureForMenuSymbol() view.addSubview(slider) view.addSubview(icon) self.icon = icon diff --git a/MonitorControlHelper/Info.plist b/MonitorControlHelper/Info.plist index 7ae7926..00249ad 100644 --- a/MonitorControlHelper/Info.plist +++ b/MonitorControlHelper/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 7171 + 7175 LSApplicationCategoryType public.app-category.utilities LSBackgroundOnly From 8a2e7f1e62953dd9dfd0e5660398afacd11b9512 Mon Sep 17 00:00:00 2001 From: Abdul Rehman Date: Mon, 6 Apr 2026 13:59:26 +0500 Subject: [PATCH 06/12] Tahoe PR: HUD cleanup, force volume keys, slider/CustomHUD fixes - Add KeyboardVolume.mediaForce and UI for always capturing volume/mute keys - Clean up CustomHUD windows when displays are cleared - Fix SliderHandler percentage label update (remove tautology check) - CustomHUD: RunLoop.common timer, drop unused SwiftUI import - Widen volume control popup for new menu item; en Main.strings Made-with: Cursor --- MonitorControl/Enums/PrefKey.swift | 2 ++ MonitorControl/Info.plist | 2 +- MonitorControl/Support/AppDelegate.swift | 2 +- MonitorControl/Support/CustomHUD.swift | 10 ++++------ MonitorControl/Support/DisplayManager.swift | 3 +++ MonitorControl/Support/MediaKeyTapManager.swift | 6 +++--- MonitorControl/Support/SliderHandler.swift | 8 ++------ MonitorControl/UI/Base.lproj/Main.storyboard | 3 ++- MonitorControl/UI/en.lproj/Main.strings | 3 +++ .../Onboarding/OnboardingViewController.swift | 2 +- MonitorControlHelper/Info.plist | 2 +- 11 files changed, 23 insertions(+), 20 deletions(-) diff --git a/MonitorControl/Enums/PrefKey.swift b/MonitorControl/Enums/PrefKey.swift index 76b5f29..3f9a18e 100644 --- a/MonitorControl/Enums/PrefKey.swift +++ b/MonitorControl/Enums/PrefKey.swift @@ -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 } diff --git a/MonitorControl/Info.plist b/MonitorControl/Info.plist index 39d5cf8..e06e3af 100644 --- a/MonitorControl/Info.plist +++ b/MonitorControl/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 7175 + 7176 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion diff --git a/MonitorControl/Support/AppDelegate.swift b/MonitorControl/Support/AppDelegate.swift index 6d1013b..7d7525b 100644 --- a/MonitorControl/Support/AppDelegate.swift +++ b/MonitorControl/Support/AppDelegate.swift @@ -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) } diff --git a/MonitorControl/Support/CustomHUD.swift b/MonitorControl/Support/CustomHUD.swift index 9b69c7f..7a18777 100644 --- a/MonitorControl/Support/CustomHUD.swift +++ b/MonitorControl/Support/CustomHUD.swift @@ -3,10 +3,6 @@ import Cocoa -#if swift(>=5.3) -import SwiftUI -#endif - // MARK: - Custom HUD Manager /// Manages custom HUD windows for brightness/volume display on macOS 26+ @@ -181,11 +177,13 @@ class CustomHUDManager { window.alphaValue = 1.0 window.orderFrontRegardless() - // Schedule fade out after 1.5 seconds - fadeTimers[displayID] = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { [weak self, weak window] _ in + // 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) { diff --git a/MonitorControl/Support/DisplayManager.swift b/MonitorControl/Support/DisplayManager.swift index f53d5bb..babf0c3 100644 --- a/MonitorControl/Support/DisplayManager.swift +++ b/MonitorControl/Support/DisplayManager.swift @@ -311,6 +311,9 @@ class DisplayManager { } func clearDisplays() { + for display in self.displays { + CustomHUDManager.shared.cleanupDisplay(display.identifier) + } self.displays = [] } diff --git a/MonitorControl/Support/MediaKeyTapManager.swift b/MonitorControl/Support/MediaKeyTapManager.swift index f484a6e..5ebb97e 100644 --- a/MonitorControl/Support/MediaKeyTapManager.swift +++ b/MonitorControl/Support/MediaKeyTapManager.swift @@ -150,7 +150,7 @@ class MediaKeyTapManager: MediaKeyTapDelegate { if [KeyboardBrightness.media.rawValue, KeyboardBrightness.both.rawValue].contains(prefs.integer(forKey: PrefKey.keyboardBrightness.rawValue)) { keys.append(contentsOf: [.brightnessUp, .brightnessDown]) } - if [KeyboardVolume.media.rawValue, KeyboardVolume.both.rawValue].contains(prefs.integer(forKey: PrefKey.keyboardVolume.rawValue)) { + if [KeyboardVolume.media.rawValue, KeyboardVolume.both.rawValue, KeyboardVolume.mediaForce.rawValue].contains(prefs.integer(forKey: PrefKey.keyboardVolume.rawValue)) { keys.append(contentsOf: [.mute, .volumeUp, .volumeDown]) } // Remove brightness keys if no external displays are connected, but only if brightness fine control is not active @@ -166,8 +166,8 @@ class MediaKeyTapManager: MediaKeyTapDelegate { let keysToDelete: [MediaKey] = [.brightnessUp, .brightnessDown] keys.removeAll { keysToDelete.contains($0) } } - // Remove volume related keys if audio device is controllable - if let defaultAudioDevice = app.coreAudio.defaultOutputDevice { + // Remove volume related keys if audio device is controllable (skip when user chose force-capture mode) + if prefs.integer(forKey: PrefKey.keyboardVolume.rawValue) != KeyboardVolume.mediaForce.rawValue, let defaultAudioDevice = app.coreAudio.defaultOutputDevice { let keysToDelete: [MediaKey] = [.volumeUp, .volumeDown, .mute] if prefs.integer(forKey: PrefKey.multiKeyboardVolume.rawValue) == MultiKeyboardVolume.audioDeviceNameMatching.rawValue { if DisplayManager.shared.updateAudioControlTargetDisplays(deviceName: defaultAudioDevice.name) == 0 { diff --git a/MonitorControl/Support/SliderHandler.swift b/MonitorControl/Support/SliderHandler.swift index 320623f..7d2a3a0 100644 --- a/MonitorControl/Support/SliderHandler.swift +++ b/MonitorControl/Support/SliderHandler.swift @@ -360,9 +360,7 @@ class SliderHandler { slider.floatValue = value } } - if self.percentageBox == self.percentageBox { - self.percentageBox?.stringValue = "" + String(Int(value * 100)) + "%" - } + self.percentageBox?.stringValue = "" + String(Int(value * 100)) + "%" for display in self.displays { slider.setHighlightItem(display.identifier, value: value) if self.command == .brightness, let appleDisplay = display as? AppleDisplay { @@ -418,9 +416,7 @@ class SliderHandler { } else { slider.setDisplayHighlightItems(false) } - if self.percentageBox == self.percentageBox { - self.percentageBox?.stringValue = "" + String(Int(value * 100)) + "%" - } + self.percentageBox?.stringValue = "" + String(Int(value * 100)) + "%" } } } diff --git a/MonitorControl/UI/Base.lproj/Main.storyboard b/MonitorControl/UI/Base.lproj/Main.storyboard index c269c53..a3b1e56 100644 --- a/MonitorControl/UI/Base.lproj/Main.storyboard +++ b/MonitorControl/UI/Base.lproj/Main.storyboard @@ -997,13 +997,14 @@ - + + diff --git a/MonitorControl/UI/en.lproj/Main.strings b/MonitorControl/UI/en.lproj/Main.strings index 7c40246..41aa602 100644 --- a/MonitorControl/UI/en.lproj/Main.strings +++ b/MonitorControl/UI/en.lproj/Main.strings @@ -415,5 +415,8 @@ /* Class = "NSButtonCell"; title = "Show percentages"; ObjectID = "ZUu-MR-XwA"; */ "ZUu-MR-XwA.title" = "Show percentages"; +/* Class = "NSMenuItem"; title = "Standard keys (force capture)"; ObjectID = "fVk-mR-9Xp"; */ +"fVk-mR-9Xp.title" = "Standard keys (force capture)"; + /* Class = "NSTextFieldCell"; title = "Combined dimming switchover point:"; ObjectID = "zv8-pZ-OPy"; */ "zv8-pZ-OPy.title" = "Combined dimming switchover point:"; diff --git a/MonitorControl/View Controllers/Onboarding/OnboardingViewController.swift b/MonitorControl/View Controllers/Onboarding/OnboardingViewController.swift index 4d07325..130dcbb 100644 --- a/MonitorControl/View Controllers/Onboarding/OnboardingViewController.swift +++ b/MonitorControl/View Controllers/Onboarding/OnboardingViewController.swift @@ -27,7 +27,7 @@ class OnboardingViewController: NSViewController { // MARK: - Style private func setPermissionsButtonState() { - let volumePermissions: Bool = [KeyboardVolume.media.rawValue, KeyboardVolume.both.rawValue].contains(prefs.integer(forKey: PrefKey.keyboardVolume.rawValue)) + let volumePermissions: Bool = [KeyboardVolume.media.rawValue, KeyboardVolume.both.rawValue, KeyboardVolume.mediaForce.rawValue].contains(prefs.integer(forKey: PrefKey.keyboardVolume.rawValue)) let brigthnessPermissions: Bool = [KeyboardBrightness.media.rawValue, KeyboardBrightness.both.rawValue].contains(prefs.integer(forKey: PrefKey.keyboardBrightness.rawValue)) let permissionsRequired: Bool = volumePermissions || brigthnessPermissions let enabled: Bool = !MediaKeyTapManager.readPrivileges(prompt: false) && permissionsRequired diff --git a/MonitorControlHelper/Info.plist b/MonitorControlHelper/Info.plist index 00249ad..1c04b68 100644 --- a/MonitorControlHelper/Info.plist +++ b/MonitorControlHelper/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 7175 + 7176 LSApplicationCategoryType public.app-category.utilities LSBackgroundOnly From f403b309022056a9fe3a6a4097e09bdddf540e53 Mon Sep 17 00:00:00 2001 From: Abdul Rehman Date: Mon, 6 Apr 2026 15:29:55 +0500 Subject: [PATCH 07/12] fix(menu): restore display block border and match menu material MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Draw the original soft ring + inner edge on BlockBorderOverlayView so the pre–visual-effect border look returns without relying on layer draw. - Use NSVisualEffectView material .menu and blending .withinWindow so the card matches standard NSMenu rows (replacing .popover / .behindWindow). - Light appearance uses a separator stroke instead of a white fill over the blurred material. - Bump CFBundleVersion to 7182 in app and helper Info.plist. Made-with: Cursor --- MonitorControl/Info.plist | 2 +- MonitorControl/Support/MenuHandler.swift | 43 +++++++++++++++++++++--- MonitorControlHelper/Info.plist | 2 +- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/MonitorControl/Info.plist b/MonitorControl/Info.plist index e06e3af..11fd03d 100644 --- a/MonitorControl/Info.plist +++ b/MonitorControl/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 7176 + 7182 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion diff --git a/MonitorControl/Support/MenuHandler.swift b/MonitorControl/Support/MenuHandler.swift index baebdc1..27a5728 100644 --- a/MonitorControl/Support/MenuHandler.swift +++ b/MonitorControl/Support/MenuHandler.swift @@ -103,19 +103,54 @@ class MenuHandler: NSMenu, NSMenuDelegate { func addDisplayMenuBlock(addedSliderHandlers: [SliderHandler], blockName: String, monitorSubMenu: NSMenu, numOfDisplays: Int, asSubMenu: Bool) { if numOfDisplays > 1, prefs.integer(forKey: PrefKey.multiSliders.rawValue) != MultiSliders.relevant.rawValue, !DEBUG_MACOS10, #available(macOS 11.0, *) { + /// Draws the original soft outer rings + inner edge; layered on top of `NSVisualEffectView` (which does not reliably call `draw(_:)`). + class BlockBorderOverlayView: NSView { + override var isOpaque: Bool { false } + + override func viewDidChangeEffectiveAppearance() { + super.viewDidChangeEffectiveAppearance() + self.needsDisplay = true + } + + override func draw(_ dirtyRect: NSRect) { + let radius = prefs.bool(forKey: PrefKey.showTickMarks.rawValue) ? CGFloat(4) : CGFloat(11) + let outerMargin = CGFloat(15) + let blockRect = self.bounds.insetBy(dx: outerMargin, dy: outerMargin / 2 + 2).offsetBy(dx: 0, dy: outerMargin / 2 * -1 + 7) + for i in 1 ... 5 { + let blockPath = NSBezierPath(roundedRect: blockRect.insetBy(dx: CGFloat(i) * -1, dy: CGFloat(i) * -1), xRadius: radius + CGFloat(i) * 0.5, yRadius: radius + CGFloat(i) * 0.5) + NSColor.black.withAlphaComponent(0.1 / CGFloat(i)).setStroke() + blockPath.lineWidth = 1 + blockPath.stroke() + } + let blockPath = NSBezierPath(roundedRect: blockRect, xRadius: radius, yRadius: radius) + if [NSAppearance.Name.darkAqua, NSAppearance.Name.vibrantDark].contains(self.effectiveAppearance.name) { + NSColor.systemGray.withAlphaComponent(0.3).setStroke() + blockPath.lineWidth = 1 + blockPath.stroke() + } else { + // With `NSVisualEffectView` behind, skip the old semi-opaque white fill so the material shows; keep a clear inner edge. + NSColor.separatorColor.withAlphaComponent(0.45).setStroke() + blockPath.lineWidth = 1 + blockPath.stroke() + } + } + } + class BlockView: NSVisualEffectView { override init(frame frameRect: NSRect) { super.init(frame: frameRect) - self.material = .popover - self.blendingMode = .behindWindow + // Match the surrounding NSMenu chrome (display name row above, etc.); `.popover` reads as a different panel. + self.material = .menu + self.blendingMode = .withinWindow self.state = .active self.isEmphasized = false self.wantsLayer = true let tickMarks = prefs.bool(forKey: PrefKey.showTickMarks.rawValue) self.layer?.cornerRadius = tickMarks ? CGFloat(10) : CGFloat(14) self.layer?.masksToBounds = true - self.layer?.borderWidth = 0.5 - self.layer?.borderColor = NSColor.separatorColor.withAlphaComponent(0.45).cgColor + let borderOverlay = BlockBorderOverlayView(frame: self.bounds) + borderOverlay.autoresizingMask = [.width, .height] + self.addSubview(borderOverlay) } @available(*, unavailable) diff --git a/MonitorControlHelper/Info.plist b/MonitorControlHelper/Info.plist index 1c04b68..5af0012 100644 --- a/MonitorControlHelper/Info.plist +++ b/MonitorControlHelper/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 7176 + 7182 LSApplicationCategoryType public.app-category.utilities LSBackgroundOnly From a96f654948c0d5b1b75eb031327454635b519973 Mon Sep 17 00:00:00 2001 From: Abdul Rehman Date: Mon, 20 Apr 2026 17:58:34 +0500 Subject: [PATCH 08/12] UI: Refine Custom HUD and Menu Slider aesthetics - Make Custom HUD background transparent to avoid square opaque corners - Update custom HUD icon and slider colors for better macOS integration - Adjust Menu Slider icon padding and layout constraints --- MonitorControl/Support/CustomHUD.swift | 21 ++++++++++----- MonitorControl/Support/SliderHandler.swift | 30 ++++++++++++++-------- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/MonitorControl/Support/CustomHUD.swift b/MonitorControl/Support/CustomHUD.swift index 7a18777..b2ea56d 100644 --- a/MonitorControl/Support/CustomHUD.swift +++ b/MonitorControl/Support/CustomHUD.swift @@ -41,7 +41,7 @@ class CustomHUDManager { private func createHUDWindow(displayID: CGDirectDisplayID) -> NSWindow { let window = NSPanel( contentRect: NSRect(x: 0, y: 0, width: 260, height: 56), - styleMask: [.nonactivatingPanel, .hudWindow], + styleMask: [.nonactivatingPanel], backing: .buffered, defer: false ) @@ -96,6 +96,13 @@ class CustomHUDManager { private func createHUDContentView(type: HUDType, value: Float, maxValue: Float) -> NSView { let w: CGFloat = 260 let h: CGFloat = 56 + + // Wrap the visual effect view in a transparent root view. + // This ensures the NSWindow doesn't force a square opaque background on the corners. + 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 @@ -103,6 +110,8 @@ class CustomHUDManager { containerView.wantsLayer = true containerView.layer?.cornerRadius = h / 2 containerView.layer?.masksToBounds = true + + rootView.addSubview(containerView) let iconSize: CGFloat = 26 let iconView = NSImageView(frame: NSRect(x: 18, y: (h - iconSize) / 2, width: iconSize, height: iconSize)) @@ -149,7 +158,7 @@ class CustomHUDManager { let normalizedValue = CGFloat(min(max(value / maxValue, 0), 1)) let progressFill = NSView(frame: NSRect(x: barX, y: barY, width: max(barH, barW * normalizedValue), height: barH)) progressFill.wantsLayer = true - progressFill.layer?.backgroundColor = NSColor.white.cgColor + progressFill.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.9).cgColor progressFill.layer?.cornerRadius = barH / 2 containerView.addSubview(progressFill) @@ -161,7 +170,7 @@ class CustomHUDManager { label.alignment = .right containerView.addSubview(label) - return containerView + return rootView } private var fadeTimers: [CGDirectDisplayID: Timer] = [:] @@ -229,9 +238,9 @@ enum HUDType { var iconNSColor: NSColor { switch self { - case .brightness: return .systemYellow - case .volume, .volumeMuted: return .systemBlue - case .contrast: return .systemGray + case .brightness: return .white.withAlphaComponent(0.9) + case .volume, .volumeMuted: return .white.withAlphaComponent(0.9) + case .contrast: return .white.withAlphaComponent(0.8) } } } diff --git a/MonitorControl/Support/SliderHandler.swift b/MonitorControl/Support/SliderHandler.swift index 1c6679a..c14c7d3 100644 --- a/MonitorControl/Support/SliderHandler.swift +++ b/MonitorControl/Support/SliderHandler.swift @@ -29,7 +29,7 @@ class SliderHandler { let offsetY: CGFloat = -1.5 let tickMarkKnobExtraInset: CGFloat = 4 - let tickMarkKnobExtraRadiusMultiplier: CGFloat = 0.25 + let tickMarkKnobExtraRadiusMultiplier: CGFloat = 0.75 var numOfTickmarks: Int = 0 var isHighlightDisplayItems: Bool = false @@ -226,7 +226,7 @@ class SliderHandler { if #available(macOS 12.0, *) { let paletteColor: NSColor switch command { - case .brightness: paletteColor = .systemYellow + case .brightness: paletteColor = .labelColor.withAlphaComponent(0.85) default: paletteColor = .labelColor.withAlphaComponent(0.72) } let palette = NSImage.SymbolConfiguration(paletteColors: [paletteColor]) @@ -247,13 +247,23 @@ class SliderHandler { } self.slider = slider if !DEBUG_MACOS10, #available(macOS 11.0, *) { - slider.frame.size.width = 196 + let iconSize: CGFloat = 18 + let iconPadding: CGFloat = 10 + slider.frame.size.width = 180 var sliderFrame = slider.frame sliderFrame.size.height = max(sliderFrame.size.height, 22) slider.frame = sliderFrame - slider.frame.origin = NSPoint(x: 15, y: 8) - let view = NSView(frame: NSRect(x: 0, y: 0, width: slider.frame.width + 30 + (showPercent ? 38 : 0), height: slider.frame.height + 42)) + + // Horizontal layout: Icon then Slider + let iconX: CGFloat = 15 + let sliderX = iconX + iconSize + iconPadding + slider.frame.origin = NSPoint(x: sliderX, y: 8) + + let viewWidth = sliderX + slider.frame.width + 15 + (showPercent ? 38 : 0) + let viewHeight = slider.frame.height + 16 + let view = NSView(frame: NSRect(x: 0, y: 0, width: viewWidth, height: viewHeight)) view.frame.origin = NSPoint(x: 12, y: 0) + var iconName = "circle.dashed" switch command { case .audioSpeakerVolume: iconName = "speaker.wave.2.fill" @@ -262,23 +272,23 @@ class SliderHandler { default: break } let icon = SliderHandler.ClickThroughImageView() - let iconSize: CGFloat = 18 icon.image = SliderHandler.menuSymbolImage(named: iconName, pointSize: iconSize, command: command) icon.imageScaling = .scaleProportionallyDown if #available(macOS 12.0, *) { icon.contentTintColor = nil } else { - icon.contentTintColor = command == .brightness ? NSColor.systemYellow : NSColor.labelColor.withAlphaComponent(0.72) + icon.contentTintColor = .labelColor.withAlphaComponent(0.72) } - // Position icon at horizontal left (start), 8px above slider - icon.frame = NSRect(x: slider.frame.origin.x, y: slider.frame.origin.y + slider.frame.height + 8, width: iconSize, height: iconSize) + + // Position icon to the left of the slider, vertically centered + icon.frame = NSRect(x: iconX, y: slider.frame.origin.y + (slider.frame.height - iconSize) / 2, width: iconSize, height: iconSize) icon.imageAlignment = .alignCenter icon.configureForMenuSymbol() view.addSubview(slider) view.addSubview(icon) self.icon = icon if showPercent { - let percentageBox = NSTextField(frame: NSRect(x: 15 + slider.frame.size.width - 2, y: slider.frame.origin.y + (slider.frame.height - 12) / 2, width: 40, height: 12)) + let percentageBox = NSTextField(frame: NSRect(x: sliderX + slider.frame.size.width + 2, y: slider.frame.origin.y + (slider.frame.height - 12) / 2, width: 40, height: 12)) self.setupPercentageBox(percentageBox) self.percentageBox = percentageBox view.addSubview(percentageBox) From 71261471f872f9bb8324c6a525529ca8c7222341 Mon Sep 17 00:00:00 2001 From: Abdul Rehman Date: Mon, 20 Apr 2026 18:15:59 +0500 Subject: [PATCH 09/12] Refine UI aesthetics: improve slider drawing and re-enable dynamic volume icons --- MonitorControl/Support/CustomHUD.swift | 2 +- MonitorControl/Support/SliderHandler.swift | 53 +++++++++++----------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/MonitorControl/Support/CustomHUD.swift b/MonitorControl/Support/CustomHUD.swift index b2ea56d..a0561e9 100644 --- a/MonitorControl/Support/CustomHUD.swift +++ b/MonitorControl/Support/CustomHUD.swift @@ -158,7 +158,7 @@ class CustomHUDManager { let normalizedValue = CGFloat(min(max(value / maxValue, 0), 1)) let progressFill = NSView(frame: NSRect(x: barX, y: barY, width: max(barH, barW * normalizedValue), height: barH)) progressFill.wantsLayer = true - progressFill.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.9).cgColor + progressFill.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.85).cgColor progressFill.layer?.cornerRadius = barH / 2 containerView.addSubview(progressFill) diff --git a/MonitorControl/Support/SliderHandler.swift b/MonitorControl/Support/SliderHandler.swift index c14c7d3..d5f7e00 100644 --- a/MonitorControl/Support/SliderHandler.swift +++ b/MonitorControl/Support/SliderHandler.swift @@ -14,15 +14,15 @@ class SliderHandler { var icon: ClickThroughImageView? class MCSliderCell: NSSliderCell { - let knobFillColor = NSColor(white: 1, alpha: 1) - let knobFillColorTracking = NSColor(white: 0.8, alpha: 1) - let knobStrokeColor = NSColor.systemGray.withAlphaComponent(0.5) - let knobShadowColor = NSColor(white: 0, alpha: 0.03) - let barFillColor = NSColor.systemGray.withAlphaComponent(0.2) - let barStrokeColor = NSColor.systemGray.withAlphaComponent(0.5) - let barFilledFillColor = NSColor(white: 1, alpha: 1) - let highlightDisplayIndicatorColor = NSColor(white: 0.85, alpha: 1) // This is visible if there is more the 2 displays - let tickMarkColor = NSColor.systemGray.withAlphaComponent(0.5) + let knobFillColor = NSColor.white.withAlphaComponent(0.9) + let knobFillColorTracking = NSColor.white.withAlphaComponent(0.75) + let knobStrokeColor = NSColor.systemGray.withAlphaComponent(0.4) + let knobShadowColor = NSColor(white: 0, alpha: 0.05) + let barFillColor = NSColor.systemGray.withAlphaComponent(0.25) + let barStrokeColor = NSColor.systemGray.withAlphaComponent(0.4) + let barFilledFillColor = NSColor.white.withAlphaComponent(0.85) + let highlightDisplayIndicatorColor = NSColor.white.withAlphaComponent(0.85) // This is visible if there is more the 2 displays + let tickMarkColor = NSColor.systemGray.withAlphaComponent(0.4) let inset: CGFloat = 3.5 let offsetX: CGFloat = -1.5 @@ -118,7 +118,7 @@ class SliderHandler { } let knob = NSBezierPath(roundedRect: knobRect, xRadius: knobRadius, yRadius: knobRadius) - (self.isTracking ? self.knobFillColorTracking : self.knobFillColor).withAlphaComponent(knobAlpha).setFill() + (self.isTracking ? self.knobFillColorTracking : self.knobFillColor).withAlphaComponent(self.knobFillColor.alphaComponent * knobAlpha).setFill() knob.fill() if self.isHighlightDisplayItems, self.displayHighlightItems.count > 2 { @@ -135,7 +135,8 @@ class SliderHandler { self.knobStrokeColor.withAlphaComponent(self.knobStrokeColor.alphaComponent * knobAlpha).setStroke() knob.stroke() - self.barStrokeColor.setStroke() + // Use a subtle stroke for the bar as well + self.barStrokeColor.withAlphaComponent(self.barStrokeColor.alphaComponent * (1 - knobAlpha * 0.5)).setStroke() bar.stroke() } } @@ -383,21 +384,21 @@ class SliderHandler { } func updateIcon() { - // This looks hideous so I disable it for now. Maybe after a bit of tinkering it will look better - /* - if self.command == .audioSpeakerVolume { - let value = self.slider?.floatValue ?? 0.5 - if value > 2/3 { - self.icon?.image = NSImage(systemSymbolName: "speaker.wave.3.fill", accessibilityDescription: "") - } else if value > 1/3 { - self.icon?.image = NSImage(systemSymbolName: "speaker.wave.2.fill", accessibilityDescription: "") - } else if value != 0 { - self.icon?.image = NSImage(systemSymbolName: "speaker.wave.1.fill", accessibilityDescription: "") - } else { - self.icon?.image = NSImage(systemSymbolName: "speaker.slash.fill", accessibilityDescription: "") - } - } - */ + if #available(macOS 11.0, *), self.command == .audioSpeakerVolume { + let value = self.slider?.floatValue ?? 0.5 + let iconName: String + if value > 2/3 { + iconName = "speaker.wave.3.fill" + } else if value > 1/3 { + iconName = "speaker.wave.2.fill" + } else if value > 0 { + iconName = "speaker.wave.1.fill" + } else { + iconName = "speaker.slash.fill" + } + let iconSize: CGFloat = 18 + self.icon?.image = SliderHandler.menuSymbolImage(named: iconName, pointSize: iconSize, command: self.command) + } } func setValue(_ value: Float, displayID: CGDirectDisplayID = 0) { From 6539556a2049293e655c4340e6d51bb905be091e Mon Sep 17 00:00:00 2001 From: Abdul Rehman Date: Mon, 20 Apr 2026 23:56:25 +0500 Subject: [PATCH 10/12] refactor: update HUD styling, increment build versions, and add library validation entitlement --- MonitorControl/Support/CustomHUD.swift | 29 ++++++++++++++++---------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/MonitorControl/Support/CustomHUD.swift b/MonitorControl/Support/CustomHUD.swift index a0561e9..8720ab8 100644 --- a/MonitorControl/Support/CustomHUD.swift +++ b/MonitorControl/Support/CustomHUD.swift @@ -104,13 +104,18 @@ class CustomHUDManager { rootView.layer?.backgroundColor = NSColor.clear.cgColor let containerView = NSVisualEffectView(frame: NSRect(x: 0, y: 0, width: w, height: h)) - containerView.material = .hudWindow + containerView.material = .popover containerView.blendingMode = .behindWindow containerView.state = .active + containerView.appearance = NSAppearance(named: .vibrantDark) containerView.wantsLayer = true containerView.layer?.cornerRadius = h / 2 containerView.layer?.masksToBounds = true + // Add liquid glass edge highlight + containerView.layer?.borderWidth = 1.0 + containerView.layer?.borderColor = NSColor.white.withAlphaComponent(0.15).cgColor + rootView.addSubview(containerView) let iconSize: CGFloat = 26 @@ -145,26 +150,28 @@ class CustomHUDManager { containerView.addSubview(iconView) let barX: CGFloat = 18 + iconSize + 12 - let barW: CGFloat = w - barX - 56 + let barW: CGFloat = w - barX - 74 let barH: CGFloat = 10 let barY = (h - barH) / 2 - let progressBg = NSView(frame: NSRect(x: barX, y: barY, width: barW, height: barH)) - progressBg.wantsLayer = true - progressBg.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.22).cgColor - progressBg.layer?.cornerRadius = 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.22) + progressBg.cornerRadius = barH / 2 containerView.addSubview(progressBg) let normalizedValue = CGFloat(min(max(value / maxValue, 0), 1)) - let progressFill = NSView(frame: NSRect(x: barX, y: barY, width: max(barH, barW * normalizedValue), height: barH)) - progressFill.wantsLayer = true - progressFill.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.85).cgColor - progressFill.layer?.cornerRadius = barH / 2 + let progressFill = NSBox(frame: NSRect(x: barX, y: barY, width: max(barH, barW * normalizedValue), height: barH)) + progressFill.boxType = .custom + progressFill.borderType = .noBorder + progressFill.fillColor = NSColor.white.withAlphaComponent(0.85) + progressFill.cornerRadius = barH / 2 containerView.addSubview(progressFill) let percentage = Int(normalizedValue * 100) let label = NSTextField(labelWithString: "\(percentage)%") - label.frame = NSRect(x: barX + barW + 8, y: (h - 22) / 2, width: 44, height: 22) + label.frame = NSRect(x: barX + barW + 12, y: (h - 22) / 2, width: 44, height: 22) label.font = NSFont.monospacedDigitSystemFont(ofSize: 15, weight: .semibold) label.textColor = .white label.alignment = .right From c88f806bf1ac1cbeee627a543c0afc642008fe76 Mon Sep 17 00:00:00 2001 From: Abdul Rehman Date: Tue, 21 Apr 2026 01:15:54 +0500 Subject: [PATCH 11/12] Refine main branch UI: dark OSD, premium icons, and fixed corners --- MonitorControl/Support/CustomHUD.swift | 25 ++++++++++++++++------ MonitorControl/Support/SliderHandler.swift | 2 +- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/MonitorControl/Support/CustomHUD.swift b/MonitorControl/Support/CustomHUD.swift index 8136c33..21f1a27 100644 --- a/MonitorControl/Support/CustomHUD.swift +++ b/MonitorControl/Support/CustomHUD.swift @@ -98,14 +98,26 @@ class CustomHUDManager { } private func createHUDContentView(type: HUDType, value: Float, maxValue: Float) -> NSView { + // Wrap in a transparent root view to fix "offending corners" and ensure perfect rounding + let rootView = NSView(frame: NSRect(x: 0, y: 0, width: 220, height: 60)) + rootView.wantsLayer = true + rootView.layer?.backgroundColor = NSColor.clear.cgColor + let containerView = NSVisualEffectView(frame: NSRect(x: 0, y: 0, width: 220, height: 60)) - containerView.material = .hudWindow + containerView.material = .popover containerView.blendingMode = .behindWindow containerView.state = .active + containerView.appearance = NSAppearance(named: .vibrantDark) containerView.wantsLayer = true - containerView.layer?.cornerRadius = 12 + containerView.layer?.cornerRadius = 30 // Pill shape (height / 2) containerView.layer?.masksToBounds = true + // Add subtle edge highlight for a premium look + containerView.layer?.borderWidth = 1.0 + containerView.layer?.borderColor = NSColor.white.withAlphaComponent(0.12).cgColor + + rootView.addSubview(containerView) + // Icon let iconView = NSImageView(frame: NSRect(x: 16, y: 18, width: 24, height: 24)) if #available(macOS 11.0, *) { @@ -138,7 +150,7 @@ class CustomHUDManager { let normalizedValue = CGFloat(min(max(value / maxValue, 0), 1)) let progressFill = NSView(frame: NSRect(x: 52, y: 26, width: 108 * normalizedValue, height: 8)) progressFill.wantsLayer = true - progressFill.layer?.backgroundColor = NSColor.white.cgColor + progressFill.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.85).cgColor progressFill.layer?.cornerRadius = 4 containerView.addSubview(progressFill) @@ -151,7 +163,7 @@ class CustomHUDManager { label.alignment = .right containerView.addSubview(label) - return containerView + return rootView } private var fadeTimers: [CGDirectDisplayID: Timer] = [:] @@ -217,9 +229,8 @@ enum HUDType { var iconNSColor: NSColor { switch self { - case .brightness: return .systemYellow - case .volume, .volumeMuted: return .systemBlue - case .contrast: return .systemGray + case .brightness, .volume, .volumeMuted: return .white.withAlphaComponent(0.9) + case .contrast: return .white.withAlphaComponent(0.8) } } } diff --git a/MonitorControl/Support/SliderHandler.swift b/MonitorControl/Support/SliderHandler.swift index ff98e78..7eb24ac 100644 --- a/MonitorControl/Support/SliderHandler.swift +++ b/MonitorControl/Support/SliderHandler.swift @@ -231,7 +231,7 @@ class SliderHandler { } let icon = SliderHandler.ClickThroughImageView() icon.image = NSImage(systemSymbolName: iconName, accessibilityDescription: title) - icon.contentTintColor = NSColor.black.withAlphaComponent(0.6) + icon.contentTintColor = NSColor.labelColor.withAlphaComponent(0.72) // Position icon at horizontal left (start), 8px above slider let iconSize: CGFloat = 18 icon.frame = NSRect(x: slider.frame.origin.x, y: slider.frame.origin.y + slider.frame.height + 8, width: iconSize, height: iconSize) From 7d55f05419af0707c4c59e6e3475334431972db2 Mon Sep 17 00:00:00 2001 From: Abdul Rehman Date: Sat, 9 May 2026 22:47:23 +0500 Subject: [PATCH 12/12] UI: Refine Custom HUD to high-fidelity macOS Tahoe 'Liquid Glass' style --- MonitorControl/Info.plist | 2 +- MonitorControl/Support/CustomHUD.swift | 62 +++++++++++--------------- MonitorControlHelper/Info.plist | 2 +- 3 files changed, 28 insertions(+), 38 deletions(-) diff --git a/MonitorControl/Info.plist b/MonitorControl/Info.plist index 77785e0..9d91b77 100644 --- a/MonitorControl/Info.plist +++ b/MonitorControl/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 7182 + 7184 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion diff --git a/MonitorControl/Support/CustomHUD.swift b/MonitorControl/Support/CustomHUD.swift index 8720ab8..47c47dd 100644 --- a/MonitorControl/Support/CustomHUD.swift +++ b/MonitorControl/Support/CustomHUD.swift @@ -40,8 +40,8 @@ class CustomHUDManager { private func createHUDWindow(displayID: CGDirectDisplayID) -> NSWindow { let window = NSPanel( - contentRect: NSRect(x: 0, y: 0, width: 260, height: 56), - styleMask: [.nonactivatingPanel], + contentRect: NSRect(x: 0, y: 0, width: 280, height: 64), + styleMask: [.nonactivatingPanel, .fullSizeContentView], backing: .buffered, defer: false ) @@ -56,6 +56,8 @@ class CustomHUDManager { window.isMovable = false window.isMovableByWindowBackground = false window.ignoresMouseEvents = true + window.titleVisibility = .hidden + window.titlebarAppearsTransparent = true positionWindow(window, onDisplay: displayID) @@ -70,7 +72,7 @@ class CustomHUDManager { // Position at center-bottom of screen, above the dock let x = screenFrame.origin.x + (screenFrame.width - windowSize.width) / 2 - let y = screenFrame.origin.y + 100 // 100 points from bottom + let y = screenFrame.origin.y + 80 // 80 points from bottom for better visibility window.setFrameOrigin(NSPoint(x: x, y: y)) } @@ -94,17 +96,16 @@ class CustomHUDManager { } private func createHUDContentView(type: HUDType, value: Float, maxValue: Float) -> NSView { - let w: CGFloat = 260 - let h: CGFloat = 56 + let w: CGFloat = 280 + let h: CGFloat = 64 // Wrap the visual effect view in a transparent root view. - // This ensures the NSWindow doesn't force a square opaque background on the corners. 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 = .popover + containerView.material = .hudWindow containerView.blendingMode = .behindWindow containerView.state = .active containerView.appearance = NSAppearance(named: .vibrantDark) @@ -112,29 +113,24 @@ class CustomHUDManager { containerView.layer?.cornerRadius = h / 2 containerView.layer?.masksToBounds = true - // Add liquid glass edge highlight - containerView.layer?.borderWidth = 1.0 - containerView.layer?.borderColor = NSColor.white.withAlphaComponent(0.15).cgColor + // 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 = 26 - let iconView = NSImageView(frame: NSRect(x: 18, y: (h - iconSize) / 2, width: iconSize, height: iconSize)) + 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) { - if #available(macOS 12.0, *) { - let base = NSImage.SymbolConfiguration(pointSize: iconSize * 0.85, weight: .semibold) - let palette = NSImage.SymbolConfiguration(paletteColors: [type.iconNSColor]) - iconView.image = icon.withSymbolConfiguration(base.applying(palette)) - iconView.contentTintColor = nil - } else { - iconView.image = icon - iconView.contentTintColor = type.iconNSColor - } + let config = NSImage.SymbolConfiguration(pointSize: iconSize * 0.9, weight: .bold) + iconView.image = icon.withSymbolConfiguration(config) + iconView.contentTintColor = .white } } else { let fallbackIcon: String @@ -145,38 +141,32 @@ class CustomHUDManager { case .contrast: fallbackIcon = NSImage.touchBarColorPickerFillName } iconView.image = NSImage(named: fallbackIcon) - iconView.contentTintColor = type.iconNSColor + iconView.contentTintColor = .white } containerView.addSubview(iconView) - let barX: CGFloat = 18 + iconSize + 12 - let barW: CGFloat = w - barX - 74 - let barH: CGFloat = 10 + // 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.22) + progressBg.fillColor = NSColor.white.withAlphaComponent(0.15) progressBg.cornerRadius = barH / 2 containerView.addSubview(progressBg) let normalizedValue = CGFloat(min(max(value / maxValue, 0), 1)) - let progressFill = NSBox(frame: NSRect(x: barX, y: barY, width: max(barH, barW * normalizedValue), height: barH)) + 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.85) + progressFill.fillColor = NSColor.white.withAlphaComponent(0.9) progressFill.cornerRadius = barH / 2 containerView.addSubview(progressFill) - let percentage = Int(normalizedValue * 100) - let label = NSTextField(labelWithString: "\(percentage)%") - label.frame = NSRect(x: barX + barW + 12, y: (h - 22) / 2, width: 44, height: 22) - label.font = NSFont.monospacedDigitSystemFont(ofSize: 15, weight: .semibold) - label.textColor = .white - label.alignment = .right - containerView.addSubview(label) - return rootView } diff --git a/MonitorControlHelper/Info.plist b/MonitorControlHelper/Info.plist index 5af0012..a17ee8c 100644 --- a/MonitorControlHelper/Info.plist +++ b/MonitorControlHelper/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 7182 + 7184 LSApplicationCategoryType public.app-category.utilities LSBackgroundOnly