Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Add edge scroll controls for brightness and volume
  • Loading branch information
华凯云 华凯云
华凯云 authored and 华凯云 committed May 9, 2026
commit 3fbdc6353d0463950b27dcd9b784ba240a6708eb
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ Carthage
.DS_Store

### Xcode ###
xcuserdata/
xcuserdata/build/
18 changes: 14 additions & 4 deletions MonitorControl.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
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 */; };
E1D2C3B52F10000100000001 /* EdgeScrollManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D2C3B42F10000100000001 /* EdgeScrollManager.swift */; };
E1D2C3B72F10000100000001 /* MousePrefsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D2C3B62F10000100000001 /* MousePrefsViewController.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 */; };
Expand Down Expand Up @@ -166,6 +168,8 @@
B7FA437E2AC5857A00A94C01 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Main.strings; sourceTree = "<group>"; };
B7FA437F2AC5857A00A94C01 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InternetAccessPolicy.strings; sourceTree = "<group>"; };
B7FA43802AC5857A00A94C01 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
E1D2C3B42F10000100000001 /* EdgeScrollManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeScrollManager.swift; sourceTree = "<group>"; };
E1D2C3B62F10000100000001 /* MousePrefsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MousePrefsViewController.swift; sourceTree = "<group>"; };
F01B0682228221B6008E64DB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
F01B0685228221B6008E64DB /* Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Bridging-Header.h"; sourceTree = "<group>"; };
F01B0686228221B6008E64DB /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
Expand Down Expand Up @@ -314,6 +318,7 @@
6C85EFD922C941B000227EA1 /* DisplayManager.swift */,
AA25F6D626E68C160087F3A2 /* MediaKeyTapManager.swift */,
AA44E70627038F7F00E06865 /* KeyboardShortcutsManager.swift */,
E1D2C3B42F10000100000001 /* EdgeScrollManager.swift */,
AA16139A26BE772E00DCF027 /* Arm64DDC.swift */,
AA4398A826DD55DA00943F16 /* IntelDDC.swift */,
FE4E0895249D584C003A50BB /* OSDUtils.swift */,
Expand Down Expand Up @@ -356,6 +361,7 @@
F0445D3720023E710025AE82 /* MainPrefsViewController.swift */,
AA25F6CE26E680510087F3A2 /* MenuslidersPrefsViewController.swift */,
AA25F6D026E681D30087F3A2 /* KeyboardPrefsViewController.swift */,
E1D2C3B62F10000100000001 /* MousePrefsViewController.swift */,
AA062E8926C9A039007E628C /* DisplaysPrefsViewController.swift */,
AA062E8D26CA7BE5007E628C /* DisplaysPrefsCellView.swift */,
AA665A5C26C5892800FEF2C1 /* AboutPrefsViewController.swift */,
Expand Down Expand Up @@ -628,6 +634,7 @@
6CBFE27C23DB27A200D1BC41 /* AppleDisplay.swift in Sources */,
AA062E8E26CA7BE5007E628C /* DisplaysPrefsCellView.swift in Sources */,
AA25F6D726E68C160087F3A2 /* MediaKeyTapManager.swift in Sources */,
E1D2C3B52F10000100000001 /* EdgeScrollManager.swift in Sources */,
FE4E0896249D584C003A50BB /* OSDUtils.swift in Sources */,
6CBFE27A23DB266000D1BC41 /* Display.swift in Sources */,
AA44E70727038F7F00E06865 /* KeyboardShortcutsManager.swift in Sources */,
Expand All @@ -640,6 +647,7 @@
28D1DDF2227FBE71004CB494 /* NSScreen+Extension.swift in Sources */,
AA99521726FE25AB00612E07 /* AppDelegate.swift in Sources */,
AA25F6D126E681D30087F3A2 /* KeyboardPrefsViewController.swift in Sources */,
E1D2C3B72F10000100000001 /* MousePrefsViewController.swift in Sources */,
F01B069F228221B7008E64DB /* SliderHandler.swift in Sources */,
AACE5E2327050C63006C2A48 /* NSNotification+Extension.swift in Sources */,
AA25F6CF26E680510087F3A2 /* MenuslidersPrefsViewController.swift in Sources */,
Expand Down Expand Up @@ -862,14 +870,15 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_ENTITLEMENTS = MonitorControl/MonitorControlDebug.entitlements;
CODE_SIGN_IDENTITY = "-";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 7100;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 299YSU96J7;
DEVELOPMENT_TEAM = HNDG94D2RC;
ENABLE_HARDENED_RUNTIME = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(PROJECT_DIR)/**",
Expand Down Expand Up @@ -900,13 +909,14 @@
buildSettings = {
ARCHS = "$(ARCHS_STANDARD)";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_IDENTITY = "-";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 7100;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 299YSU96J7;
DEVELOPMENT_TEAM = HNDG94D2RC;
ENABLE_HARDENED_RUNTIME = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(PROJECT_DIR)/**",
Expand Down Expand Up @@ -946,7 +956,7 @@
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 7100;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 299YSU96J7;
DEVELOPMENT_TEAM = HNDG94D2RC;
ENABLE_HARDENED_RUNTIME = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = MonitorControlHelper/Info.plist;
Expand Down Expand Up @@ -977,7 +987,7 @@
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 7100;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 299YSU96J7;
DEVELOPMENT_TEAM = HNDG94D2RC;
ENABLE_HARDENED_RUNTIME = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = MonitorControlHelper/Info.plist;
Expand Down
34 changes: 34 additions & 0 deletions MonitorControl/Enums/PrefKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ enum PrefKey: String {
// Sliders for multiple displays
case multiSliders

// Mouse wheel action on the left screen edge
case edgeScrollLeftAction

// Mouse wheel action on the right screen edge
case edgeScrollRightAction

// Mouse wheel edge control precision
case edgeScrollPrecision

// Play feedback sound when controlling volume from the screen edge
case edgeScrollVolumeSoundFeedback

/* -- Display specific settings */

