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.
This commit is contained in:
Abdul Rehman 2025-12-27 10:53:02 +05:00
parent 195a8e88ec
commit cbe2fa7e85
9 changed files with 429 additions and 12 deletions

View file

@ -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 = "<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 */
@ -317,6 +319,7 @@
AA16139A26BE772E00DCF027 /* Arm64DDC.swift */,
AA4398A826DD55DA00943F16 /* IntelDDC.swift */,
FE4E0895249D584C003A50BB /* OSDUtils.swift */,
CE12345678901234003A50BC /* CustomHUD.swift */,
);
path = Support;
sourceTree = "<group>";
@ -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 */,

View file

@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>7141</string>
<string>7168</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSMinimumSystemVersion</key>

View file

@ -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
}
}
}

View file

@ -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)
}
}

View file

@ -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)

View file

@ -709,7 +709,7 @@
<objects>
<viewController storyboardIdentifier="KeyboardPrefsVC" id="eJ2-Cv-Vfp" customClass="KeyboardPrefsViewController" customModule="MonitorControl" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" translatesAutoresizingMaskIntoConstraints="NO" id="yCl-jO-kpd">
<rect key="frame" x="0.0" y="0.0" width="730" height="676"/>
<rect key="frame" x="0.0" y="0.0" width="730" height="726"/>
<subviews>
<gridView xPlacement="leading" yPlacement="fill" rowAlignment="none" rowSpacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="bKX-vi-7gY">
<rect key="frame" x="20" y="20" width="690" height="636"/>
@ -1164,7 +1164,7 @@
</gridView>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="bKX-vi-7gY" secondAttribute="bottom" constant="20" id="BWt-5s-vf7"/>
<constraint firstAttribute="bottom" secondItem="bKX-vi-7gY" secondAttribute="bottom" constant="55" id="BWt-5s-vf7"/>
<constraint firstItem="bKX-vi-7gY" firstAttribute="leading" secondItem="yCl-jO-kpd" secondAttribute="leading" constant="20" id="Pdh-1U-kqr"/>
<constraint firstItem="bKX-vi-7gY" firstAttribute="top" secondItem="yCl-jO-kpd" secondAttribute="top" constant="20" id="fmV-It-Xaq"/>
<constraint firstAttribute="trailing" secondItem="bKX-vi-7gY" secondAttribute="trailing" constant="20" id="ssM-r3-qK0"/>
@ -1280,7 +1280,7 @@
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" id="jz0-4Z-xVm">
<rect key="frame" x="210" y="449" width="166" height="16"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="#bc-ignore!" id="6GJ-6Q-gqz">
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="#bc-ignore!" id="6GJ-6Q-gqz">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
@ -1340,7 +1340,7 @@
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" id="mIA-Wh-7aB">
<rect key="frame" x="78" y="492" width="463" height="21"/>
<rect key="frame" x="78" y="490" width="463" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" title="#bc-ignore!" id="ibQ-4u-ClE">
<font key="font" textStyle="title2" name=".SFNS-Regular"/>
@ -1987,7 +1987,7 @@
</textFieldCell>
</textField>
<textField identifier="softwareNameLabel" focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" id="dtg-g7-hZb">
<rect key="frame" x="247" y="258" width="147" height="26"/>
<rect key="frame" x="247" y="254" width="200" height="30"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="left" title="MonitorControl" id="1PJ-14-Bvn">
<font key="font" textStyle="title1" name=".SFNS-Regular"/>

View file

@ -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í?";

View file

@ -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()
}
}

View file

@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>7141</string>
<string>7168</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSBackgroundOnly</key>