mirror of
https://github.com/MonitorControl/MonitorControl.git
synced 2026-05-16 06:05:52 -06:00
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:
parent
195a8e88ec
commit
cbe2fa7e85
9 changed files with 429 additions and 12 deletions
|
|
@ -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 */,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
225
MonitorControl/Support/CustomHUD.swift
Normal file
225
MonitorControl/Support/CustomHUD.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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í?";
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue