diff --git a/.github/screenshot.png b/.github/screenshot.png index 671762b..700cefd 100644 Binary files a/.github/screenshot.png and b/.github/screenshot.png differ diff --git a/MonitorControl/Enums/PrefKey.swift b/MonitorControl/Enums/PrefKey.swift index ec67f81..3d81e74 100644 --- a/MonitorControl/Enums/PrefKey.swift +++ b/MonitorControl/Enums/PrefKey.swift @@ -1,8 +1,7 @@ // Copyright © MonitorControl. @JoniVR, @theOneyouseek, @waydabber and others enum PrefKey: String { - // Enable mute DDC for display - case enableMuteUnmute + /* -- App-wide settings -- */ // Sparkle automatic checks case SUEnableAutomaticChecks @@ -10,57 +9,6 @@ enum PrefKey: String { // Receive beta updates? case isBetaChannel // This is not added to Preferences yet as it will be needed in the future only. - // Hide OSD for display - case hideOsd - - // Longer delay DDC for display - case longerDelay - - // DDC polling mode for display - case pollingMode - - // 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) - case avoidGamma - - // Command value display - case value - - // Min command value display - case minDDCOverride - - // Max command value display - case maxDDC - - // Max user override command value display - case maxDDCOverride - - // Max command value display - case curveDDC - - // Is the specific control is set as unavailable for display? - case unavailableDDC - - // Invert DDC scale? - case invertDDC - - // Override DDC control command code - case remapDDC - - // User assigned audio device name for display - case audioDeviceNameOverride - - // Display disabled for keyboard control - case isDisabled - - // Force software mode for display - case forceSw - - // Software brightness for display - case SwBrightness - // Build number case buildNumber @@ -94,9 +42,6 @@ enum PrefKey: String { // Lower via software after brightness case disableCombinedBrightness - // Lower via software after brightness - case combinedBrightnessSwitchingPoint - // Use separated OSD scale for combined brightness case separateCombinedScale @@ -115,9 +60,6 @@ enum PrefKey: String { // Show tick marks for sliders case showTickMarks - // Friendly name changed - case friendlyName - // Instead of assuming default values, enable read or write upon startup (according to readDDCInsteadOfRestoreValues) case enableDDCDuringStartup @@ -159,6 +101,73 @@ enum PrefKey: String { // Combine sliders for all displays case slidersCombine + + /* -- Display specific settings */ + + // Enable mute DDC for display + case enableMuteUnmute + + // Hide OSD for display + case hideOsd + + // Longer delay DDC for display + case longerDelay + + // DDC polling mode for display + case pollingMode + + // 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) + case avoidGamma + + // User assigned audio device name for display + case audioDeviceNameOverride + + // Display disabled for keyboard control + case isDisabled + + // Force software mode for display + case forceSw + + // Software brightness for display + case SwBrightness + + // Combined brightness switching point + case combinedBrightnessSwitchingPoint + + // Friendly name + case friendlyName + + /* -- Display+Command specific settings -- */ + + // Command value display + case value + + // Was the setting ever changed by the user? + case isTouched + + // Min command value display + case minDDCOverride + + // Max command value display + case maxDDC + + // Max user override command value display + case maxDDCOverride + + // Max command value display + case curveDDC + + // Is the specific control is set as unavailable for display? + case unavailableDDC + + // Invert DDC scale? + case invertDDC + + // Override DDC control command code + case remapDDC } enum PollingMode: Int { diff --git a/MonitorControl/Info.plist b/MonitorControl/Info.plist index 637c02d..c158f20 100644 --- a/MonitorControl/Info.plist +++ b/MonitorControl/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 6421 + 6440 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion diff --git a/MonitorControl/Model/OtherDisplay.swift b/MonitorControl/Model/OtherDisplay.swift index 1222c30..5120e21 100644 --- a/MonitorControl/Model/OtherDisplay.swift +++ b/MonitorControl/Model/OtherDisplay.swift @@ -1,6 +1,5 @@ // Copyright © MonitorControl. @JoniVR, @theOneyouseek, @waydabber and others -import AVFoundation import Cocoa import IOKit import os.log @@ -13,7 +12,6 @@ class OtherDisplay: Display { var arm64avService: IOAVService? var isDiscouraged: Bool = false let DDC_MAX_DETECT_LIMIT: Int = 100 - private var audioPlayer: AVAudioPlayer? var pollingCount: Int { get { switch self.readPrefAsInt(key: .pollingMode) { @@ -114,7 +112,8 @@ class OtherDisplay: Display { os_log("- Minimum DDC value: %{public}@ (overrides 0)", type: .info, String(self.readPrefAsInt(key: .minDDCOverride, for: command))) os_log("- Maximum DDC value: %{public}@ (overrides %{public}@)", type: .info, String(self.readPrefAsInt(key: .maxDDC, for: command)), String(maxDDCValue)) os_log("- Current internal value: %{public}@", type: .info, String(self.readPrefAsFloat(for: command))) - if prefs.bool(forKey: PrefKey.enableDDCDuringStartup.rawValue), !prefs.bool(forKey: PrefKey.readDDCInsteadOfRestoreValues.rawValue), !app.safeMode { + os_log("- Command status: %{public}@", type: .info, self.readPrefAsBool(key: .isTouched, for: command) ? "Touched" : "Untouched") + if self.readPrefAsBool(key: .isTouched, for: command), prefs.bool(forKey: PrefKey.enableDDCDuringStartup.rawValue), !prefs.bool(forKey: PrefKey.readDDCInsteadOfRestoreValues.rawValue), !app.safeMode { os_log("- Writing last saved DDC values.", type: .info, self.name, String(reflecting: command)) _ = self.writeDDCValues(command: command, value: currentDDCValue) } @@ -390,6 +389,7 @@ class OtherDisplay: Display { } else { success = self.ddc?.write(command: command.rawValue, value: value, errorRecoveryWaitTime: 2000) ?? false } + self.savePref(true, key: PrefKey.isTouched, for: command) // We deliberatly consider the value tuched no matter if the call succeeded } } return success @@ -486,19 +486,6 @@ class OtherDisplay: Display { return max(min(value, 1), 0) } - func playVolumeChangedSound() { - guard let preferences = app.getSystemPreferences(), let hasSoundEnabled = preferences["com.apple.sound.beep.feedback"] as? Int, hasSoundEnabled == 1 else { - return - } - do { - self.audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: "/System/Library/LoginPlugins/BezelServices.loginPlugin/Contents/Resources/volume.aiff")) - self.audioPlayer?.volume = 1 - self.audioPlayer?.play() - } catch { - os_log("%{public}@", type: .error, error.localizedDescription) - } - } - func combinedBrightnessSwitchingValue() -> Float { return Float(self.readPrefAsInt(key: .combinedBrightnessSwitchingPoint) + 8) / 16 } diff --git a/MonitorControl/Support/AppDelegate.swift b/MonitorControl/Support/AppDelegate.swift index 92c075b..fcb8665 100644 --- a/MonitorControl/Support/AppDelegate.swift +++ b/MonitorControl/Support/AppDelegate.swift @@ -1,5 +1,6 @@ // Copyright © MonitorControl. @JoniVR, @theOneyouseek, @waydabber and others +import AVFoundation import Cocoa import Foundation import MediaKeyTap @@ -19,6 +20,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { var sleepID: Int = 0 // sleep event ID var safeMode = false var jobRunning = false + var audioPlayer: AVAudioPlayer? let updaterController = SPUStandardUpdaterController(startingUpdater: false, updaterDelegate: UpdaterDelegate(), userDriverDelegate: nil) var preferencePaneStyle: Preferences.Style { @@ -299,4 +301,17 @@ class AppDelegate: NSObject, NSApplicationDelegate { return true } } + + func playVolumeChangedSound() { + guard let preferences = app.getSystemPreferences(), let hasSoundEnabled = preferences["com.apple.sound.beep.feedback"] as? Int, hasSoundEnabled == 1 else { + return + } + do { + self.audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: "/System/Library/LoginPlugins/BezelServices.loginPlugin/Contents/Resources/volume.aiff")) + self.audioPlayer?.volume = 1 + self.audioPlayer?.play() + } catch { + os_log("%{public}@", type: .error, error.localizedDescription) + } + } } diff --git a/MonitorControl/Support/KeyboardShortcutsManager.swift b/MonitorControl/Support/KeyboardShortcutsManager.swift index 1c10a67..8de4ac8 100644 --- a/MonitorControl/Support/KeyboardShortcutsManager.swift +++ b/MonitorControl/Support/KeyboardShortcutsManager.swift @@ -5,8 +5,9 @@ import KeyboardShortcuts import os.log class KeyboardShortcutsManager { - var initialKeyRepeat = 0.24 // This should come from UserDefaults instead, but it's ok for now. - var keyRepeat = 0.032 // This should come from UserDefaults instead, but it's ok for now. + var initialKeyRepeat = 0.21 + var keyRepeat = 0.028 + var keyRepeatCount = 0 var currentCommand = KeyboardShortcuts.Name.none var isFirstKeypress = false @@ -62,6 +63,7 @@ class KeyboardShortcutsManager { self.isFirstKeypress = true self.isHold = true self.currentEventId += 1 + self.keyRepeatCount = 0 self.apply(shortcut, eventId: self.currentEventId) } @@ -69,10 +71,11 @@ class KeyboardShortcutsManager { self.isHold = false self.isFirstKeypress = false self.currentCommand = KeyboardShortcuts.Name.none + self.keyRepeatCount = 0 } func apply(_ shortcut: KeyboardShortcuts.Name, eventId: Int) { - guard app.sleepID == 0, app.reconfigureID == 0 else { + guard app.sleepID == 0, app.reconfigureID == 0, self.keyRepeatCount <= 100 else { self.disengage() return } @@ -92,6 +95,7 @@ class KeyboardShortcutsManager { self.apply(shortcut, eventId: eventId) } } + self.keyRepeatCount += 1 switch shortcut { case KeyboardShortcuts.Name.brightnessUp: self.brightness(isUp: true) case KeyboardShortcuts.Name.brightnessDown: self.brightness(isUp: false) @@ -142,7 +146,7 @@ class KeyboardShortcutsManager { if isPressed { display.stepVolume(isUp: isUp, isSmallIncrement: prefs.bool(forKey: PrefKey.useFineScaleVolume.rawValue)) } else if !wasNotIsPressedVolumeSentAlready, !display.readPrefAsBool(key: .unavailableDDC, for: .audioSpeakerVolume) { - display.playVolumeChangedSound() + app.playVolumeChangedSound() wasNotIsPressedVolumeSentAlready = true } } @@ -158,7 +162,7 @@ class KeyboardShortcutsManager { if let display = display as? OtherDisplay { display.toggleMute() if !wasNotIsPressedVolumeSentAlready, display.readPrefAsInt(for: .audioMuteScreenBlank) != 1, !display.readPrefAsBool(key: .unavailableDDC, for: .audioSpeakerVolume) { - display.playVolumeChangedSound() + app.playVolumeChangedSound() wasNotIsPressedVolumeSentAlready = true } } diff --git a/MonitorControl/Support/MediaKeyTapManager.swift b/MonitorControl/Support/MediaKeyTapManager.swift index 2a5f223..f6a446a 100644 --- a/MonitorControl/Support/MediaKeyTapManager.swift +++ b/MonitorControl/Support/MediaKeyTapManager.swift @@ -94,7 +94,7 @@ class MediaKeyTapManager: MediaKeyTapDelegate { if !isRepeat, isPressed, let display = display as? OtherDisplay { display.toggleMute() if !wasNotIsPressedVolumeSentAlready, display.readPrefAsInt(for: .audioMuteScreenBlank) != 1, !display.readPrefAsBool(key: .unavailableDDC, for: .audioSpeakerVolume) { - display.playVolumeChangedSound() + app.playVolumeChangedSound() wasNotIsPressedVolumeSentAlready = true } } @@ -104,7 +104,7 @@ class MediaKeyTapManager: MediaKeyTapDelegate { if isPressed { display.stepVolume(isUp: mediaKey == .volumeUp, isSmallIncrement: isSmallIncrement) } else if !wasNotIsPressedVolumeSentAlready, !display.readPrefAsBool(key: .unavailableDDC, for: .audioSpeakerVolume) { - display.playVolumeChangedSound() + app.playVolumeChangedSound() wasNotIsPressedVolumeSentAlready = true } } @@ -163,13 +163,13 @@ class MediaKeyTapManager: MediaKeyTapDelegate { if [KeyboardVolume.media.rawValue, KeyboardVolume.both.rawValue].contains(prefs.integer(forKey: PrefKey.keyboardVolume.rawValue)) { keys.append(contentsOf: [.mute, .volumeUp, .volumeDown]) } - // Remove keys if no external displays are connected + // Remove brightness keys if no external displays are connected, but only if brightness fine control is not active var isInternalDisplayOnly = true for display in DisplayManager.shared.getAllDisplays() where !display.isBuiltIn() { isInternalDisplayOnly = false } - if isInternalDisplayOnly { - let keysToDelete: [MediaKey] = [.volumeUp, .volumeDown, .mute, .brightnessUp, .brightnessDown] + if isInternalDisplayOnly, !prefs.bool(forKey: PrefKey.useFineScaleBrightness.rawValue) { + let keysToDelete: [MediaKey] = [.brightnessUp, .brightnessDown] keys.removeAll { keysToDelete.contains($0) } } // Remove volume related keys if audio device is controllable diff --git a/MonitorControl/Support/MenuHandler.swift b/MonitorControl/Support/MenuHandler.swift index 61b10f2..7a53f09 100644 --- a/MonitorControl/Support/MenuHandler.swift +++ b/MonitorControl/Support/MenuHandler.swift @@ -21,6 +21,7 @@ class MenuHandler: NSMenu, NSMenuDelegate { func menuWillOpen(_: NSMenu) { self.updateMenuRelevantDisplay() + app.keyboardShortcuts.disengage() } func updateMenus(dontClose: Bool = false) { diff --git a/MonitorControlHelper/Info.plist b/MonitorControlHelper/Info.plist index 89a3013..c1793f6 100644 --- a/MonitorControlHelper/Info.plist +++ b/MonitorControlHelper/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 6421 + 6440 LSApplicationCategoryType public.app-category.utilities LSBackgroundOnly