// Enable mute DDC for display
Expand Down Expand Up @@ -213,3 +225,25 @@ enum KeyboardVolume: Int {
case both = 2
case disabled = 3
}

enum EdgeScrollAction: Int, CaseIterable {
case disabled = 0
case brightness = 1
case volume = 2
}

enum EdgeScrollPrecision: Int, CaseIterable {
case standard = 0
case fine = 1
case veryFine = 2
case coarse = 3

var step: Float {
switch self {
case .standard: return 0.02
case .fine: return 0.01
case .veryFine: return 0.005
case .coarse: return 0.05
}
}
}
1 change: 1 addition & 0 deletions MonitorControl/Extensions/Preferences+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ extension Settings.PaneIdentifier {
static let main = Self("Main")
static let menusliders = Self("Menu & Sliders")
static let keyboard = Self("Keyboard")
static let mouse = Self("Mouse")
static let displays = Self("Displays")
static let about = Self("About")
}
Expand Down
2 changes: 1 addition & 1 deletion MonitorControl/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>7141</string>
<string>7148</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSMinimumSystemVersion</key>
Expand Down
14 changes: 14 additions & 0 deletions MonitorControl/Model/Display.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,20 @@ class Display: Equatable {
}
}

func adjustBrightness(by delta: Float) {
guard !self.readPrefAsBool(key: .unavailableDDC, for: .brightness) else {
return
}
let value = max(0, min(1, self.getBrightness() + delta))
if self.setBrightness(value) {
OSDUtils.showOsdProgress(displayID: self.identifier, command: .brightness, value: value)
if let slider = self.sliderHandler[.brightness] {
slider.setValue(value, displayID: self.identifier)
self.brightnessSyncSourceValue = value
}
}
}

