From 86f8ab68aa24ac4a72a08fe50721029f212c6683 Mon Sep 17 00:00:00 2001
From: waydabber <37590873+waydabber@users.noreply.github.com>
Date: Thu, 24 Oct 2024 15:57:38 +0200
Subject: [PATCH 1/8] Fixed slider percentage crash
---
MonitorControl.xcodeproj/project.pbxproj | 4 ++--
MonitorControl/Info.plist | 2 +-
MonitorControl/Support/SliderHandler.swift | 2 +-
MonitorControlHelper/Info.plist | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/MonitorControl.xcodeproj/project.pbxproj b/MonitorControl.xcodeproj/project.pbxproj
index 0583586f2..3ae92ca08 100644
--- a/MonitorControl.xcodeproj/project.pbxproj
+++ b/MonitorControl.xcodeproj/project.pbxproj
@@ -881,7 +881,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
- MARKETING_VERSION = 4.3.2;
+ MARKETING_VERSION = 4.3.4;
PRODUCT_BUNDLE_IDENTIFIER = app.monitorcontrol.MonitorControl;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -918,7 +918,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
- MARKETING_VERSION = 4.3.2;
+ MARKETING_VERSION = 4.3.4;
PRODUCT_BUNDLE_IDENTIFIER = app.monitorcontrol.MonitorControl;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
diff --git a/MonitorControl/Info.plist b/MonitorControl/Info.plist
index 6648c5087..5d3614d2d 100644
--- a/MonitorControl/Info.plist
+++ b/MonitorControl/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
- 7122
+ 7123
LSApplicationCategoryType
public.app-category.utilities
LSMinimumSystemVersion
diff --git a/MonitorControl/Support/SliderHandler.swift b/MonitorControl/Support/SliderHandler.swift
index 1d7d20ec8..d539bb9a8 100644
--- a/MonitorControl/Support/SliderHandler.swift
+++ b/MonitorControl/Support/SliderHandler.swift
@@ -379,7 +379,7 @@ class SliderHandler {
slider.setDisplayHighlightItems(false)
}
if self.percentageBox == self.percentageBox {
- self.percentageBox?.stringValue = "" + String(Int(value * 100)) + "%"
+ self.percentageBox?.stringValue = "\(String(format: "%.0f%%", Double(value) * 100))"
}
}
}
diff --git a/MonitorControlHelper/Info.plist b/MonitorControlHelper/Info.plist
index 3d3b53958..f0c137337 100644
--- a/MonitorControlHelper/Info.plist
+++ b/MonitorControlHelper/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
- 7122
+ 7123
LSApplicationCategoryType
public.app-category.utilities
LSBackgroundOnly
From d3bfea0572462632b53ffdd3109a05dd330fc30d Mon Sep 17 00:00:00 2001
From: waydabber <37590873+waydabber@users.noreply.github.com>
Date: Sun, 19 Apr 2026 08:34:11 +0200
Subject: [PATCH 2/8] Added `UIDesignRequiresCompatibility` so the project is
properly compiled with XCode 26.
---
MonitorControl/Enums/Command.swift | 2 +-
MonitorControl/Info.plist | 4 ++-
MonitorControl/Model/AppleDisplay.swift | 4 +--
MonitorControl/Model/OtherDisplay.swift | 2 +-
MonitorControl/Support/Arm64DDC.swift | 4 +--
MonitorControl/Support/DisplayManager.swift | 4 ++-
MonitorControl/Support/SliderHandler.swift | 2 +-
.../DisplaysPrefsViewController.swift | 6 ++--
MonitorControlHelper/Info.plist | 2 +-
README.md | 35 ++++++++-----------
10 files changed, 31 insertions(+), 34 deletions(-)
diff --git a/MonitorControl/Enums/Command.swift b/MonitorControl/Enums/Command.swift
index d6d1b7c8e..d374094ee 100644
--- a/MonitorControl/Enums/Command.swift
+++ b/MonitorControl/Enums/Command.swift
@@ -168,5 +168,5 @@ enum Command: UInt8 {
case bottomLeftScreenPurity = 0xEA
case bottomRightScreenPurity = 0xEB
- public static let brightness = luminance
+ static let brightness = luminance
}
diff --git a/MonitorControl/Info.plist b/MonitorControl/Info.plist
index 5d3614d2d..92532c34c 100644
--- a/MonitorControl/Info.plist
+++ b/MonitorControl/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
- 7123
+ 7125
LSApplicationCategoryType
public.app-category.utilities
LSMinimumSystemVersion
@@ -36,5 +36,7 @@
https://monitorcontrol.app/appcast2.xml
SUPublicEDKey
ITSTMp8AypsLawojJ+UR3tm2mN18AFoNMvXf1G3t62s=
+ UIDesignRequiresCompatibility
+
diff --git a/MonitorControl/Model/AppleDisplay.swift b/MonitorControl/Model/AppleDisplay.swift
index d49eae415..8c7dff6f4 100644
--- a/MonitorControl/Model/AppleDisplay.swift
+++ b/MonitorControl/Model/AppleDisplay.swift
@@ -11,7 +11,7 @@ class AppleDisplay: Display {
super.init(identifier, name: name, vendorNumber: vendorNumber, modelNumber: modelNumber, serialNumber: serialNumber, isVirtual: isVirtual, isDummy: isDummy)
}
- public func getAppleBrightness() -> Float {
+ func getAppleBrightness() -> Float {
guard !self.isDummy else {
return 1
}
@@ -20,7 +20,7 @@ class AppleDisplay: Display {
return brightness
}
- public func setAppleBrightness(value: Float) {
+ func setAppleBrightness(value: Float) {
guard !self.isDummy else {
return
}
diff --git a/MonitorControl/Model/OtherDisplay.swift b/MonitorControl/Model/OtherDisplay.swift
index d920990ea..7c579c4ea 100644
--- a/MonitorControl/Model/OtherDisplay.swift
+++ b/MonitorControl/Model/OtherDisplay.swift
@@ -377,7 +377,7 @@ class OtherDisplay: Display {
return intCodes
}
- public func writeDDCValues(command: Command, value: UInt16) {
+ func writeDDCValues(command: Command, value: UInt16) {
guard app.sleepID == 0, app.reconfigureID == 0, !self.readPrefAsBool(key: .forceSw), !self.readPrefAsBool(key: .unavailableDDC, for: command) else {
return
}
diff --git a/MonitorControl/Support/Arm64DDC.swift b/MonitorControl/Support/Arm64DDC.swift
index 2ec04252f..e8d149508 100644
--- a/MonitorControl/Support/Arm64DDC.swift
+++ b/MonitorControl/Support/Arm64DDC.swift
@@ -8,9 +8,9 @@ let ARM64_DDC_DATA_ADDRESS: UInt8 = 0x51
class Arm64DDC: NSObject {
#if arch(arm64)
- public static let isArm64: Bool = true
+ static let isArm64: Bool = true
#else
- public static let isArm64: Bool = false
+ static let isArm64: Bool = false
#endif
static let MAX_MATCH_SCORE: Int = 20
diff --git a/MonitorControl/Support/DisplayManager.swift b/MonitorControl/Support/DisplayManager.swift
index 7ace44850..a861bd5d4 100644
--- a/MonitorControl/Support/DisplayManager.swift
+++ b/MonitorControl/Support/DisplayManager.swift
@@ -5,7 +5,7 @@ import CoreGraphics
import os.log
class DisplayManager {
- public static let shared = DisplayManager()
+ static let shared = DisplayManager()
var displays: [Display] = []
var audioControlTargetDisplays: [OtherDisplay] = []
@@ -177,6 +177,7 @@ class DisplayManager {
let isDummy: Bool = DisplayManager.isDummy(displayID: onlineDisplayID)
let isVirtual: Bool = DisplayManager.isVirtual(displayID: onlineDisplayID)
if !DEBUG_SW, DisplayManager.isAppleDisplay(displayID: onlineDisplayID) { // MARK: (point of interest for testing)
+
let appleDisplay = AppleDisplay(id, name: name, vendorNumber: vendorNumber, modelNumber: modelNumber, serialNumber: serialNumber, isVirtual: isVirtual, isDummy: isDummy)
os_log("Apple display found - %{public}@", type: .info, "ID: \(appleDisplay.identifier), Name: \(appleDisplay.name) (Vendor: \(appleDisplay.vendorNumber ?? 0), Model: \(appleDisplay.modelNumber ?? 0))")
self.addDisplay(display: appleDisplay)
@@ -519,6 +520,7 @@ class DisplayManager {
}
}
if let screen = getByDisplayID(displayID: displayID) { // MARK: This, and NSScreen+Extension.swift will not be needed when we drop MacOS 10 support.
+
if #available(macOS 10.15, *) {
return screen.localizedName
} else {
diff --git a/MonitorControl/Support/SliderHandler.swift b/MonitorControl/Support/SliderHandler.swift
index d539bb9a8..de28e9085 100644
--- a/MonitorControl/Support/SliderHandler.swift
+++ b/MonitorControl/Support/SliderHandler.swift
@@ -209,7 +209,7 @@ class SliderHandler {
}
}
- public init(display: Display?, command: Command, title: String = "", position _: Int = 0) {
+ init(display: Display?, command: Command, title: String = "", position _: Int = 0) {
self.command = command
self.title = title
let slider = SliderHandler.MCSlider(value: 0, minValue: 0, maxValue: 1, target: self, action: #selector(SliderHandler.valueChanged))
diff --git a/MonitorControl/View Controllers/Preferences/DisplaysPrefsViewController.swift b/MonitorControl/View Controllers/Preferences/DisplaysPrefsViewController.swift
index 7896b3a81..0a8384da3 100644
--- a/MonitorControl/View Controllers/Preferences/DisplaysPrefsViewController.swift
+++ b/MonitorControl/View Controllers/Preferences/DisplaysPrefsViewController.swift
@@ -67,7 +67,7 @@ class DisplaysPrefsViewController: NSViewController, SettingsPane, NSTableViewDa
self.displays.count
}
- public static func isImac() -> Bool {
+ static func isImac() -> Bool {
let platformExpertDevice = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice"))
if let modelIdentifier = IORegistryEntryCreateCFProperty(platformExpertDevice, "model" as CFString, kCFAllocatorDefault, 0).takeRetainedValue() as? String {
return modelIdentifier.contains("iMac")
@@ -75,14 +75,14 @@ class DisplaysPrefsViewController: NSViewController, SettingsPane, NSTableViewDa
return false
}
- public struct DisplayInfo {
+ struct DisplayInfo {
var displayType = ""
var displayImage = ""
var controlMethod = ""
var controlStatus = ""
}
- public static func getDisplayInfo(display: Display) -> DisplayInfo {
+ static func getDisplayInfo(display: Display) -> DisplayInfo {
var displayType = NSLocalizedString("Other Display", comment: "Shown in the Display Settings")
var displayImage = "display.trianglebadge.exclamationmark"
var controlMethod = NSLocalizedString("No Control", comment: "Shown in the Display Settings") + " ⚠️"
diff --git a/MonitorControlHelper/Info.plist b/MonitorControlHelper/Info.plist
index f0c137337..6f318b931 100644
--- a/MonitorControlHelper/Info.plist
+++ b/MonitorControlHelper/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
- 7123
+ 7125
LSApplicationCategoryType
public.app-category.utilities
LSBackgroundOnly
diff --git a/README.md b/README.md
index e13635220..b24b97ce6 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
-
MonitorControl - now compatible with macOS Sequoia
+
MonitorControl
Controls your external display brightness and volume and shows native OSD.
Use menubar extra sliders or the keyboard, including native Apple keys!

@@ -24,11 +24,14 @@ Use menubar extra sliders or the keyboard, including native Apple keys!
+> [!WARNING]
+> MonitorControl v4.2.0 [may crash](https://github.com/MonitorControl/MonitorControl/issues/1663) on certain configurations running macOS 15 Sequoia or Tahoe. Additionally, this version will not automatically update to the [latest app version](https://github.com/MonitorControl/MonitorControl/releases). To resolve the issue and ensure future updates, please upgrade manually.
+
## Download
Go to [Releases](https://github.com/MonitorControl/MonitorControl/releases) and download the latest `.dmg`, or you can install via Homebrew:
```shell
-brew install MonitorControl
+brew install --cask monitorcontrol
```
## Major features
@@ -44,7 +47,7 @@ brew install MonitorControl
- Support for custom keyboard shortcuts as well as standard brightness and media keys on Apple keyboards.
- Dozens of customization options to tweak the inner workings of the app to suit your hardware and needs (don't forget to enable `Show advanced settings` in app Settings).
- Simple, unobtrusive UI to blend in to the general aesthetics of macOS.
-- **One of the best app of its kind, completely FREE.**
+- Completely FREE.
For additional features, more advanced brightness control with XDR/HDR brightness upscaling and support for more Mac models and displays, check out [BetterDisplay](https://github.com/waydabber/BetterDisplay#readme)!
@@ -78,7 +81,9 @@ For additional features, more advanced brightness control with XDR/HDR brightnes
_* With some limitations - full functionality available on macOS 11 Big Sur or newer._
-For macOS Sequoia compatibility [v4.3.2 or newer](https://github.com/MonitorControl/MonitorControl/releases) is required!
+For macOS Sequoia and Tahoe 26 compatibility [v4.3.3 or newer](https://github.com/MonitorControl/MonitorControl/releases) is required!
+
+Please note that current versions have limited native macOS OSD support on macOS Tahoe - although the Control Center brightness or volume OSD appears, the OSD percentage value will not show or update.
### Supported displays
@@ -93,17 +98,10 @@ Notable exceptions for hardware control compatibility:
- Some displays (notably EIZO) use MCCS over USB or an entirely custom protocol for control. These displays are supported with software dimming only.
- DisplayLink docks and dongles do not allow for DDC control on Macs, only software dimming is available for these connections.
-Compatibility with
-
-- f.lux users: please activate `Avoid gamma table manipulation` under `Settings` » `Displays`! This step is not needed if you use Night Shift.
-- [BetterDisplay](https://betterdisplay.pro/) users: either activate `Avoid gamma table manipulation` in MonitorControl or turn off `Allow color table adjustments` in BetterDisplay (under Settings/Displays/Overview). You might want to disable native keyboard control either in MonitorControl or BetterDisplay, depending on which app you want to use for brightness control and dimming.
-
## Contributing to the project
-- You can help out [by contributiong to the project with your one-time donation or by being a regular Sponsor](https://opencollective.com/monitorcontrol)!
- If you want, you can fork the code, make improvements and submit a pull request to improve the app. Accepting a PR is solely in the hands of the maintainer - before making fundamental changes expecting it to be accepted, please consult the maintainer of the project!
-
## How to build
### Required
@@ -132,17 +130,12 @@ git clone https://github.com/MonitorControl/MonitorControl.git
- [KeyboardShortcuts](https://github.com/sindresorhus/KeyboardShortcuts)
- [Sparkle](https://github.com/sparkle-project/Sparkle)
-## Hall of Honor
-
-### Current maintainer of the project
-
-- [@waydabber](https://github.com/waydabber), developer of [BetterDisplay](https://github.com/waydabber/BetterDisplay#readme).
-
-### Former maintainers, special contributors
+## Credits
-- [@the0neyouseek](https://github.com/the0neyouseek) - previous (now honorary) maintainer
-- [@JoniVR](https://github.com/JoniVR) - previous (now honorary) maintainer
-- [@alin23](https://github.com/alin23) (generally spearheaded M1 DDC support and figured out a many of the caveats)
+- [@waydabber](https://github.com/waydabber), maintainer, developer of [BetterDisplay](https://github.com/waydabber/BetterDisplay#readme).
+- [@the0neyouseek](https://github.com/the0neyouseek) - honorary maintainer
+- [@JoniVR](https://github.com/JoniVR) - honorary maintainer
+- [@alin23](https://github.com/alin23) - spearheaded M1 DDC support, developer of [Lunar](https://lunar.fyi)
- [@mathew-kurian](https://github.com/mathew-kurian/) (original developer)
- [@Tyilo](https://github.com/Tyilo/) (fork)
- [@Bensge](https://github.com/Bensge/) - (used some code from his project [NativeDisplayBrightness](https://github.com/Bensge/NativeDisplayBrightness))
From 678573efac91c430b0596ba06ea8b0e6d3a0f9d3 Mon Sep 17 00:00:00 2001
From: waydabber <37590873+waydabber@users.noreply.github.com>
Date: Tue, 15 Sep 2026 09:20:38 +0200
Subject: [PATCH 3/8] Updated deployment target for Xcode 27 (don't forget to
downgrade it to 10.14 later). Added applicationShouldHandleReopen fix for the
macOS 27 Siri app issue.
---
MonitorControl.xcodeproj/project.pbxproj | 8 +-
MonitorControl/Enums/Command.swift | 2 +-
MonitorControl/Enums/PrefKey.swift | 98 +++++++++----------
.../Extensions/Preferences+Extension.swift | 1 -
MonitorControl/Info.plist | 2 +-
MonitorControl/Support/AppDelegate.swift | 35 +++++--
MonitorControl/Support/Arm64DDC.swift | 2 +-
MonitorControl/Support/DisplayManager.swift | 40 ++++----
MonitorControl/Support/SliderHandler.swift | 2 +-
.../MenuslidersPrefsViewController.swift | 4 +-
MonitorControl/main.swift | 2 +-
MonitorControlHelper/Info.plist | 2 +-
12 files changed, 108 insertions(+), 90 deletions(-)
diff --git a/MonitorControl.xcodeproj/project.pbxproj b/MonitorControl.xcodeproj/project.pbxproj
index 3ae92ca08..8c3222b0a 100644
--- a/MonitorControl.xcodeproj/project.pbxproj
+++ b/MonitorControl.xcodeproj/project.pbxproj
@@ -880,7 +880,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
- MACOSX_DEPLOYMENT_TARGET = 10.14;
+ MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 4.3.4;
PRODUCT_BUNDLE_IDENTIFIER = app.monitorcontrol.MonitorControl;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -917,7 +917,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
- MACOSX_DEPLOYMENT_TARGET = 10.14;
+ MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 4.3.4;
PRODUCT_BUNDLE_IDENTIFIER = app.monitorcontrol.MonitorControl;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -954,7 +954,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
- MACOSX_DEPLOYMENT_TARGET = 10.14;
+ MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 4.1.0;
PRODUCT_BUNDLE_IDENTIFIER = app.monitorcontrol.MonitorControlHelper;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -985,7 +985,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
- MACOSX_DEPLOYMENT_TARGET = 10.14;
+ MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 4.1.0;
PRODUCT_BUNDLE_IDENTIFIER = app.monitorcontrol.MonitorControlHelper;
PRODUCT_NAME = "$(TARGET_NAME)";
diff --git a/MonitorControl/Enums/Command.swift b/MonitorControl/Enums/Command.swift
index d374094ee..2f5fd24a6 100644
--- a/MonitorControl/Enums/Command.swift
+++ b/MonitorControl/Enums/Command.swift
@@ -87,7 +87,7 @@ enum Command: UInt8 {
case audioBalanceLR = 0x93
case audioProcessorMode = 0x94
- // OSD/Button Event Control
+ /// OSD/Button Event Control
case osd = 0xCA
// Image Adjustment
diff --git a/MonitorControl/Enums/PrefKey.swift b/MonitorControl/Enums/PrefKey.swift
index 76b5f2985..12b134d40 100644
--- a/MonitorControl/Enums/PrefKey.swift
+++ b/MonitorControl/Enums/PrefKey.swift
@@ -3,155 +3,155 @@
enum PrefKey: String {
/* -- App-wide settings -- */
- // Sparkle automatic checks
+ /// Sparkle automatic checks
case SUEnableAutomaticChecks
- // Receive beta updates?
+ /// Receive beta updates?
case isBetaChannel // This is not added to Settings yet as it will be needed in the future only.
- // Build number
+ /// Build number
case buildNumber
- // Was the app launched once
+ /// Was the app launched once
case appAlreadyLaunched
- // Hide menu icon
+ /// Hide menu icon
case menuIcon
- // Menu item style
+ /// Menu item style
case menuItemStyle
- // Keys listened for
+ /// Keys listened for
case keyboardBrightness
- // Keys listened for
+ /// Keys listened for
case keyboardVolume
- // Don't listen to F14/F15
+ /// Don't listen to F14/F15
case disableAltBrightnessKeys
- // Hide brightness sliders
+ /// Hide brightness sliders
case hideBrightness
- // Show volume sliders
+ /// Show volume sliders
case showContrast
- // Show volume sliders
+ /// Show volume sliders
case hideVolume
- // Lower via software after brightness
+ /// Lower via software after brightness
case disableCombinedBrightness
- // Use separated OSD scale for combined brightness
+ /// Use separated OSD scale for combined brightness
case separateCombinedScale
- // Do not show sliders for Apple displays (including built-in display) in menu
+ /// Do not show sliders for Apple displays (including built-in display) in menu
case hideAppleFromMenu
- // Disable slider snapping
+ /// Disable slider snapping
case enableSliderSnap
- // Disable slider snapping
+ /// Disable slider snapping
case enableSliderPercent
- // Show tick marks for sliders
+ /// Show tick marks for sliders
case showTickMarks
- // Instead of assuming default values, enable read or write upon startup (according to readDDCInsteadOfRestoreValues)
+ /// Instead of assuming default values, enable read or write upon startup (according to readDDCInsteadOfRestoreValues)
case startupAction
- // Show advanced options under Displays tab in Settings
+ /// Show advanced options under Displays tab in Settings
case showAdvancedSettings
- // Allow zero software brightness
+ /// Allow zero software brightness
case allowZeroSwBrightness
- // Keyboard brightness control for multiple displays
+ /// Keyboard brightness control for multiple displays
case multiKeyboardBrightness
- // Keyboard volume control for multiple devices
+ /// Keyboard volume control for multiple devices
case multiKeyboardVolume
- // Use fine OSD scale for brightness
+ /// Use fine OSD scale for brightness
case useFineScaleBrightness
- // Use fine OSD scale for volume
+ /// Use fine OSD scale for volume
case useFineScaleVolume
- // Use smoothBrightness
+ /// Use smoothBrightness
case disableSmoothBrightness
- // Synchronize brightness from sync source displays among all other displays
+ /// Synchronize brightness from sync source displays among all other displays
case enableBrightnessSync
- // Sliders for multiple displays
+ /// Sliders for multiple displays
case multiSliders
/* -- Display specific settings */
- // Enable mute DDC for display
+ /// Enable mute DDC for display
case enableMuteUnmute
- // Hide OSD for display
+ /// Hide OSD for display
case hideOsd
- // Longer delay DDC for display
+ /// Longer delay DDC for display
case longerDelay
- // DDC polling mode for display
+ /// DDC polling mode for display
case pollingMode
- // DDC polling count for display
+ /// DDC polling count for display
case pollingCount
- // Display should avoid gamma table manipulation and use shades instead (to coexist with other apps doing gamma manipulation)
+ /// Display should avoid gamma table manipulation and use shades instead (to coexist with other apps doing gamma manipulation)
case avoidGamma
- // User assigned audio device name for display
+ /// User assigned audio device name for display
case audioDeviceNameOverride
- // Display disabled for keyboard control
+ /// Display disabled for keyboard control
case isDisabled
- // Force software mode for display
+ /// Force software mode for display
case forceSw
- // Software brightness for display
+ /// Software brightness for display
case SwBrightness
- // Combined brightness switching point
+ /// Combined brightness switching point
case combinedBrightnessSwitchingPoint
- // Friendly name
+ /// Friendly name
case friendlyName
/* -- Display+Command specific settings -- */
- // Command value display
+ /// Command value display
case value
- // Was the setting ever changed by the user?
+ /// Was the setting ever changed by the user?
case isTouched
- // Min command value display
+ /// Min command value display
case minDDCOverride
- // Max command value display
+ /// Max command value display
case maxDDC
- // Max user override command value display
+ /// Max user override command value display
case maxDDCOverride
- // Max command value display
+ /// Max command value display
case curveDDC
- // Is the specific control is set as unavailable for display?
+ /// Is the specific control is set as unavailable for display?
case unavailableDDC
- // Invert DDC scale?
+ /// Invert DDC scale?
case invertDDC
- // Override DDC control command code
+ /// Override DDC control command code
case remapDDC
}
diff --git a/MonitorControl/Extensions/Preferences+Extension.swift b/MonitorControl/Extensions/Preferences+Extension.swift
index 2e4e4f0ce..c52d29bd2 100644
--- a/MonitorControl/Extensions/Preferences+Extension.swift
+++ b/MonitorControl/Extensions/Preferences+Extension.swift
@@ -1,7 +1,6 @@
// Copyright © MonitorControl. @JoniVR, @theOneyouseek, @waydabber and others
import Cocoa
-
import Settings
extension Settings.PaneIdentifier {
diff --git a/MonitorControl/Info.plist b/MonitorControl/Info.plist
index 79873fe97..611dbe95a 100644
--- a/MonitorControl/Info.plist
+++ b/MonitorControl/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
-
7141
+
7146
LSApplicationCategoryType
public.app-category.utilities
LSMinimumSystemVersion
diff --git a/MonitorControl/Support/AppDelegate.swift b/MonitorControl/Support/AppDelegate.swift
index 4b3cbafb2..6a7c9767f 100644
--- a/MonitorControl/Support/AppDelegate.swift
+++ b/MonitorControl/Support/AppDelegate.swift
@@ -16,6 +16,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
item.behavior = .removalAllowed
return item
}()
+
var mediaKeyTap = MediaKeyTapManager()
var keyboardShortcuts = KeyboardShortcutsManager()
let coreAudio = SimplyCoreAudio()
@@ -81,9 +82,27 @@ class AppDelegate: NSObject, NSApplicationDelegate {
self.settingsWindowController.show()
}
+ private func currentReopenSenderBundleIdentifier() -> String? {
+ guard let event = NSAppleEventManager.shared().currentAppleEvent else { return nil }
+ if let pid = event.attributeDescriptor(forKeyword: keySenderPIDAttr)?.int32Value,
+ let sender = NSRunningApplication(processIdentifier: pid_t(pid))
+ {
+ return sender.bundleIdentifier
+ }
+ if let pid = event.attributeDescriptor(forKeyword: keyAddressAttr)?.coerce(toDescriptorType: typeKernelProcessID)?.int32Value,
+ let sender = NSRunningApplication(processIdentifier: pid_t(pid))
+ {
+ return sender.bundleIdentifier
+ }
+ return nil
+ }
+
func applicationShouldHandleReopen(_: NSApplication, hasVisibleWindows _: Bool) -> Bool {
- app.prefsClicked(self)
- return true
+ guard let senderBundleIdentifier = self.currentReopenSenderBundleIdentifier()?.lowercased(),
+ !senderBundleIdentifier.contains("workflow"), !senderBundleIdentifier.contains("shortcut")
+ else { return false }
+ self.prefsClicked(self)
+ return false
}
func applicationWillTerminate(_: Notification) {
@@ -176,7 +195,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(self.sleepNotification), name: NSWorkspace.willSleepNotification, object: nil)
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(self.wakeNotification), name: NSWorkspace.didWakeNotification, object: nil)
_ = DistributedNotificationCenter.default().addObserver(forName: NSNotification.Name(rawValue: NSNotification.Name.accessibilityApi.rawValue), object: nil, queue: nil) { _ in DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { self.updateMediaKeyTap() } } // listen for accessibility status changes
- self.statusItemObserver = statusItem.observe(\.isVisible, options: [.old, .new]) { _, _ in self.statusItemVisibilityChanged() }
+ self.statusItemObserver = self.statusItem.observe(\.isVisible, options: [.old, .new]) { _, _ in self.statusItemVisibilityChanged() }
}
@objc private func sleepNotification() {
@@ -360,16 +379,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
onboardingVc?.window?.center()
NSApp.activate(ignoringOtherApps: true)
}
-
+
private func statusItemVisibilityChanged() {
if !self.statusItem.isVisible, self.statusItemVisibilityChangedByUser {
prefs.set(MenuIcon.hide.rawValue, forKey: PrefKey.menuIcon.rawValue)
}
}
-
+
func updateStatusItemVisibility(_ visible: Bool) {
- statusItemVisibilityChangedByUser = false
- statusItem.isVisible = visible
- statusItemVisibilityChangedByUser = true
+ self.statusItemVisibilityChangedByUser = false
+ self.statusItem.isVisible = visible
+ self.statusItemVisibilityChangedByUser = true
}
}
diff --git a/MonitorControl/Support/Arm64DDC.swift b/MonitorControl/Support/Arm64DDC.swift
index e8d149508..df59262e2 100644
--- a/MonitorControl/Support/Arm64DDC.swift
+++ b/MonitorControl/Support/Arm64DDC.swift
@@ -116,7 +116,7 @@ class Arm64DDC: NSObject {
return success
}
- // DDC checksum calculator
+ /// DDC checksum calculator
static func checksum(chk: UInt8, data: inout [UInt8], start: Int, end: Int) -> UInt8 {
var chkd: UInt8 = chk
for i in start ... end {
diff --git a/MonitorControl/Support/DisplayManager.swift b/MonitorControl/Support/DisplayManager.swift
index 00a4627a5..bdef24430 100644
--- a/MonitorControl/Support/DisplayManager.swift
+++ b/MonitorControl/Support/DisplayManager.swift
@@ -243,43 +243,43 @@ class DisplayManager {
func sortDisplays() {
// Opsiyonel: sıralamadan önce log al
- let before = displays.map { $0.name }
+ let before = self.displays.map(\.name)
os_log("Displays before sorting: %{public}@", before)
-
+
// In‑place sıralama
- displays.sort { lhs, rhs in
+ self.displays.sort { lhs, rhs in
lhs.name.localizedStandardCompare(rhs.name) == .orderedAscending
}
-
+
// Opsiyonel: sıralamadan sonra log al
- let after = displays.map { $0.name }
+ let after = self.displays.map(\.name)
os_log("Displays after sorting: %{public}@", after)
}
-
+
func sortDisplaysByFriendlyName() -> [Display] {
- return displays.sorted { lhs, rhs in
- let lhsTitle = lhs.readPrefAsString(key: .friendlyName).isEmpty
- ? lhs.name
- : lhs.readPrefAsString(key: .friendlyName)
- let rhsTitle = rhs.readPrefAsString(key: .friendlyName).isEmpty
- ? rhs.name
- : rhs.readPrefAsString(key: .friendlyName)
- return lhsTitle.localizedStandardCompare(rhsTitle) == .orderedDescending
- }
+ self.displays.sorted { lhs, rhs in
+ let lhsTitle = lhs.readPrefAsString(key: .friendlyName).isEmpty
+ ? lhs.name
+ : lhs.readPrefAsString(key: .friendlyName)
+ let rhsTitle = rhs.readPrefAsString(key: .friendlyName).isEmpty
+ ? rhs.name
+ : rhs.readPrefAsString(key: .friendlyName)
+ return lhsTitle.localizedStandardCompare(rhsTitle) == .orderedDescending
+ }
}
-
-
/// displays dizisini sıralar ve döner
func getAllDisplays() -> [Display] {
- return displays
+ self.displays
}
func getDdcCapableDisplays() -> [OtherDisplay] {
self.displays.compactMap { display -> OtherDisplay? in
if let otherDisplay = display as? OtherDisplay, !otherDisplay.isSw() {
return otherDisplay
- } else { return nil }
+ } else {
+ return nil
+ }
}
}
@@ -314,7 +314,7 @@ class DisplayManager {
func clearDisplays() {
self.displays = []
}
-
+
func addDisplayCounterSuffixes() {
var nameDisplays: [String: [Display]] = [:]
for display in self.displays {
diff --git a/MonitorControl/Support/SliderHandler.swift b/MonitorControl/Support/SliderHandler.swift
index de28e9085..c781f52f0 100644
--- a/MonitorControl/Support/SliderHandler.swift
+++ b/MonitorControl/Support/SliderHandler.swift
@@ -180,7 +180,7 @@ class SliderHandler {
}
}
- // Credits for this class go to @thompsonate - https://github.com/thompsonate/Scrollable-NSSlider
+ /// Credits for this class go to @thompsonate - https://github.com/thompsonate/Scrollable-NSSlider
override func scrollWheel(with event: NSEvent) {
guard self.isEnabled else { return }
let range = Float(self.maxValue - self.minValue)
diff --git a/MonitorControl/View Controllers/Preferences/MenuslidersPrefsViewController.swift b/MonitorControl/View Controllers/Preferences/MenuslidersPrefsViewController.swift
index 3d132eaa2..392a80f86 100644
--- a/MonitorControl/View Controllers/Preferences/MenuslidersPrefsViewController.swift
+++ b/MonitorControl/View Controllers/Preferences/MenuslidersPrefsViewController.swift
@@ -211,8 +211,8 @@ class MenuslidersPrefsViewController: NSViewController, SettingsPane {
app.updateMenusAndKeys()
self.updateGridLayout()
}
-
- override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
+
+ override func observeValue(forKeyPath keyPath: String?, of object: Any?, change _: [NSKeyValueChangeKey: Any]?, context _: UnsafeMutableRawPointer?) {
guard let object = object as? AnyObject else { return }
if object === prefs, keyPath == PrefKey.menuIcon.rawValue {
self.populateSettings()
diff --git a/MonitorControl/main.swift b/MonitorControl/main.swift
index 0b628c728..17f735ec2 100644
--- a/MonitorControl/main.swift
+++ b/MonitorControl/main.swift
@@ -10,7 +10,7 @@ let DEBUG_MACOS10 = false
let DEBUG_GAMMA_ENFORCER = false
let DDC_MAX_DETECT_LIMIT: Int = 100
-// Version
+/// Version
let MIN_PREVIOUS_BUILD_NUMBER = 6262
// App
diff --git a/MonitorControlHelper/Info.plist b/MonitorControlHelper/Info.plist
index e6cc0a2fb..2d6870508 100644
--- a/MonitorControlHelper/Info.plist
+++ b/MonitorControlHelper/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
-
7141
+
7146
LSApplicationCategoryType
public.app-category.utilities
LSBackgroundOnly
From 21e82e494bb0b40bc1372054f0613f3744171456 Mon Sep 17 00:00:00 2001
From: waydabber <37590873+waydabber@users.noreply.github.com>
Date: Tue, 15 Sep 2026 09:21:22 +0200
Subject: [PATCH 4/8] Version bump to v4.4.0
---
MonitorControl.xcodeproj/project.pbxproj | 4 ++--
MonitorControl/Info.plist | 2 +-
MonitorControlHelper/Info.plist | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/MonitorControl.xcodeproj/project.pbxproj b/MonitorControl.xcodeproj/project.pbxproj
index 8c3222b0a..dc1f98927 100644
--- a/MonitorControl.xcodeproj/project.pbxproj
+++ b/MonitorControl.xcodeproj/project.pbxproj
@@ -881,7 +881,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
- MARKETING_VERSION = 4.3.4;
+ MARKETING_VERSION = 4.4.0;
PRODUCT_BUNDLE_IDENTIFIER = app.monitorcontrol.MonitorControl;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -918,7 +918,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
- MARKETING_VERSION = 4.3.4;
+ MARKETING_VERSION = 4.4.0;
PRODUCT_BUNDLE_IDENTIFIER = app.monitorcontrol.MonitorControl;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
diff --git a/MonitorControl/Info.plist b/MonitorControl/Info.plist
index 611dbe95a..1e739b3e1 100644
--- a/MonitorControl/Info.plist
+++ b/MonitorControl/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
-
7146
+
7147
LSApplicationCategoryType
public.app-category.utilities
LSMinimumSystemVersion
diff --git a/MonitorControlHelper/Info.plist b/MonitorControlHelper/Info.plist
index 2d6870508..b01033e98 100644
--- a/MonitorControlHelper/Info.plist
+++ b/MonitorControlHelper/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
-
7146
+
7147
LSApplicationCategoryType
public.app-category.utilities
LSBackgroundOnly
From 4455fec4cb450a36ccac6f60a70abfe6838e8a30 Mon Sep 17 00:00:00 2001
From: waydabber <37590873+waydabber@users.noreply.github.com>
Date: Tue, 15 Sep 2026 09:35:12 +0200
Subject: [PATCH 5/8] Restored legacy OSD for macOS 26/27
---
.../xcshareddata/swiftpm/Package.resolved | 4 +-
MonitorControl/Info.plist | 2 +-
MonitorControl/Support/OSDUtils.swift | 117 ++++++++++++++----
MonitorControlHelper/Info.plist | 2 +-
4 files changed, 97 insertions(+), 28 deletions(-)
diff --git a/MonitorControl.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/MonitorControl.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index e90afb093..b1cca8445 100644
--- a/MonitorControl.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/MonitorControl.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -6,8 +6,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/sindresorhus/KeyboardShortcuts",
"state" : {
- "revision" : "8a2cc9130b0eec6e1dae0a9a405a17741437caa5",
- "version" : "1.6.0"
+ "revision" : "ac12762853126cf2e7ad63a6a58e1c9f58c6a0ee",
+ "version" : "1.17.0"
}
},
{
diff --git a/MonitorControl/Info.plist b/MonitorControl/Info.plist
index 1e739b3e1..bbf53b55b 100644
--- a/MonitorControl/Info.plist
+++ b/MonitorControl/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
-
7147
+
7149
LSApplicationCategoryType
public.app-category.utilities
LSMinimumSystemVersion
diff --git a/MonitorControl/Support/OSDUtils.swift b/MonitorControl/Support/OSDUtils.swift
index a2502732c..5f6552078 100644
--- a/MonitorControl/Support/OSDUtils.swift
+++ b/MonitorControl/Support/OSDUtils.swift
@@ -1,6 +1,7 @@
// Copyright © MonitorControl. @victorchabbert, @JoniVR, @theOneyouseek, @waydabber and others
import Cocoa
+import os.log
class OSDUtils: NSObject {
enum OSDImage: Int64 {
@@ -22,43 +23,47 @@ class OSDUtils: NSObject {
}
static func showOsd(displayID: CGDirectDisplayID, command: Command, value: Float, maxValue: Float = 1, roundChiclet: Bool = false, lock: Bool = false) {
- guard let manager = OSDManager.sharedManager() as? OSDManager else {
- return
- }
- let osdImage = self.getOSDImageByCommand(command: command, value: value)
- let filledChiclets: Int
- let totalChiclets: Int
- if roundChiclet {
- let osdChiclet = OSDUtils.chiclet(fromValue: value, maxValue: maxValue)
- filledChiclets = Int(round(osdChiclet))
- totalChiclets = 16
- } else {
- filledChiclets = Int(value * 100)
- totalChiclets = Int(maxValue * 100)
+ self.withOSD { manager in
+ let osdImage = self.getOSDImageByCommand(command: command, value: value)
+ let filledChiclets: Int
+ let totalChiclets: Int
+ if roundChiclet {
+ let osdChiclet = OSDUtils.chiclet(fromValue: value, maxValue: maxValue)
+ filledChiclets = Int(round(osdChiclet))
+ totalChiclets = 16
+ } else {
+ filledChiclets = Int(value * 100)
+ totalChiclets = Int(maxValue * 100)
+ }
+ manager.showImage(osdImage.rawValue, onDisplayID: displayID, priority: 0x1F4, msecUntilFade: 1000, filledChiclets: UInt32(filledChiclets), totalChiclets: UInt32(totalChiclets), locked: lock)
}
- manager.showImage(osdImage.rawValue, onDisplayID: displayID, priority: 0x1F4, msecUntilFade: 1000, filledChiclets: UInt32(filledChiclets), totalChiclets: UInt32(totalChiclets), locked: lock)
}
static func showOsdVolumeDisabled(displayID: CGDirectDisplayID) {
- guard let manager = OSDManager.sharedManager() as? OSDManager else {
- return
+ self.withOSD { manager in
+ manager.showImage(22, onDisplayID: displayID, priority: 0x1F4, msecUntilFade: 1000)
}
- manager.showImage(22, onDisplayID: displayID, priority: 0x1F4, msecUntilFade: 1000)
}
static func showOsdMuteDisabled(displayID: CGDirectDisplayID) {
- guard let manager = OSDManager.sharedManager() as? OSDManager else {
- return
+ self.withOSD { manager in
+ manager.showImage(21, onDisplayID: displayID, priority: 0x1F4, msecUntilFade: 1000)
}
- manager.showImage(21, onDisplayID: displayID, priority: 0x1F4, msecUntilFade: 1000)
}
static func popEmptyOsd(displayID: CGDirectDisplayID, command: Command) {
- guard let manager = OSDManager.sharedManager() as? OSDManager else {
- return
+ self.withOSD { manager in
+ let osdImage = self.getOSDImageByCommand(command: command)
+ manager.showImage(osdImage.rawValue, onDisplayID: displayID, priority: 0x1F4, msecUntilFade: 0)
+ }
+ }
+
+ private static func withOSD(_ show: @escaping (OSDUIHelperProtocol) -> Void) {
+ if #available(macOS 26.0, *), ProcessInfo.processInfo.operatingSystemVersion.majorVersion <= 27 {
+ TraditionalOSD.show(show)
+ } else if let manager = OSDManager.sharedManager() as? OSDManager {
+ show(manager)
}
- let osdImage = self.getOSDImageByCommand(command: command)
- manager.showImage(osdImage.rawValue, onDisplayID: displayID, priority: 0x1F4, msecUntilFade: 0)
}
static let chicletCount: Float = 16
@@ -75,3 +80,67 @@ class OSDUtils: NSObject {
abs(chiclet.rounded(.towardZero) - chiclet)
}
}
+
+@available(macOS 26.0, *)
+private enum TraditionalOSD {
+ private static var connection: NSXPCConnection?
+
+ static func show(_ show: @escaping (OSDUIHelperProtocol) -> Void) {
+ guard Thread.isMainThread else {
+ DispatchQueue.main.async {
+ self.show(show)
+ }
+ return
+ }
+ guard let helper = self.helper() else { return }
+ show(helper)
+ }
+
+ private static func helper() -> OSDUIHelperProtocol? {
+ let connection: NSXPCConnection
+ if let existingConnection = self.connection {
+ connection = existingConnection
+ } else {
+ connection = NSXPCConnection(machServiceName: "com.apple.OSDUIHelper", options: [])
+ connection.remoteObjectInterface = NSXPCInterface(with: OSDUIHelperProtocol.self)
+ connection.interruptionHandler = { [weak connection] in
+ os_log("Traditional OSD XPC connection interrupted", type: .error)
+ guard let connection = connection else { return }
+ DispatchQueue.main.async {
+ self.reset(connection)
+ }
+ }
+ connection.invalidationHandler = { [weak connection] in
+ guard let connection = connection else { return }
+ DispatchQueue.main.async {
+ if self.connection === connection {
+ self.connection = nil
+ }
+ }
+ }
+ self.connection = connection
+ connection.resume()
+ }
+ let proxy = connection.remoteObjectProxyWithErrorHandler { [weak connection] error in
+ os_log("Traditional OSD XPC proxy error: %{public}@", type: .error, error.localizedDescription)
+ guard let connection = connection else { return }
+ DispatchQueue.main.async {
+ self.reset(connection)
+ }
+ }
+ guard let helper = proxy as? OSDUIHelperProtocol else {
+ os_log("Unable to obtain Traditional OSD XPC proxy", type: .error)
+ self.reset(connection)
+ return nil
+ }
+ return helper
+ }
+
+ private static func reset(_ connection: NSXPCConnection) {
+ guard self.connection === connection else { return }
+ connection.interruptionHandler = nil
+ connection.invalidationHandler = nil
+ connection.invalidate()
+ self.connection = nil
+ }
+}
diff --git a/MonitorControlHelper/Info.plist b/MonitorControlHelper/Info.plist
index b01033e98..402112f6e 100644
--- a/MonitorControlHelper/Info.plist
+++ b/MonitorControlHelper/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
-
7147
+
7149
LSApplicationCategoryType
public.app-category.utilities
LSBackgroundOnly
From 909c1cce2577b158f214ae4598874bc66a4b439b Mon Sep 17 00:00:00 2001
From: waydabber <37590873+waydabber@users.noreply.github.com>
Date: Tue, 15 Sep 2026 10:21:43 +0200
Subject: [PATCH 6/8] Fixing KeyboardShortcuts
---
MonitorControl.xcodeproj/project.pbxproj | 21 +-
.../xcshareddata/swiftpm/Package.resolved | 11 +-
.../KeyboardShortcuts+Extension.swift | 2 -
MonitorControl/Info.plist | 2 +-
.../Support/KeyboardShortcuts.swift | 821 ++++++++++++++++++
.../Support/KeyboardShortcutsManager.swift | 4 -
.../KeyboardPrefsViewController.swift | 26 +-
MonitorControlHelper/Info.plist | 2 +-
8 files changed, 846 insertions(+), 43 deletions(-)
create mode 100644 MonitorControl/Support/KeyboardShortcuts.swift
diff --git a/MonitorControl.xcodeproj/project.pbxproj b/MonitorControl.xcodeproj/project.pbxproj
index dc1f98927..f565cbde1 100644
--- a/MonitorControl.xcodeproj/project.pbxproj
+++ b/MonitorControl.xcodeproj/project.pbxproj
@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
+ CB0D15010000000000000001 /* KeyboardShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB0D15010000000000000002 /* KeyboardShortcuts.swift */; };
28D1DDF2227FBE71004CB494 /* NSScreen+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28D1DDF1227FBE71004CB494 /* NSScreen+Extension.swift */; };
28D1DDF3227FC8C6004CB494 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 56754EB01D9A4016007BCDC5 /* Assets.xcassets */; };
56754EAF1D9A4016007BCDC5 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56754EAE1D9A4016007BCDC5 /* main.swift */; };
@@ -28,7 +29,6 @@
AA25F6D726E68C160087F3A2 /* MediaKeyTapManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA25F6D626E68C160087F3A2 /* MediaKeyTapManager.swift */; };
AA3B4A2826AE103C00B74CD2 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = AA3B4A2726AE103C00B74CD2 /* README.md */; };
AA4398A926DD55DA00943F16 /* IntelDDC.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4398A826DD55DA00943F16 /* IntelDDC.swift */; };
- AA44E703270377C200E06865 /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = AA44E702270377C200E06865 /* KeyboardShortcuts */; };
AA44E7052703790100E06865 /* KeyboardShortcuts+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44E7042703790100E06865 /* KeyboardShortcuts+Extension.swift */; };
AA44E70727038F7F00E06865 /* KeyboardShortcutsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA44E70627038F7F00E06865 /* KeyboardShortcutsManager.swift */; };
AA473EB126DFF8DE0063A181 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA473EB026DFF8DE0063A181 /* Command.swift */; };
@@ -75,6 +75,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ CB0D15010000000000000002 /* KeyboardShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardShortcuts.swift; sourceTree = "
"; };
01D6471D2AF916A800321FFB /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Main.strings; sourceTree = ""; };
01D6471E2AF916A800321FFB /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/InternetAccessPolicy.strings; sourceTree = ""; };
01D6471F2AF916A800321FFB /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Localizable.strings; sourceTree = ""; };
@@ -190,7 +191,6 @@
files = (
AA9AE87126B5BFB700B6CA65 /* CoreDisplay.framework in Frameworks */,
AA70817C27046B9800CC5625 /* Sparkle in Frameworks */,
- AA44E703270377C200E06865 /* KeyboardShortcuts in Frameworks */,
6CD35F53264FFFC6001F1344 /* SimplyCoreAudio in Frameworks */,
6CD35F5626500008001F1344 /* MediaKeyTap in Frameworks */,
AA9AE86F26B5BF3D00B6CA65 /* OSD.framework in Frameworks */,
@@ -313,6 +313,7 @@
F01B068F228221B7008E64DB /* SliderHandler.swift */,
6C85EFD922C941B000227EA1 /* DisplayManager.swift */,
AA25F6D626E68C160087F3A2 /* MediaKeyTapManager.swift */,
+ CB0D15010000000000000002 /* KeyboardShortcuts.swift */,
AA44E70627038F7F00E06865 /* KeyboardShortcutsManager.swift */,
AA16139A26BE772E00DCF027 /* Arm64DDC.swift */,
AA4398A826DD55DA00943F16 /* IntelDDC.swift */,
@@ -394,7 +395,6 @@
packageProductDependencies = (
6CD35F52264FFFC6001F1344 /* SimplyCoreAudio */,
6CD35F5526500008001F1344 /* MediaKeyTap */,
- AA44E702270377C200E06865 /* KeyboardShortcuts */,
AA70817B27046B9800CC5625 /* Sparkle */,
AAD7DD332CAFF3D90062822F /* Settings */,
);
@@ -475,7 +475,6 @@
6CD35F51264FFFC6001F1344 /* XCRemoteSwiftPackageReference "SimplyCoreAudio" */,
6CD35F5426500008001F1344 /* XCRemoteSwiftPackageReference "MediaKeyTap" */,
6CD35F5A2650003F001F1344 /* XCRemoteSwiftPackageReference "Settings" */,
- AA44E701270377C200E06865 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */,
AA70817A27046B9800CC5625 /* XCRemoteSwiftPackageReference "Sparkle" */,
);
productRefGroup = 56754EAC1D9A4016007BCDC5 /* Products */;
@@ -630,6 +629,7 @@
AA25F6D726E68C160087F3A2 /* MediaKeyTapManager.swift in Sources */,
FE4E0896249D584C003A50BB /* OSDUtils.swift in Sources */,
6CBFE27A23DB266000D1BC41 /* Display.swift in Sources */,
+ CB0D15010000000000000001 /* KeyboardShortcuts.swift in Sources */,
AA44E70727038F7F00E06865 /* KeyboardShortcutsManager.swift in Sources */,
F03A8DF21FFBAA6F0034DC27 /* OtherDisplay.swift in Sources */,
AA78BDBD2709FE7B00CA8DF7 /* UpdaterDelegate.swift in Sources */,
@@ -1051,14 +1051,6 @@
kind = branch;
};
};
- AA44E701270377C200E06865 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */ = {
- isa = XCRemoteSwiftPackageReference;
- repositoryURL = "https://github.com/sindresorhus/KeyboardShortcuts";
- requirement = {
- kind = upToNextMajorVersion;
- minimumVersion = 1.0.0;
- };
- };
AA70817A27046B9800CC5625 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/sparkle-project/Sparkle";
@@ -1080,11 +1072,6 @@
package = 6CD35F5426500008001F1344 /* XCRemoteSwiftPackageReference "MediaKeyTap" */;
productName = MediaKeyTap;
};
- AA44E702270377C200E06865 /* KeyboardShortcuts */ = {
- isa = XCSwiftPackageProductDependency;
- package = AA44E701270377C200E06865 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */;
- productName = KeyboardShortcuts;
- };
AA70817B27046B9800CC5625 /* Sparkle */ = {
isa = XCSwiftPackageProductDependency;
package = AA70817A27046B9800CC5625 /* XCRemoteSwiftPackageReference "Sparkle" */;
diff --git a/MonitorControl.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/MonitorControl.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index b1cca8445..844fa8888 100644
--- a/MonitorControl.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/MonitorControl.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -1,15 +1,6 @@
{
- "originHash" : "df195b12c1f6bd471af1cacb709842cb7eb355cce32fee1413f0f7d7aa5c4eb2",
+ "originHash" : "f1e1257b2b6f49d4a2249ee595030e60931b7b84f7eee8197554bac73d4e024c",
"pins" : [
- {
- "identity" : "keyboardshortcuts",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/sindresorhus/KeyboardShortcuts",
- "state" : {
- "revision" : "ac12762853126cf2e7ad63a6a58e1c9f58c6a0ee",
- "version" : "1.17.0"
- }
- },
{
"identity" : "mediakeytap",
"kind" : "remoteSourceControl",
diff --git a/MonitorControl/Extensions/KeyboardShortcuts+Extension.swift b/MonitorControl/Extensions/KeyboardShortcuts+Extension.swift
index 5bbb3d08e..d438168d9 100644
--- a/MonitorControl/Extensions/KeyboardShortcuts+Extension.swift
+++ b/MonitorControl/Extensions/KeyboardShortcuts+Extension.swift
@@ -1,7 +1,5 @@
// Copyright © MonitorControl. @JoniVR, @theOneyouseek, @waydabber and others
-import KeyboardShortcuts
-
extension KeyboardShortcuts.Name {
static let brightnessUp = Self("brightnessUp")
static let brightnessDown = Self("brightnessDown")
diff --git a/MonitorControl/Info.plist b/MonitorControl/Info.plist
index bbf53b55b..1a65eba16 100644
--- a/MonitorControl/Info.plist
+++ b/MonitorControl/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
- 7149
+ 7150
LSApplicationCategoryType
public.app-category.utilities
LSMinimumSystemVersion
diff --git a/MonitorControl/Support/KeyboardShortcuts.swift b/MonitorControl/Support/KeyboardShortcuts.swift
new file mode 100644
index 000000000..2f0b998bc
--- /dev/null
+++ b/MonitorControl/Support/KeyboardShortcuts.swift
@@ -0,0 +1,821 @@
+// Copyright © MonitorControl. @waydabber and others
+// Adapted from BetterDisplay’s KeyboardShortcuts.swift replacement.
+// Preserves the KeyboardShortcuts UserDefaults keys and JSON representation.
+
+import AppKit
+import Carbon
+
+private func shortcutsEventHandler(eventHandlerCall _: EventHandlerCallRef?, event: EventRef?, userData _: UnsafeMutableRawPointer?) -> OSStatus {
+ KeyboardShortcuts.handleEvent(event)
+}
+
+enum KeyboardShortcuts {
+ struct Name: Hashable, RawRepresentable {
+ let rawValue: String
+ let defaultShortcut: Shortcut?
+ var shortcut: Shortcut? {
+ KeyboardShortcuts.getShortcut(for: self)
+ }
+
+ init(_ name: String, default defaultShortcut: Shortcut? = nil) {
+ self.rawValue = name
+ self.defaultShortcut = defaultShortcut
+ if let defaultShortcut, !KeyboardShortcuts.userDefaultsContains(name: self) {
+ KeyboardShortcuts.setShortcut(defaultShortcut, for: self)
+ }
+ }
+
+ init?(rawValue: String) {
+ self.init(rawValue)
+ }
+ }
+
+ struct Key: Hashable, RawRepresentable {
+ static let `return` = Self(kVK_Return)
+ static let space = Self(kVK_Space)
+ static let tab = Self(kVK_Tab)
+ static let pageUp = Self(kVK_PageUp)
+ static let pageDown = Self(kVK_PageDown)
+ static let home = Self(kVK_Home)
+ static let end = Self(kVK_End)
+ static let upArrow = Self(kVK_UpArrow)
+ static let rightArrow = Self(kVK_RightArrow)
+ static let downArrow = Self(kVK_DownArrow)
+ static let leftArrow = Self(kVK_LeftArrow)
+ static let escape = Self(kVK_Escape)
+ static let delete = Self(kVK_Delete)
+ static let deleteForward = Self(kVK_ForwardDelete)
+ static let help = Self(kVK_Help)
+ static let f1 = Self(kVK_F1)
+ static let f2 = Self(kVK_F2)
+ static let f3 = Self(kVK_F3)
+ static let f4 = Self(kVK_F4)
+ static let f5 = Self(kVK_F5)
+ static let f6 = Self(kVK_F6)
+ static let f7 = Self(kVK_F7)
+ static let f8 = Self(kVK_F8)
+ static let f9 = Self(kVK_F9)
+ static let f10 = Self(kVK_F10)
+ static let f11 = Self(kVK_F11)
+ static let f12 = Self(kVK_F12)
+ static let f13 = Self(kVK_F13)
+ static let f14 = Self(kVK_F14)
+ static let f15 = Self(kVK_F15)
+ static let f16 = Self(kVK_F16)
+ static let f17 = Self(kVK_F17)
+ static let f18 = Self(kVK_F18)
+ static let f19 = Self(kVK_F19)
+ static let f20 = Self(kVK_F20)
+
+ let rawValue: Int
+
+ init(rawValue: Int) {
+ self.rawValue = rawValue
+ }
+
+ private init(_ value: Int) {
+ self.init(rawValue: value)
+ }
+ }
+
+ struct Shortcut: Hashable, Codable {
+ let carbonKeyCode: Int
+ let carbonModifiers: Int
+ var key: Key? {
+ Key(rawValue: self.carbonKeyCode)
+ }
+
+ var modifiers: NSEvent.ModifierFlags {
+ NSEvent.ModifierFlags(carbon: self.carbonModifiers)
+ }
+
+ init(carbonKeyCode: Int, carbonModifiers: Int = 0) {
+ self.carbonKeyCode = carbonKeyCode
+ self.carbonModifiers = NSEvent.ModifierFlags(carbon: carbonModifiers).carbon
+ }
+
+ init(event: NSEvent) {
+ self.init(carbonKeyCode: Int(event.keyCode), carbonModifiers: event.modifiers.carbon)
+ }
+ }
+
+ private final class HotKey {
+ let shortcut: Shortcut
+ let id: Int
+ let ref: EventHotKeyRef
+ let onKeyDown: (Shortcut) -> Void
+ let onKeyUp: (Shortcut) -> Void
+
+ init(shortcut: Shortcut, id: Int, ref: EventHotKeyRef, onKeyDown: @escaping (Shortcut) -> Void, onKeyUp: @escaping (Shortcut) -> Void) {
+ self.shortcut = shortcut
+ self.id = id
+ self.ref = ref
+ self.onKeyDown = onKeyDown
+ self.onKeyUp = onKeyUp
+ }
+ }
+
+ private static let userDefaultsPrefix = "KeyboardShortcuts_"
+ private static let hotKeySignature: UInt32 = 1_397_967_699 // "SSKS", for compatibility with existing registrations.
+ private static var hotKeyId = 0
+ private static var eventHandler: EventHandlerRef?
+ private static var hotKeys = [Int: HotKey]()
+ private static var registeredShortcuts = Set()
+ private static var keyDownHandlers = [Name: [() -> Void]]()
+ private static var keyUpHandlers = [Name: [() -> Void]]()
+ private static var disabledNames = Set()
+ static var isPaused = false
+
+ static func onKeyDown(for name: Name, action: @escaping () -> Void) {
+ self.keyDownHandlers[name, default: []].append(action)
+ self.registerShortcutIfNeeded(for: name)
+ }
+
+ static func onKeyUp(for name: Name, action: @escaping () -> Void) {
+ self.keyUpHandlers[name, default: []].append(action)
+ self.registerShortcutIfNeeded(for: name)
+ }
+
+ /// Track disabled names independently so shared bindings remain registered while in use.
+ static func enable(_ name: Name) {
+ self.disabledNames.remove(name)
+ self.registerShortcutIfNeeded(for: name)
+ }
+
+ static func disable(_ name: Name) {
+ self.disabledNames.insert(name)
+ if let shortcut = getShortcut(for: name) {
+ self.unregisterIfUnused(shortcut)
+ }
+ }
+
+ static func removeAllHandlers() {
+ for shortcut in self.shortcutsForHandlers {
+ self.unregister(shortcut)
+ }
+ self.keyDownHandlers = [:]
+ self.keyUpHandlers = [:]
+ }
+
+ static func setShortcut(_ shortcut: Shortcut?, for name: Name) {
+ guard let shortcut else {
+ self.userDefaultsRemove(name: name)
+ return
+ }
+ self.userDefaultsSet(name: name, shortcut: shortcut)
+ }
+
+ static func getShortcut(for name: Name) -> Shortcut? {
+ guard
+ let data = UserDefaults.standard.string(forKey: userDefaultsKey(for: name))?.data(using: .utf8),
+ let shortcut = try? JSONDecoder().decode(Shortcut.self, from: data)
+ else {
+ return nil
+ }
+ return shortcut
+ }
+
+ fileprivate static func handleEvent(_ event: EventRef?) -> OSStatus {
+ guard let event else {
+ return OSStatus(eventNotHandledErr)
+ }
+
+ var eventHotKeyId = EventHotKeyID()
+ let error = GetEventParameter(
+ event,
+ UInt32(kEventParamDirectObject),
+ UInt32(typeEventHotKeyID),
+ nil,
+ MemoryLayout.size,
+ nil,
+ &eventHotKeyId
+ )
+
+ guard error == noErr else {
+ return error
+ }
+ guard eventHotKeyId.signature == self.hotKeySignature, let hotKey = hotKeys[Int(eventHotKeyId.id)] else {
+ return OSStatus(eventNotHandledErr)
+ }
+
+ switch Int(GetEventKind(event)) {
+ case kEventHotKeyPressed:
+ hotKey.onKeyDown(hotKey.shortcut)
+ return noErr
+ case kEventHotKeyReleased:
+ hotKey.onKeyUp(hotKey.shortcut)
+ return noErr
+ default:
+ return OSStatus(eventNotHandledErr)
+ }
+ }
+
+ private static var shortcutsForHandlers: Set {
+ Set([keyDownHandlers.keys, keyUpHandlers.keys].flatMap { $0 }.filter { !disabledNames.contains($0) }.compactMap(\.shortcut))
+ }
+
+ private static func userDefaultsKey(for name: Name) -> String {
+ "\(self.userDefaultsPrefix)\(name.rawValue)"
+ }
+
+ private static func registerShortcutIfNeeded(for name: Name) {
+ guard !self.disabledNames.contains(name), let shortcut = getShortcut(for: name) else {
+ return
+ }
+ self.register(shortcut)
+ }
+
+ private static func register(_ shortcut: Shortcut) {
+ guard !self.registeredShortcuts.contains(shortcut) else {
+ return
+ }
+ self.hotKeyId += 1
+
+ var eventHotKey: EventHotKeyRef?
+ let error = RegisterEventHotKey(
+ UInt32(shortcut.carbonKeyCode),
+ UInt32(shortcut.carbonModifiers),
+ EventHotKeyID(signature: self.hotKeySignature, id: UInt32(self.hotKeyId)),
+ GetEventDispatcherTarget(),
+ 0,
+ &eventHotKey
+ )
+
+ guard error == noErr, let eventHotKey else {
+ return
+ }
+
+ self.hotKeys[self.hotKeyId] = HotKey(
+ shortcut: shortcut,
+ id: self.hotKeyId,
+ ref: eventHotKey,
+ onKeyDown: self.handleOnKeyDown,
+ onKeyUp: self.handleOnKeyUp
+ )
+ self.registeredShortcuts.insert(shortcut)
+ self.setUpEventHandlerIfNeeded()
+ }
+
+ private static func unregister(_ shortcut: Shortcut) {
+ for hotKey in self.hotKeys.values where hotKey.shortcut == shortcut {
+ UnregisterEventHotKey(hotKey.ref)
+ hotKeys.removeValue(forKey: hotKey.id)
+ }
+ self.registeredShortcuts.remove(shortcut)
+ }
+
+ private static func setUpEventHandlerIfNeeded() {
+ guard self.eventHandler == nil, let dispatcher = GetEventDispatcherTarget() else {
+ return
+ }
+ var eventSpecs = [
+ EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed)),
+ EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyReleased)),
+ ]
+ InstallEventHandler(dispatcher, shortcutsEventHandler, eventSpecs.count, &eventSpecs, nil, &self.eventHandler)
+ }
+
+ private static func handleOnKeyDown(_ shortcut: Shortcut) {
+ guard !self.isPaused else {
+ return
+ }
+ for (name, handlers) in self.keyDownHandlers where !self.disabledNames.contains(name) && self.getShortcut(for: name) == shortcut {
+ handlers.forEach { $0() }
+ }
+ }
+
+ private static func handleOnKeyUp(_ shortcut: Shortcut) {
+ guard !self.isPaused else {
+ return
+ }
+ for (name, handlers) in self.keyUpHandlers where !self.disabledNames.contains(name) && self.getShortcut(for: name) == shortcut {
+ handlers.forEach { $0() }
+ }
+ }
+
+ private static func userDefaultsSet(name: Name, shortcut: Shortcut) {
+ guard let encoded = try? JSONEncoder().encode(shortcut), let string = String(data: encoded, encoding: .utf8) else {
+ return
+ }
+ let oldShortcut = self.getShortcut(for: name)
+ UserDefaults.standard.set(string, forKey: self.userDefaultsKey(for: name))
+ if let oldShortcut, oldShortcut != shortcut {
+ self.unregisterIfUnused(oldShortcut)
+ }
+ self.registerShortcutIfNeeded(for: name)
+ self.userDefaultsDidChange(name: name)
+ }
+
+ private static func userDefaultsRemove(name: Name) {
+ guard let shortcut = getShortcut(for: name) else {
+ return
+ }
+ UserDefaults.standard.set(false, forKey: self.userDefaultsKey(for: name))
+ self.unregisterIfUnused(shortcut)
+ self.userDefaultsDidChange(name: name)
+ }
+
+ private static func userDefaultsContains(name: Name) -> Bool {
+ UserDefaults.standard.object(forKey: self.userDefaultsKey(for: name)) != nil
+ }
+
+ private static func userDefaultsDidChange(name: Name) {
+ NotificationCenter.default.post(name: .shortcutByNameDidChange, object: nil, userInfo: ["name": name])
+ }
+
+ private static func unregisterIfUnused(_ shortcut: Shortcut) {
+ guard !self.shortcutsForHandlers.contains(shortcut) else {
+ return
+ }
+ self.unregister(shortcut)
+ }
+}
+
+extension KeyboardShortcuts {
+ final class RecorderCocoa: NSSearchField, NSSearchFieldDelegate {
+ private let minimumWidth = 130.0
+ private var eventMonitor: LocalEventMonitor?
+ private var isRecording = false
+ private var idlePlaceholder: String?
+ private var observer: NSObjectProtocol?
+ private var windowObserver: NSObjectProtocol?
+ private var cancelButton: NSButtonCell?
+
+ var shortcutName: KeyboardShortcuts.Name {
+ didSet {
+ guard self.shortcutName != oldValue else {
+ return
+ }
+ self.setStringValue()
+ DispatchQueue.main.async {
+ self.window?.makeFirstResponder(nil)
+ }
+ }
+ }
+
+ override var canBecomeKeyView: Bool {
+ false
+ }
+
+ override var intrinsicContentSize: CGSize {
+ var size = super.intrinsicContentSize
+ size.width = self.minimumWidth
+ return size
+ }
+
+ private var showsCancelButton: Bool {
+ get { (cell as? NSSearchFieldCell)?.cancelButtonCell != nil }
+ set { (cell as? NSSearchFieldCell)?.cancelButtonCell = newValue ? self.cancelButton : nil }
+ }
+
+ init(for name: KeyboardShortcuts.Name) {
+ self.shortcutName = name
+ super.init(frame: .zero)
+ delegate = self
+ placeholderString = NSLocalizedString("Record", comment: "Shortcut recorder placeholder")
+ alignment = .center
+ baseWritingDirection = .leftToRight // Keep modifier glyphs before digit and arrow keys in RTL.
+ (cell as? NSSearchFieldCell)?.searchButtonCell = nil
+ wantsLayer = true
+ translatesAutoresizingMaskIntoConstraints = false
+ setContentHuggingPriority(.defaultHigh, for: .vertical)
+ setContentHuggingPriority(.defaultHigh, for: .horizontal)
+ widthAnchor.constraint(greaterThanOrEqualToConstant: self.minimumWidth).isActive = true
+ self.cancelButton = (cell as? NSSearchFieldCell)?.cancelButtonCell
+ self.setStringValue()
+ self.observer = NotificationCenter.default.addObserver(forName: .shortcutByNameDidChange, object: nil, queue: nil) { [weak self] notification in
+ guard
+ let self,
+ let name = notification.userInfo?["name"] as? KeyboardShortcuts.Name,
+ name == self.shortcutName
+ else {
+ return
+ }
+ self.setStringValue()
+ }
+ }
+
+ required init?(coder _: NSCoder) {
+ nil
+ }
+
+ deinit {
+ if isRecording {
+ KeyboardShortcuts.isPaused = false
+ }
+ if let observer {
+ NotificationCenter.default.removeObserver(observer)
+ }
+ if let windowObserver {
+ NotificationCenter.default.removeObserver(windowObserver)
+ }
+ }
+
+ override func viewDidMoveToWindow() {
+ super.viewDidMoveToWindow()
+ if let windowObserver = self.windowObserver {
+ NotificationCenter.default.removeObserver(windowObserver)
+ self.windowObserver = nil
+ }
+ self.stopRecording()
+ guard let window else { return }
+ self.windowObserver = NotificationCenter.default.addObserver(forName: NSWindow.didResignKeyNotification, object: window, queue: nil) { [weak self] _ in
+ guard let self, self.isRecording else { return }
+ self.stopRecording()
+ self.window?.makeFirstResponder(nil)
+ }
+ }
+
+ func controlTextDidChange(_: Notification) {
+ let recordedShortcutGlyph = KeyboardShortcuts.getShortcut(for: self.shortcutName)?.shortcutGlyph ?? ""
+ if stringValue.isEmpty {
+ self.saveShortcut(nil)
+ } else if self.isRecording, stringValue != recordedShortcutGlyph {
+ NSSound.beep()
+ self.setStringValue()
+ }
+ self.showsCancelButton = !stringValue.isEmpty
+ if stringValue.isEmpty {
+ window?.makeFirstResponder(self)
+ }
+ }
+
+ func controlTextDidBeginEditing(_: Notification) {
+ self.startRecording()
+ }
+
+ func controlTextDidEndEditing(_: Notification) {
+ self.stopRecording()
+ }
+
+ override func mouseDown(with event: NSEvent) {
+ super.mouseDown(with: event)
+ self.startRecording()
+ }
+
+ private func stopRecording() {
+ guard self.isRecording else { return }
+ self.eventMonitor = nil
+ self.isRecording = false
+ placeholderString = self.idlePlaceholder
+ self.showsCancelButton = !stringValue.isEmpty
+ KeyboardShortcuts.isPaused = false
+ }
+
+ override func becomeFirstResponder() -> Bool {
+ let shouldBecomeFirstResponder = super.becomeFirstResponder()
+ guard shouldBecomeFirstResponder else {
+ return shouldBecomeFirstResponder
+ }
+
+ self.startRecording()
+
+ return shouldBecomeFirstResponder
+ }
+
+ private func startRecording() {
+ guard window != nil, !self.isRecording else {
+ return
+ }
+
+ self.idlePlaceholder = placeholderString
+ self.isRecording = true
+ placeholderString = NSLocalizedString("Press Shortcut", comment: "Shortcut recorder prompt")
+ self.showsCancelButton = !stringValue.isEmpty
+ (currentEditor() as? NSTextView)?.insertionPointColor = .clear
+ KeyboardShortcuts.isPaused = true
+ self.eventMonitor = LocalEventMonitor(events: [.keyDown, .leftMouseUp, .rightMouseUp]) { [weak self] event in
+ self?.handle(event)
+ }.start()
+ }
+
+ private func handle(_ event: NSEvent) -> NSEvent? {
+ let clickPoint = convert(event.locationInWindow, from: nil)
+ if event.type == .leftMouseUp || event.type == .rightMouseUp, !bounds.insetBy(dx: -3, dy: -3).contains(clickPoint) {
+ window?.makeFirstResponder(nil)
+ return event
+ }
+
+ guard event.isKeyEvent else {
+ return nil
+ }
+
+ if event.modifiers.isEmpty, event.specialKey == .tab {
+ window?.makeFirstResponder(nil)
+ return event
+ }
+
+ if event.modifiers.isEmpty, event.keyCode == kVK_Escape {
+ window?.makeFirstResponder(nil)
+ return nil
+ }
+
+ if event.modifiers.isEmpty, event.specialKey == .delete || event.specialKey == .deleteForward || event.specialKey == .backspace {
+ self.clear()
+ return nil
+ }
+
+ guard !event.modifiers.subtracting(.shift).isEmpty || event.specialKey?.isSupportedFunctionKey == true else {
+ NSSound.beep()
+ return nil
+ }
+
+ let shortcut = KeyboardShortcuts.Shortcut(event: event)
+
+ if let menuItem = shortcut.takenByMainMenu {
+ window?.makeFirstResponder(nil)
+ NSAlert.showModal(for: window, title: String(format: NSLocalizedString("This keyboard shortcut is already used by the menu item \"%@\".", comment: "Shortcut conflict; menu item title"), menuItem.title))
+ window?.makeFirstResponder(self)
+ return nil
+ }
+
+ guard !shortcut.isTakenBySystem else {
+ window?.makeFirstResponder(nil)
+ NSAlert.showModal(for: window, title: NSLocalizedString("This keyboard shortcut is used by the system.", comment: "Shortcut conflict"), message: NSLocalizedString("Keyboard shortcuts can be changed in system keyboard settings.", comment: "Shortcut conflict guidance"))
+ window?.makeFirstResponder(self)
+ return nil
+ }
+
+ stringValue = shortcut.shortcutGlyph
+ self.showsCancelButton = true
+ self.saveShortcut(shortcut)
+ window?.makeFirstResponder(nil)
+ return nil
+ }
+
+ private func setStringValue() {
+ stringValue = KeyboardShortcuts.getShortcut(for: self.shortcutName)?.shortcutGlyph ?? ""
+ self.showsCancelButton = !stringValue.isEmpty
+ }
+
+ private func saveShortcut(_ shortcut: KeyboardShortcuts.Shortcut?) {
+ KeyboardShortcuts.setShortcut(shortcut, for: self.shortcutName)
+ }
+
+ private func clear() {
+ (cell as? NSSearchFieldCell)?.cancelButtonCell?.performClick(self)
+ }
+ }
+}
+
+private final class LocalEventMonitor {
+ private let events: NSEvent.EventTypeMask
+ private let callback: (NSEvent) -> NSEvent?
+ private var monitor: AnyObject?
+
+ init(events: NSEvent.EventTypeMask, callback: @escaping (NSEvent) -> NSEvent?) {
+ self.events = events
+ self.callback = callback
+ }
+
+ deinit {
+ stop()
+ }
+
+ @discardableResult
+ func start() -> Self {
+ self.monitor = NSEvent.addLocalMonitorForEvents(matching: self.events, handler: self.callback) as AnyObject
+ return self
+ }
+
+ private func stop() {
+ guard let monitor else {
+ return
+ }
+ NSEvent.removeMonitor(monitor)
+ }
+}
+
+private let keyToCharacterMapping: [KeyboardShortcuts.Key: String] = [
+ .return: "↩",
+ .delete: "⌫",
+ .deleteForward: "⌦",
+ .end: "↘",
+ .escape: "⎋",
+ .help: "?⃝",
+ .home: "↖",
+ .space: "⎵",
+ .tab: "⇥",
+ .pageUp: "⇞",
+ .pageDown: "⇟",
+ .upArrow: "↑",
+ .rightArrow: "→",
+ .downArrow: "↓",
+ .leftArrow: "←",
+ .f1: "F1",
+ .f2: "F2",
+ .f3: "F3",
+ .f4: "F4",
+ .f5: "F5",
+ .f6: "F6",
+ .f7: "F7",
+ .f8: "F8",
+ .f9: "F9",
+ .f10: "F10",
+ .f11: "F11",
+ .f12: "F12",
+ .f13: "F13",
+ .f14: "F14",
+ .f15: "F15",
+ .f16: "F16",
+ .f17: "F17",
+ .f18: "F18",
+ .f19: "F19",
+ .f20: "F20",
+]
+
+private extension KeyboardShortcuts.Shortcut {
+ static var system: [Self] {
+ var shortcutsUnmanaged: Unmanaged?
+ guard
+ CopySymbolicHotKeys(&shortcutsUnmanaged) == noErr,
+ let shortcuts = shortcutsUnmanaged?.takeRetainedValue() as? [[String: Any]]
+ else {
+ return []
+ }
+
+ return shortcuts.compactMap {
+ guard
+ ($0[kHISymbolicHotKeyEnabled] as? Bool) == true,
+ let carbonKeyCode = $0[kHISymbolicHotKeyCode] as? Int,
+ let carbonModifiers = $0[kHISymbolicHotKeyModifiers] as? Int
+ else {
+ return nil
+ }
+ return Self(carbonKeyCode: carbonKeyCode, carbonModifiers: carbonModifiers)
+ }
+ }
+
+ var isTakenBySystem: Bool {
+ self != Self(carbonKeyCode: kVK_F12) && Self.system.contains(self)
+ }
+
+ var takenByMainMenu: NSMenuItem? {
+ guard let mainMenu = NSApp.mainMenu else {
+ return nil
+ }
+ return self.menuItemWithMatchingShortcut(in: mainMenu)
+ }
+
+ func menuItemWithMatchingShortcut(in menu: NSMenu) -> NSMenuItem? {
+ for item in menu.items {
+ if self.keyToCharacter() == item.keyEquivalent, modifiers == item.keyEquivalentModifierMask {
+ return item
+ }
+ if let submenu = item.submenu, let menuItem = menuItemWithMatchingShortcut(in: submenu) {
+ return menuItem
+ }
+ }
+ return nil
+ }
+
+ func keyToCharacter() -> String? {
+ if let key, let character = keyToCharacterMapping[key] {
+ return character
+ }
+
+ guard
+ let source = TISCopyCurrentASCIICapableKeyboardLayoutInputSource()?.takeRetainedValue(),
+ let layoutDataPointer = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData)
+ else {
+ return nil
+ }
+
+ let layoutData = unsafeBitCast(layoutDataPointer, to: CFData.self)
+ let keyLayout = unsafeBitCast(CFDataGetBytePtr(layoutData), to: UnsafePointer.self)
+ var deadKeyState: UInt32 = 0
+ let maxLength = 4
+ var length = 0
+ var characters = [UniChar](repeating: 0, count: maxLength)
+
+ let error = UCKeyTranslate(
+ keyLayout,
+ UInt16(carbonKeyCode),
+ UInt16(kUCKeyActionDisplay),
+ 0,
+ UInt32(LMGetKbdType()),
+ OptionBits(kUCKeyTranslateNoDeadKeysBit),
+ &deadKeyState,
+ maxLength,
+ &length,
+ &characters
+ )
+
+ guard error == noErr else {
+ return nil
+ }
+ return String(utf16CodeUnits: characters, count: length)
+ }
+}
+
+extension KeyboardShortcuts.Shortcut {
+ var shortcutGlyph: String {
+ modifiers.shortcutGlyph + (self.keyToCharacter()?.uppercased() ?? "?")
+ }
+}
+
+private extension NSEvent {
+ var isKeyEvent: Bool {
+ type == .keyDown || type == .keyUp
+ }
+
+ var modifiers: ModifierFlags {
+ modifierFlags
+ .intersection(.deviceIndependentFlagsMask)
+ .subtracting([.capsLock, .numericPad, .function])
+ }
+}
+
+private extension NSEvent.SpecialKey {
+ var isSupportedFunctionKey: Bool {
+ switch self {
+ case .f1, .f2, .f3, .f4, .f5, .f6, .f7, .f8, .f9, .f10, .f11, .f12, .f13, .f14, .f15, .f16, .f17, .f18, .f19, .f20:
+ true
+ default:
+ false
+ }
+ }
+}
+
+extension NSEvent.ModifierFlags {
+ var carbon: Int {
+ var flags = 0
+ if contains(.control) {
+ flags |= controlKey
+ }
+ if contains(.option) {
+ flags |= optionKey
+ }
+ if contains(.shift) {
+ flags |= shiftKey
+ }
+ if contains(.command) {
+ flags |= cmdKey
+ }
+ return flags
+ }
+
+ init(carbon: Int) {
+ self.init()
+ if carbon & controlKey == controlKey {
+ insert(.control)
+ }
+ if carbon & optionKey == optionKey {
+ insert(.option)
+ }
+ if carbon & shiftKey == shiftKey {
+ insert(.shift)
+ }
+ if carbon & cmdKey == cmdKey {
+ insert(.command)
+ }
+ }
+
+ public var shortcutGlyph: String {
+ var shortcutGlyph = ""
+ if contains(.control) {
+ shortcutGlyph += "⌃"
+ }
+ if contains(.option) {
+ shortcutGlyph += "⌥"
+ }
+ if contains(.shift) {
+ shortcutGlyph += "⇧"
+ }
+ if contains(.command) {
+ shortcutGlyph += "⌘"
+ }
+ return shortcutGlyph
+ }
+}
+
+private extension NSAlert {
+ convenience init(title: String, message: String? = nil) {
+ self.init()
+ messageText = title
+ alertStyle = .warning
+ if let message {
+ informativeText = message
+ }
+ }
+
+ @discardableResult
+ static func showModal(for window: NSWindow? = nil, title: String, message: String? = nil) -> NSApplication.ModalResponse {
+ NSAlert(title: title, message: message).runModal(for: window)
+ }
+
+ @discardableResult
+ func runModal(for window: NSWindow? = nil) -> NSApplication.ModalResponse {
+ guard let window else {
+ return self.runModal()
+ }
+ beginSheetModal(for: window) { code in
+ NSApp.stopModal(withCode: code)
+ }
+ return NSApp.runModal(for: window)
+ }
+}
+
+private extension Notification.Name {
+ static let shortcutByNameDidChange = Self("KeyboardShortcuts_shortcutByNameDidChange")
+}
diff --git a/MonitorControl/Support/KeyboardShortcutsManager.swift b/MonitorControl/Support/KeyboardShortcutsManager.swift
index c38b7e1b9..7e860f879 100644
--- a/MonitorControl/Support/KeyboardShortcutsManager.swift
+++ b/MonitorControl/Support/KeyboardShortcutsManager.swift
@@ -1,7 +1,6 @@
// Copyright © MonitorControl. @JoniVR, @theOneyouseek, @waydabber and others
import Foundation
-import KeyboardShortcuts
import os.log
class KeyboardShortcutsManager {
@@ -71,9 +70,6 @@ class KeyboardShortcutsManager {
KeyboardShortcuts.enable(shortcut)
}
for shortcut in disabledShortcuts {
- guard let assignedShortcut = KeyboardShortcuts.getShortcut(for: shortcut), !enabledShortcuts.contains(where: { KeyboardShortcuts.getShortcut(for: $0) == assignedShortcut }) else {
- continue
- }
KeyboardShortcuts.disable(shortcut)
}
}
diff --git a/MonitorControl/View Controllers/Preferences/KeyboardPrefsViewController.swift b/MonitorControl/View Controllers/Preferences/KeyboardPrefsViewController.swift
index 8bccf3a6c..1272ba67e 100644
--- a/MonitorControl/View Controllers/Preferences/KeyboardPrefsViewController.swift
+++ b/MonitorControl/View Controllers/Preferences/KeyboardPrefsViewController.swift
@@ -1,7 +1,6 @@
// Copyright © MonitorControl. @JoniVR, @theOneyouseek, @waydabber and others
import Cocoa
-import KeyboardShortcuts
import ServiceManagement
import Settings
@@ -133,13 +132,24 @@ class KeyboardPrefsViewController: NSViewController, SettingsPane {
customVolumeDownRecorder.placeholderString = customBrightnessDownRecorder.placeholderString
customMuteRecorder.placeholderString = NSLocalizedString("Mute", comment: "Shown in record shortcut box")
- self.customBrightnessUp.addSubview(customBrightnessUpRecorder)
- self.customBrightnessDown.addSubview(customBrightnessDownRecorder)
- self.customContrastUp.addSubview(customContrastUpRecorder)
- self.customContrastDown.addSubview(customContrastDownRecorder)
- self.customVolumeUp.addSubview(customVolumeUpRecorder)
- self.customVolumeDown.addSubview(customVolumeDownRecorder)
- self.customMute.addSubview(customMuteRecorder)
+ for (container, recorder) in [
+ (self.customBrightnessUp, customBrightnessUpRecorder),
+ (self.customBrightnessDown, customBrightnessDownRecorder),
+ (self.customContrastUp, customContrastUpRecorder),
+ (self.customContrastDown, customContrastDownRecorder),
+ (self.customVolumeUp, customVolumeUpRecorder),
+ (self.customVolumeDown, customVolumeDownRecorder),
+ (self.customMute, customMuteRecorder),
+ ] {
+ guard let container else { continue }
+ container.addSubview(recorder)
+ NSLayoutConstraint.activate([
+ recorder.leadingAnchor.constraint(equalTo: container.leadingAnchor),
+ recorder.trailingAnchor.constraint(equalTo: container.trailingAnchor),
+ recorder.topAnchor.constraint(equalTo: container.topAnchor),
+ recorder.bottomAnchor.constraint(equalTo: container.bottomAnchor),
+ ])
+ }
self.populateSettings()
}
diff --git a/MonitorControlHelper/Info.plist b/MonitorControlHelper/Info.plist
index 402112f6e..ac5730d60 100644
--- a/MonitorControlHelper/Info.plist
+++ b/MonitorControlHelper/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
- 7149
+ 7150
LSApplicationCategoryType
public.app-category.utilities
LSBackgroundOnly
From cea3b828ee549cdc12da4ccd3b12698320c018d9 Mon Sep 17 00:00:00 2001
From: waydabber <37590873+waydabber@users.noreply.github.com>
Date: Tue, 15 Sep 2026 10:48:29 +0200
Subject: [PATCH 7/8] Further fixing KeyboardShortcuts
---
MonitorControl/Info.plist | 2 +-
.../Support/KeyboardShortcuts.swift | 210 ++++++++++++------
MonitorControlHelper/Info.plist | 2 +-
3 files changed, 141 insertions(+), 73 deletions(-)
diff --git a/MonitorControl/Info.plist b/MonitorControl/Info.plist
index 1a65eba16..30424bb07 100644
--- a/MonitorControl/Info.plist
+++ b/MonitorControl/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
- 7150
+ 7151
LSApplicationCategoryType
public.app-category.utilities
LSMinimumSystemVersion
diff --git a/MonitorControl/Support/KeyboardShortcuts.swift b/MonitorControl/Support/KeyboardShortcuts.swift
index 2f0b998bc..3bf6b106d 100644
--- a/MonitorControl/Support/KeyboardShortcuts.swift
+++ b/MonitorControl/Support/KeyboardShortcuts.swift
@@ -124,7 +124,21 @@ enum KeyboardShortcuts {
private static var keyDownHandlers = [Name: [() -> Void]]()
private static var keyUpHandlers = [Name: [() -> Void]]()
private static var disabledNames = Set()
- static var isPaused = false
+ static var isPaused = false {
+ didSet {
+ guard self.isPaused != oldValue else { return }
+ if self.isPaused {
+ // Let the recorder receive combinations that are already registered as hotkeys.
+ for shortcut in self.registeredShortcuts {
+ self.unregister(shortcut)
+ }
+ } else {
+ for shortcut in self.shortcutsForHandlers {
+ self.register(shortcut)
+ }
+ }
+ }
+ }
static func onKeyDown(for name: Name, action: @escaping () -> Void) {
self.keyDownHandlers[name, default: []].append(action)
@@ -226,7 +240,7 @@ enum KeyboardShortcuts {
}
private static func register(_ shortcut: Shortcut) {
- guard !self.registeredShortcuts.contains(shortcut) else {
+ guard !self.isPaused, !self.registeredShortcuts.contains(shortcut) else {
return
}
self.hotKeyId += 1
@@ -332,13 +346,14 @@ enum KeyboardShortcuts {
}
extension KeyboardShortcuts {
- final class RecorderCocoa: NSSearchField, NSSearchFieldDelegate {
+ final class RecorderCocoa: NSSearchField {
+ private weak static var activeRecorder: RecorderCocoa?
private let minimumWidth = 130.0
private var eventMonitor: LocalEventMonitor?
private var isRecording = false
private var idlePlaceholder: String?
private var observer: NSObjectProtocol?
- private var windowObserver: NSObjectProtocol?
+ private var recordingObservers = [NSObjectProtocol]()
private var cancelButton: NSButtonCell?
var shortcutName: KeyboardShortcuts.Name {
@@ -346,10 +361,8 @@ extension KeyboardShortcuts {
guard self.shortcutName != oldValue else {
return
}
+ self.endRecording()
self.setStringValue()
- DispatchQueue.main.async {
- self.window?.makeFirstResponder(nil)
- }
}
}
@@ -357,6 +370,28 @@ extension KeyboardShortcuts {
false
}
+ override func isAccessibilityElement() -> Bool {
+ true
+ }
+
+ override func accessibilityRole() -> NSAccessibility.Role? {
+ .button
+ }
+
+ override func accessibilityLabel() -> String? {
+ self.idlePlaceholder ?? placeholderString
+ }
+
+ override func accessibilityValue() -> String? {
+ KeyboardShortcuts.getShortcut(for: self.shortcutName)?.shortcutGlyph
+ }
+
+ override func accessibilityPerformPress() -> Bool {
+ guard isEnabled, window?.makeFirstResponder(self) == true else { return false }
+ self.startRecording()
+ return true
+ }
+
override var intrinsicContentSize: CGSize {
var size = super.intrinsicContentSize
size.width = self.minimumWidth
@@ -371,7 +406,8 @@ extension KeyboardShortcuts {
init(for name: KeyboardShortcuts.Name) {
self.shortcutName = name
super.init(frame: .zero)
- delegate = self
+ isEditable = false
+ isSelectable = false
placeholderString = NSLocalizedString("Record", comment: "Shortcut recorder placeholder")
alignment = .center
baseWritingDirection = .leftToRight // Keep modifier glyphs before digit and arrow keys in RTL.
@@ -406,107 +442,133 @@ extension KeyboardShortcuts {
if let observer {
NotificationCenter.default.removeObserver(observer)
}
- if let windowObserver {
- NotificationCenter.default.removeObserver(windowObserver)
+ for observer in recordingObservers {
+ NotificationCenter.default.removeObserver(observer)
}
}
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
- if let windowObserver = self.windowObserver {
- NotificationCenter.default.removeObserver(windowObserver)
- self.windowObserver = nil
- }
- self.stopRecording()
- guard let window else { return }
- self.windowObserver = NotificationCenter.default.addObserver(forName: NSWindow.didResignKeyNotification, object: window, queue: nil) { [weak self] _ in
- guard let self, self.isRecording else { return }
- self.stopRecording()
- self.window?.makeFirstResponder(nil)
- }
+ self.endRecording()
}
- func controlTextDidChange(_: Notification) {
- let recordedShortcutGlyph = KeyboardShortcuts.getShortcut(for: self.shortcutName)?.shortcutGlyph ?? ""
- if stringValue.isEmpty {
- self.saveShortcut(nil)
- } else if self.isRecording, stringValue != recordedShortcutGlyph {
- NSSound.beep()
- self.setStringValue()
- }
- self.showsCancelButton = !stringValue.isEmpty
- if stringValue.isEmpty {
- window?.makeFirstResponder(self)
- }
+ override func viewDidHide() {
+ super.viewDidHide()
+ self.endRecording()
+ }
+
+ /// Keep first-responder ownership on the recorder, not NSSearchField's shared text editor.
+ override var acceptsFirstResponder: Bool {
+ isEnabled
}
- func controlTextDidBeginEditing(_: Notification) {
+ override func becomeFirstResponder() -> Bool {
+ guard isEnabled, window != nil, !isHiddenOrHasHiddenAncestor else { return false }
self.startRecording()
+ return true
}
- func controlTextDidEndEditing(_: Notification) {
+ override func resignFirstResponder() -> Bool {
self.stopRecording()
+ return true
+ }
+
+ /// Newer AppKit search fields contain interactive subviews; keep their clicks here too.
+ override func hitTest(_ point: NSPoint) -> NSView? {
+ super.hitTest(point) == nil ? nil : self
}
override func mouseDown(with event: NSEvent) {
- super.mouseDown(with: event)
- self.startRecording()
+ guard isEnabled else { return }
+ let cancelBounds: NSRect
+ if #available(macOS 11.0, *) {
+ cancelBounds = cancelButtonBounds
+ } else {
+ cancelBounds = (cell as? NSSearchFieldCell)?.cancelButtonRect(forBounds: bounds) ?? .zero
+ }
+ if self.showsCancelButton, cancelBounds.contains(convert(event.locationInWindow, from: nil)) {
+ self.clear()
+ return
+ }
+ if window?.makeFirstResponder(self) == true {
+ self.startRecording()
+ }
+ }
+
+ private func endRecording() {
+ self.stopRecording()
+ if window?.firstResponder === self {
+ window?.makeFirstResponder(nil)
+ }
}
private func stopRecording() {
guard self.isRecording else { return }
self.eventMonitor = nil
+ for observer in self.recordingObservers {
+ NotificationCenter.default.removeObserver(observer)
+ }
+ self.recordingObservers.removeAll()
self.isRecording = false
placeholderString = self.idlePlaceholder
- self.showsCancelButton = !stringValue.isEmpty
- KeyboardShortcuts.isPaused = false
- }
-
- override func becomeFirstResponder() -> Bool {
- let shouldBecomeFirstResponder = super.becomeFirstResponder()
- guard shouldBecomeFirstResponder else {
- return shouldBecomeFirstResponder
+ self.setStringValue()
+ if Self.activeRecorder === self {
+ Self.activeRecorder = nil
+ KeyboardShortcuts.isPaused = false
}
-
- self.startRecording()
-
- return shouldBecomeFirstResponder
+ needsDisplay = true
}
private func startRecording() {
- guard window != nil, !self.isRecording else {
- return
- }
-
+ guard let window, !self.isRecording else { return }
+ Self.activeRecorder?.endRecording()
+ Self.activeRecorder = self
self.idlePlaceholder = placeholderString
self.isRecording = true
placeholderString = NSLocalizedString("Press Shortcut", comment: "Shortcut recorder prompt")
- self.showsCancelButton = !stringValue.isEmpty
- (currentEditor() as? NSTextView)?.insertionPointColor = .clear
+ stringValue = ""
+ self.showsCancelButton = KeyboardShortcuts.getShortcut(for: self.shortcutName) != nil
KeyboardShortcuts.isPaused = true
- self.eventMonitor = LocalEventMonitor(events: [.keyDown, .leftMouseUp, .rightMouseUp]) { [weak self] event in
- self?.handle(event)
+ self.eventMonitor = LocalEventMonitor(events: [.keyDown, .leftMouseDown, .rightMouseDown, .otherMouseDown]) { [weak self] event in
+ guard let self else { return event }
+ return self.handle(event)
}.start()
+ for (name, object) in [
+ (NSWindow.didResignKeyNotification, window as AnyObject),
+ (NSWindow.willCloseNotification, window as AnyObject),
+ (NSApplication.didResignActiveNotification, NSApp as AnyObject),
+ ] {
+ self.recordingObservers.append(NotificationCenter.default.addObserver(forName: name, object: object, queue: nil) { [weak self] _ in
+ self?.endRecording()
+ })
+ }
+ needsDisplay = true
}
private func handle(_ event: NSEvent) -> NSEvent? {
- let clickPoint = convert(event.locationInWindow, from: nil)
- if event.type == .leftMouseUp || event.type == .rightMouseUp, !bounds.insetBy(dx: -3, dy: -3).contains(clickPoint) {
- window?.makeFirstResponder(nil)
+ guard Self.activeRecorder === self, self.isRecording else { return event }
+ guard let window, window.firstResponder === self, !isHiddenOrHasHiddenAncestor else {
+ self.endRecording()
return event
}
-
- guard event.isKeyEvent else {
- return nil
+ if !event.isKeyEvent {
+ if event.window !== window || !bounds.contains(convert(event.locationInWindow, from: nil)) {
+ self.endRecording()
+ }
+ return event
+ }
+ guard event.window === window else {
+ self.endRecording()
+ return event
}
if event.modifiers.isEmpty, event.specialKey == .tab {
- window?.makeFirstResponder(nil)
+ self.endRecording()
return event
}
if event.modifiers.isEmpty, event.keyCode == kVK_Escape {
- window?.makeFirstResponder(nil)
+ self.endRecording()
return nil
}
@@ -523,23 +585,27 @@ extension KeyboardShortcuts {
let shortcut = KeyboardShortcuts.Shortcut(event: event)
if let menuItem = shortcut.takenByMainMenu {
- window?.makeFirstResponder(nil)
+ self.endRecording()
NSAlert.showModal(for: window, title: String(format: NSLocalizedString("This keyboard shortcut is already used by the menu item \"%@\".", comment: "Shortcut conflict; menu item title"), menuItem.title))
- window?.makeFirstResponder(self)
+ if window.isKeyWindow {
+ window.makeFirstResponder(self)
+ }
return nil
}
guard !shortcut.isTakenBySystem else {
- window?.makeFirstResponder(nil)
+ self.endRecording()
NSAlert.showModal(for: window, title: NSLocalizedString("This keyboard shortcut is used by the system.", comment: "Shortcut conflict"), message: NSLocalizedString("Keyboard shortcuts can be changed in system keyboard settings.", comment: "Shortcut conflict guidance"))
- window?.makeFirstResponder(self)
+ if window.isKeyWindow {
+ window.makeFirstResponder(self)
+ }
return nil
}
stringValue = shortcut.shortcutGlyph
self.showsCancelButton = true
self.saveShortcut(shortcut)
- window?.makeFirstResponder(nil)
+ self.endRecording()
return nil
}
@@ -553,7 +619,9 @@ extension KeyboardShortcuts {
}
private func clear() {
- (cell as? NSSearchFieldCell)?.cancelButtonCell?.performClick(self)
+ self.saveShortcut(nil)
+ self.setStringValue()
+ self.endRecording()
}
}
}
diff --git a/MonitorControlHelper/Info.plist b/MonitorControlHelper/Info.plist
index ac5730d60..675f61b78 100644
--- a/MonitorControlHelper/Info.plist
+++ b/MonitorControlHelper/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
- 7150
+ 7151
LSApplicationCategoryType
public.app-category.utilities
LSBackgroundOnly
From 0811f715a84deb6e08e70de3f79c9c23b754e745 Mon Sep 17 00:00:00 2001
From: waydabber <37590873+waydabber@users.noreply.github.com>
Date: Tue, 15 Sep 2026 11:00:46 +0200
Subject: [PATCH 8/8] Lowered target to 10.14
---
MonitorControl.xcodeproj/project.pbxproj | 12 ++++++------
MonitorControl/Info.plist | 2 +-
MonitorControlHelper/Info.plist | 2 +-
3 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/MonitorControl.xcodeproj/project.pbxproj b/MonitorControl.xcodeproj/project.pbxproj
index f565cbde1..c6204059a 100644
--- a/MonitorControl.xcodeproj/project.pbxproj
+++ b/MonitorControl.xcodeproj/project.pbxproj
@@ -7,7 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
- CB0D15010000000000000001 /* KeyboardShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB0D15010000000000000002 /* KeyboardShortcuts.swift */; };
28D1DDF2227FBE71004CB494 /* NSScreen+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28D1DDF1227FBE71004CB494 /* NSScreen+Extension.swift */; };
28D1DDF3227FC8C6004CB494 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 56754EB01D9A4016007BCDC5 /* Assets.xcassets */; };
56754EAF1D9A4016007BCDC5 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56754EAE1D9A4016007BCDC5 /* main.swift */; };
@@ -50,6 +49,7 @@
AACE5E2327050C63006C2A48 /* NSNotification+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AACE5E2227050C63006C2A48 /* NSNotification+Extension.swift */; };
AAD7DD342CAFF3D90062822F /* Settings in Frameworks */ = {isa = PBXBuildFile; productRef = AAD7DD332CAFF3D90062822F /* Settings */; };
AADB625A26BC196900DFFAA5 /* DisplayServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AADB625926BC196900DFFAA5 /* DisplayServices.framework */; };
+ CB0D15010000000000000001 /* KeyboardShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB0D15010000000000000002 /* KeyboardShortcuts.swift */; };
F01B0699228221B7008E64DB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F01B0680228221B6008E64DB /* Localizable.strings */; };
F01B069F228221B7008E64DB /* SliderHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F01B068F228221B7008E64DB /* SliderHandler.swift */; };
F03A8DF21FFBAA6F0034DC27 /* OtherDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03A8DF11FFBAA6F0034DC27 /* OtherDisplay.swift */; };
@@ -75,7 +75,6 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
- CB0D15010000000000000002 /* KeyboardShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardShortcuts.swift; sourceTree = ""; };
01D6471D2AF916A800321FFB /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Main.strings; sourceTree = ""; };
01D6471E2AF916A800321FFB /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/InternetAccessPolicy.strings; sourceTree = ""; };
01D6471F2AF916A800321FFB /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Localizable.strings; sourceTree = ""; };
@@ -167,6 +166,7 @@
B7FA437E2AC5857A00A94C01 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Main.strings; sourceTree = ""; };
B7FA437F2AC5857A00A94C01 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InternetAccessPolicy.strings; sourceTree = ""; };
B7FA43802AC5857A00A94C01 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; };
+ CB0D15010000000000000002 /* KeyboardShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardShortcuts.swift; sourceTree = ""; };
F01B0682228221B6008E64DB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; };
F01B0685228221B6008E64DB /* Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Bridging-Header.h"; sourceTree = ""; };
F01B0686228221B6008E64DB /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
@@ -880,7 +880,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
- MACOSX_DEPLOYMENT_TARGET = 12.0;
+ MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 4.4.0;
PRODUCT_BUNDLE_IDENTIFIER = app.monitorcontrol.MonitorControl;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -917,7 +917,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
- MACOSX_DEPLOYMENT_TARGET = 12.0;
+ MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 4.4.0;
PRODUCT_BUNDLE_IDENTIFIER = app.monitorcontrol.MonitorControl;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -954,7 +954,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
- MACOSX_DEPLOYMENT_TARGET = 12.0;
+ MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 4.1.0;
PRODUCT_BUNDLE_IDENTIFIER = app.monitorcontrol.MonitorControlHelper;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -985,7 +985,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
- MACOSX_DEPLOYMENT_TARGET = 12.0;
+ MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 4.1.0;
PRODUCT_BUNDLE_IDENTIFIER = app.monitorcontrol.MonitorControlHelper;
PRODUCT_NAME = "$(TARGET_NAME)";
diff --git a/MonitorControl/Info.plist b/MonitorControl/Info.plist
index 30424bb07..919be6ec2 100644
--- a/MonitorControl/Info.plist
+++ b/MonitorControl/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
- 7151
+ 7152
LSApplicationCategoryType
public.app-category.utilities
LSMinimumSystemVersion
diff --git a/MonitorControlHelper/Info.plist b/MonitorControlHelper/Info.plist
index 675f61b78..7ce923ff2 100644
--- a/MonitorControlHelper/Info.plist
+++ b/MonitorControlHelper/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
- 7151
+ 7152
LSApplicationCategoryType
public.app-category.utilities
LSBackgroundOnly