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 @@ App icon
-

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!

Download for macOS @@ -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