func setBrightness(_ to: Float = -1, slow: Bool = false) -> Bool {
if !prefs.bool(forKey: PrefKey.disableSmoothBrightness.rawValue) {
return self.setSmoothBrightness(to, slow: slow)
Expand Down
30 changes: 26 additions & 4 deletions MonitorControl/Model/OtherDisplay.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,14 +165,32 @@ class OtherDisplay: Display {
(command == .audioSpeakerVolume && self.readPrefAsBool(key: .enableMuteUnmute) && self.readPrefAsInt(for: .audioMuteScreenBlank) == 1) ? 0 : self.readPrefAsFloat(for: command)
}

func canAdjustVolume() -> Bool {
!self.isSw() && !self.readPrefAsBool(key: .unavailableDDC, for: .audioSpeakerVolume)
}

func stepVolume(isUp: Bool, isSmallIncrement: Bool) {
guard !self.readPrefAsBool(key: .unavailableDDC, for: .audioSpeakerVolume) else {
guard self.canAdjustVolume() else {
OSDUtils.showOsdVolumeDisabled(displayID: self.identifier)
return
}
let currentValue = self.readPrefAsFloat(for: .audioSpeakerVolume)
var muteValue: Int?
let volumeOSDValue = self.calcNewValue(currentValue: currentValue, isUp: isUp, isSmallIncrement: isSmallIncrement)
self.setVolume(volumeOSDValue, roundChiclet: !isSmallIncrement)
}

func adjustVolume(by delta: Float, forceOsd: Bool = false) {
guard self.canAdjustVolume() else {
OSDUtils.showOsdVolumeDisabled(displayID: self.identifier)
return
}
let currentValue = self.readPrefAsFloat(for: .audioSpeakerVolume)
let volumeOSDValue = max(0, min(1, currentValue + delta))
self.setVolume(volumeOSDValue, roundChiclet: false, forceOsd: forceOsd)
}

private func setVolume(_ volumeOSDValue: Float, roundChiclet: Bool, forceOsd: Bool = false) {
var muteValue: Int?
if self.readPrefAsInt(for: .audioMuteScreenBlank) == 1, volumeOSDValue > 0 {
muteValue = 2
} else if self.readPrefAsInt(for: .audioMuteScreenBlank) != 1, volumeOSDValue == 0 {
Expand All @@ -188,8 +206,12 @@ class OtherDisplay: Display {
self.writeDDCValues(command: .audioSpeakerVolume, value: self.convValueToDDC(for: .audioSpeakerVolume, from: volumeOSDValue))
}
}
if !self.readPrefAsBool(key: .hideOsd) {
OSDUtils.showOsd(displayID: self.identifier, command: .audioSpeakerVolume, value: volumeOSDValue, roundChiclet: !isSmallIncrement)
if forceOsd || !self.readPrefAsBool(key: .hideOsd) {
if roundChiclet {
OSDUtils.showOsd(displayID: self.identifier, command: .audioSpeakerVolume, value: volumeOSDValue, roundChiclet: true)
} else {
OSDUtils.showOsdProgress(displayID: self.identifier, command: .audioSpeakerVolume, value: volumeOSDValue)
}
}
if !isAlreadySet {
self.savePref(volumeOSDValue, for: .audioSpeakerVolume)
Expand Down
25 changes: 23 additions & 2 deletions MonitorControl/Support/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}()
var mediaKeyTap = MediaKeyTapManager()
var keyboardShortcuts = KeyboardShortcutsManager()
var edgeScrollManager = EdgeScrollManager()
let coreAudio = SimplyCoreAudio()
var accessibilityObserver: NSObjectProtocol!
var statusItemObserver: NSObjectProtocol!
Expand All @@ -43,6 +44,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
mainPrefsVc!,
menuslidersPrefsVc!,
keyboardPrefsVc!,
mousePrefsVc,
displaysPrefsVc!,
aboutPrefsVc!,
],
Expand All @@ -62,6 +64,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
self.setPrefsBuildNumber()
self.setDefaultPrefs()
self.setMenu()
self.edgeScrollManager.update()
CGDisplayRegisterReconfigurationCallback({ _, _, _ in app.displayReconfigured() }, nil)
self.configure(firstrun: true)
DisplayManager.shared.createGammaActivityEnforcer()
Expand Down Expand Up @@ -112,6 +115,22 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// Only settings that are not false, 0 or "" by default are set here. Assumes pre-wiped database.
prefs.set(true, forKey: PrefKey.appAlreadyLaunched.rawValue)
prefs.set(true, forKey: PrefKey.SUEnableAutomaticChecks.rawValue)
prefs.set(EdgeScrollAction.brightness.rawValue, forKey: PrefKey.edgeScrollLeftAction.rawValue)
prefs.set(EdgeScrollAction.volume.rawValue, forKey: PrefKey.edgeScrollRightAction.rawValue)
prefs.set(EdgeScrollPrecision.standard.rawValue, forKey: PrefKey.edgeScrollPrecision.rawValue)
prefs.set(true, forKey: PrefKey.edgeScrollVolumeSoundFeedback.rawValue)
}
if prefs.object(forKey: PrefKey.edgeScrollLeftAction.rawValue) == nil {
prefs.set(EdgeScrollAction.brightness.rawValue, forKey: PrefKey.edgeScrollLeftAction.rawValue)
}
if prefs.object(forKey: PrefKey.edgeScrollRightAction.rawValue) == nil {
prefs.set(EdgeScrollAction.volume.rawValue, forKey: PrefKey.edgeScrollRightAction.rawValue)
}
if prefs.object(forKey: PrefKey.edgeScrollPrecision.rawValue) == nil {
prefs.set(EdgeScrollPrecision.standard.rawValue, forKey: PrefKey.edgeScrollPrecision.rawValue)
}
if prefs.object(forKey: PrefKey.edgeScrollVolumeSoundFeedback.rawValue) == nil {
prefs.set(true, forKey: PrefKey.edgeScrollVolumeSoundFeedback.rawValue)
}
}

Expand Down Expand Up @@ -161,7 +180,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}

func checkPermissions(firstAsk: Bool = false) {
let permissionsRequired: Bool = [KeyboardVolume.media.rawValue, KeyboardVolume.both.rawValue].contains(prefs.integer(forKey: PrefKey.keyboardVolume.rawValue)) || [KeyboardBrightness.media.rawValue, KeyboardBrightness.both.rawValue].contains(prefs.integer(forKey: PrefKey.keyboardBrightness.rawValue))
let edgeScrollPermissionsRequired = EdgeScrollAction(rawValue: prefs.integer(forKey: PrefKey.edgeScrollLeftAction.rawValue)) != .disabled || EdgeScrollAction(rawValue: prefs.integer(forKey: PrefKey.edgeScrollRightAction.rawValue)) != .disabled
let permissionsRequired: Bool = [KeyboardVolume.media.rawValue, KeyboardVolume.both.rawValue].contains(prefs.integer(forKey: PrefKey.keyboardVolume.rawValue)) || [KeyboardBrightness.media.rawValue, KeyboardBrightness.both.rawValue].contains(prefs.integer(forKey: PrefKey.keyboardBrightness.rawValue)) || edgeScrollPermissionsRequired
if !MediaKeyTapManager.readPrivileges(prompt: false), permissionsRequired {
MediaKeyTapManager.acquirePrivileges(firstAsk: firstAsk)
}
Expand All @@ -174,7 +194,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(self.wakeNotification), name: NSWorkspace.screensDidWakeNotification, object: nil)
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
_ = 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(); self.edgeScrollManager.update() } } // listen for accessibility status changes
self.statusItemObserver = statusItem.observe(\.isVisible, options: [.old, .new]) { _, _ in self.statusItemVisibilityChanged() }
}

Expand Down Expand Up @@ -281,6 +301,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
self.setDefaultPrefs()
self.checkPermissions()
self.updateMediaKeyTap()
self.edgeScrollManager.update()
self.configure(firstrun: true)
}

Expand Down
Loading