This commit is contained in:
huakaiyun 2026-05-09 05:42:31 +00:00 committed by GitHub
commit 9f2ea8a748
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 543 additions and 13 deletions

2
.gitignore vendored
View file

@ -5,4 +5,4 @@ Carthage
.DS_Store
### Xcode ###
xcuserdata/
xcuserdata/build/

View file

@ -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 */; };
@ -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>"; };
@ -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 */,
@ -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 */,
@ -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 */,
@ -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 */,
@ -862,6 +870,7 @@
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";
@ -869,7 +878,7 @@
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)/**",
@ -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)/**",
@ -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;
@ -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;

View file

@ -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
@ -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
}
}
}

View file

@ -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")
}

View file

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

View file

@ -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)

View file

@ -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 {
@ -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)

View file

@ -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!
@ -43,6 +44,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
mainPrefsVc!,
menuslidersPrefsVc!,
keyboardPrefsVc!,
mousePrefsVc,
displaysPrefsVc!,
aboutPrefsVc!,
],
@ -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()
@ -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)
}
}
@ -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)
}
@ -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() }
}
@ -281,6 +301,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
self.setDefaultPrefs()
self.checkPermissions()
self.updateMediaKeyTap()
self.edgeScrollManager.update()
self.configure(firstrun: true)
}

View file

@ -0,0 +1,202 @@
// Copyright © MonitorControl. @JoniVR, @theOneyouseek, @waydabber and others
import Cocoa
import CoreGraphics
import os.log
import SimplyCoreAudio
private enum EdgeScrollSide {
case left
case right
}
private func edgeScrollEventTapCallback(proxy _: CGEventTapProxy, type: CGEventType, event: CGEvent, refcon: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
guard let refcon = refcon else {
return Unmanaged.passUnretained(event)
}
let manager = Unmanaged<EdgeScrollManager>.fromOpaque(refcon).takeUnretainedValue()
return manager.handleEventTap(type: type, event: event)
}
class EdgeScrollManager {
private let edgeActivationWidth: CGFloat = 6
private var eventTap: CFMachPort?
private var runLoopSource: CFRunLoopSource?
private var globalScrollMonitor: Any?
private var localScrollMonitor: Any?
private var preciseScrollRemainder: CGFloat = 0
private let volumeSoundFeedbackInterval: TimeInterval = 0.12
private var lastVolumeSoundFeedbackTime: TimeInterval = 0
func update() {
self.stop()
guard self.isEnabled else {
return
}
self.startEventTap()
if self.eventTap == nil {
self.startEventMonitors()
}
}
private var isEnabled: Bool {
self.action(for: .left) != .disabled || self.action(for: .right) != .disabled
}
private func stop() {
if let eventTap = eventTap {
CGEvent.tapEnable(tap: eventTap, enable: false)
}
if let runLoopSource = runLoopSource {
CFRunLoopRemoveSource(CFRunLoopGetMain(), runLoopSource, .commonModes)
}
if let globalScrollMonitor = globalScrollMonitor {
NSEvent.removeMonitor(globalScrollMonitor)
}
if let localScrollMonitor = localScrollMonitor {
NSEvent.removeMonitor(localScrollMonitor)
}
self.eventTap = nil
self.runLoopSource = nil
self.globalScrollMonitor = nil
self.localScrollMonitor = nil
self.preciseScrollRemainder = 0
}
private func startEventTap() {
let eventMask = CGEventMask(1 << CGEventType.scrollWheel.rawValue)
guard let eventTap = CGEvent.tapCreate(tap: .cgSessionEventTap, place: .headInsertEventTap, options: .defaultTap, eventsOfInterest: eventMask, callback: edgeScrollEventTapCallback, userInfo: Unmanaged.passUnretained(self).toOpaque()) else {
os_log("Edge scroll event tap unavailable. Falling back to event monitors.", type: .info)
return
}
self.eventTap = eventTap
let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
self.runLoopSource = runLoopSource
CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSource, .commonModes)
CGEvent.tapEnable(tap: eventTap, enable: true)
}
private func startEventMonitors() {
self.globalScrollMonitor = NSEvent.addGlobalMonitorForEvents(matching: .scrollWheel) { [weak self] event in
_ = self?.handleScroll(event)
}
self.localScrollMonitor = NSEvent.addLocalMonitorForEvents(matching: .scrollWheel) { [weak self] event in
if self?.handleScroll(event) == true {
return nil
}
return event
}
}
fileprivate func handleEventTap(type: CGEventType, event: CGEvent) -> Unmanaged<CGEvent>? {
if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput {
if let eventTap = self.eventTap {
CGEvent.tapEnable(tap: eventTap, enable: true)
}
return Unmanaged.passUnretained(event)
}
guard type == .scrollWheel, let nsEvent = NSEvent(cgEvent: event) else {
return Unmanaged.passUnretained(event)
}
return self.handleScroll(nsEvent) ? nil : Unmanaged.passUnretained(event)
}
private func handleScroll(_ event: NSEvent) -> Bool {
guard app.sleepID == 0, app.reconfigureID == 0, let target = self.targetForMouseLocation(), target.display.readPrefAsBool(key: .isDisabled) == false else {
return false
}
let selectedAction = self.action(for: target.side)
guard selectedAction != .disabled else {
return false
}
let stepCount = self.stepCount(from: event)
guard stepCount != 0 else {
return true
}
let delta = self.precision.step * Float(stepCount)
switch selectedAction {
case .brightness:
target.display.adjustBrightness(by: delta)
case .volume:
if !self.adjustSystemVolume(by: delta, displayID: target.display.identifier) {
return false
}
case .disabled:
break
}
return true
}
private func adjustSystemVolume(by delta: Float, displayID: CGDirectDisplayID) -> Bool {
guard let defaultDevice = app.coreAudio.defaultOutputDevice,
defaultDevice.canSetVirtualMainVolume(scope: .output),
let currentVolume = defaultDevice.virtualMainVolume(scope: .output) else {
OSDUtils.showOsdVolumeDisabled(displayID: displayID)
return false
}
let nextVolume = max(0, min(1, currentVolume + Float32(delta)))
guard defaultDevice.setVirtualMainVolume(nextVolume, scope: .output) else {
OSDUtils.showOsdVolumeDisabled(displayID: displayID)
return false
}
OSDUtils.showOsdProgress(displayID: displayID, command: .audioSpeakerVolume, value: Float(nextVolume))
self.playVolumeChangedSoundIfNeeded()
return true
}
private func playVolumeChangedSoundIfNeeded() {
guard prefs.bool(forKey: PrefKey.edgeScrollVolumeSoundFeedback.rawValue) else {
return
}
let now = Date.timeIntervalSinceReferenceDate
guard now - self.lastVolumeSoundFeedbackTime >= self.volumeSoundFeedbackInterval else {
return
}
self.lastVolumeSoundFeedbackTime = now
DispatchQueue.main.async {
app.playVolumeChangedSound()
}
}
private func action(for side: EdgeScrollSide) -> EdgeScrollAction {
let prefKey = side == .left ? PrefKey.edgeScrollLeftAction : PrefKey.edgeScrollRightAction
return EdgeScrollAction(rawValue: prefs.integer(forKey: prefKey.rawValue)) ?? .disabled
}
private var precision: EdgeScrollPrecision {
EdgeScrollPrecision(rawValue: prefs.integer(forKey: PrefKey.edgeScrollPrecision.rawValue)) ?? .standard
}
private func stepCount(from event: NSEvent) -> Int {
let scrollDelta = event.scrollingDeltaY
guard abs(scrollDelta) > 0.01 else {
return 0
}
if event.hasPreciseScrollingDeltas {
self.preciseScrollRemainder += scrollDelta / 12
guard abs(self.preciseScrollRemainder) >= 1 else {
return 0
}
let steps = max(-3, min(3, Int(self.preciseScrollRemainder.rounded(.towardZero))))
self.preciseScrollRemainder -= CGFloat(steps)
return steps
}
let steps = max(1, min(3, Int(abs(scrollDelta).rounded(.towardZero))))
return scrollDelta > 0 ? steps : -steps
}
private func targetForMouseLocation() -> (side: EdgeScrollSide, display: Display)? {
let mouseLocation = NSEvent.mouseLocation
guard let screen = NSScreen.screens.first(where: { NSMouseInRect(mouseLocation, $0.frame, false) }),
let display = DisplayManager.shared.displays.first(where: { $0.identifier == screen.displayID }) else {
return nil
}
if mouseLocation.x <= screen.frame.minX + self.edgeActivationWidth {
return (.left, display)
}
if mouseLocation.x >= screen.frame.maxX - self.edgeActivationWidth {
return (.right, display)
}
return nil
}
}

View file

@ -39,6 +39,10 @@ class OSDUtils: NSObject {
manager.showImage(osdImage.rawValue, onDisplayID: displayID, priority: 0x1F4, msecUntilFade: 1000, filledChiclets: UInt32(filledChiclets), totalChiclets: UInt32(totalChiclets), locked: lock)
}
static func showOsdProgress(displayID: CGDirectDisplayID, command: Command, value: Float, maxValue: Float = 1, lock: Bool = false) {
self.showOsd(displayID: displayID, command: command, value: value * 64, maxValue: maxValue * 64, roundChiclet: false, lock: lock)
}
static func showOsdVolumeDisabled(displayID: CGDirectDisplayID) {
guard let manager = OSDManager.sharedManager() as? OSDManager else {
return

View file

@ -145,8 +145,41 @@
/* Shown in menu */
"Volume" = "Volume";
/* Shown in Mouse Settings */
"Volume feedback sound" = "Volume feedback sound";
/* Shown in the alert dialog */
"Yes" = "Yes";
/* Shown in the alert dialog */
"You need to enable MonitorControl in System Settings > Security and Privacy > Accessibility for the keyboard shortcuts to work" = "You need to enable MonitorControl in System Settings > Security and Privacy > Accessibility for the keyboard shortcuts to work";
/* Shown in Mouse Settings */
"Coarse (5%)" = "Coarse (5%)";
/* Shown in Mouse Settings */
"Disabled" = "Disabled";
/* Shown in Mouse Settings */
"Fine (1%)" = "Fine (1%)";
/* Shown in Mouse Settings */
"Left screen edge" = "Left screen edge";
/* Shown in the main prefs window */
"Mouse" = "Mouse";
/* Shown in Mouse Settings */
"Move the pointer to a screen edge and use the scroll wheel to control the selected value on that screen." = "Move the pointer to a screen edge and use the scroll wheel to control the selected value on that screen.";
/* Shown in Mouse Settings */
"Right screen edge" = "Right screen edge";
/* Shown in Mouse Settings */
"Scroll wheel precision" = "Scroll wheel precision";
/* Shown in Mouse Settings */
"Standard (2%)" = "Standard (2%)";
/* Shown in Mouse Settings */
"Very fine (0.5%)" = "Very fine (0.5%)";

View file

@ -147,8 +147,41 @@
/* Shown in menu */
"Volume" = "音量";
/* Shown in Mouse Settings */
"Volume feedback sound" = "音量反馈声音";
/* Shown in the alert dialog */
"Yes" = "是";
/* Shown in the alert dialog */
"You need to enable MonitorControl in System Settings > Security and Privacy > Accessibility for the keyboard shortcuts to work" = "您需要在「系统偏好设置」>「安全性与隐私」>「辅助功能」中启用MonitorControl让键盘快捷键生效";
/* Shown in Mouse Settings */
"Coarse (5%)" = "粗略 (5%)";
/* Shown in Mouse Settings */
"Disabled" = "关闭";
/* Shown in Mouse Settings */
"Fine (1%)" = "精细 (1%)";
/* Shown in Mouse Settings */
"Left screen edge" = "屏幕左边缘";
/* Shown in the main prefs window */
"Mouse" = "鼠标";
/* Shown in Mouse Settings */
"Move the pointer to a screen edge and use the scroll wheel to control the selected value on that screen." = "将指针移到屏幕边缘,然后使用滚轮控制该屏幕上的选定项目。";
/* Shown in Mouse Settings */
"Right screen edge" = "屏幕右边缘";
/* Shown in Mouse Settings */
"Scroll wheel precision" = "滚轮调节精度";
/* Shown in Mouse Settings */
"Standard (2%)" = "标准 (2%)";
/* Shown in Mouse Settings */
"Very fine (0.5%)" = "非常精细 (0.5%)";

View file

@ -64,6 +64,7 @@ class MainPrefsViewController: NSViewController, SettingsPane {
// Preload Display settings to some extent to properly set up size in orther that animation won't fail
menuslidersPrefsVc?.view.layoutSubtreeIfNeeded()
keyboardPrefsVc?.view.layoutSubtreeIfNeeded()
mousePrefsVc.view.layoutSubtreeIfNeeded()
displaysPrefsVc?.view.layoutSubtreeIfNeeded()
aboutPrefsVc?.view.layoutSubtreeIfNeeded()
self.updateGridLayout()
@ -152,6 +153,7 @@ class MainPrefsViewController: NSViewController, SettingsPane {
self.populateSettings()
menuslidersPrefsVc?.populateSettings()
keyboardPrefsVc?.populateSettings()
mousePrefsVc.populateSettings()
displaysPrefsVc?.populateSettings()
}
}

View file

@ -0,0 +1,153 @@
// Copyright © MonitorControl. @JoniVR, @theOneyouseek, @waydabber and others
import Cocoa
import Settings
private final class MousePrefsRootView: NSView {
override var intrinsicContentSize: NSSize {
NSSize(width: 480, height: 220)
}
}
class MousePrefsViewController: NSViewController, SettingsPane {
let paneIdentifier = Settings.PaneIdentifier.mouse
let paneTitle: String = NSLocalizedString("Mouse", comment: "Shown in the main prefs window")
var toolbarItemIcon: NSImage {
if !DEBUG_MACOS10, #available(macOS 11.0, *) {
return NSImage(systemSymbolName: "computermouse", accessibilityDescription: "Mouse") ?? NSImage(named: NSImage.infoName)!
} else {
return NSImage(named: NSImage.infoName)!
}
}
private let leftEdgeAction = NSPopUpButton()
private let rightEdgeAction = NSPopUpButton()
private let scrollPrecision = NSPopUpButton()
private let volumeSoundFeedback = NSButton(checkboxWithTitle: "", target: nil, action: nil)
override func loadView() {
self.view = MousePrefsRootView(frame: NSRect(x: 0, y: 0, width: 480, height: 220))
self.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
self.view.widthAnchor.constraint(equalToConstant: 480),
self.view.heightAnchor.constraint(equalToConstant: 220),
])
self.buildView()
}
override func viewDidLoad() {
super.viewDidLoad()
self.populateSettings()
}
func populateSettings() {
self.leftEdgeAction.selectItem(withTag: prefs.integer(forKey: PrefKey.edgeScrollLeftAction.rawValue))
self.rightEdgeAction.selectItem(withTag: prefs.integer(forKey: PrefKey.edgeScrollRightAction.rawValue))
self.scrollPrecision.selectItem(withTag: prefs.integer(forKey: PrefKey.edgeScrollPrecision.rawValue))
self.volumeSoundFeedback.state = prefs.bool(forKey: PrefKey.edgeScrollVolumeSoundFeedback.rawValue) ? .on : .off
}
private func buildView() {
self.configureActionPopUp(self.leftEdgeAction)
self.configureActionPopUp(self.rightEdgeAction)
self.configurePrecisionPopUp(self.scrollPrecision)
self.leftEdgeAction.target = self
self.leftEdgeAction.action = #selector(self.leftEdgeActionChanged(_:))
self.rightEdgeAction.target = self
self.rightEdgeAction.action = #selector(self.rightEdgeActionChanged(_:))
self.scrollPrecision.target = self
self.scrollPrecision.action = #selector(self.scrollPrecisionChanged(_:))
self.volumeSoundFeedback.target = self
self.volumeSoundFeedback.action = #selector(self.volumeSoundFeedbackChanged(_:))
self.volumeSoundFeedback.setAccessibilityLabel(NSLocalizedString("Volume feedback sound", comment: "Shown in Mouse Settings"))
let stack = NSStackView()
stack.orientation = .vertical
stack.alignment = .leading
stack.spacing = 14
stack.edgeInsets = NSEdgeInsets(top: 24, left: 28, bottom: 24, right: 28)
stack.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(stack)
stack.addArrangedSubview(self.makeRow(title: NSLocalizedString("Left screen edge", comment: "Shown in Mouse Settings"), control: self.leftEdgeAction))
stack.addArrangedSubview(self.makeRow(title: NSLocalizedString("Right screen edge", comment: "Shown in Mouse Settings"), control: self.rightEdgeAction))
stack.addArrangedSubview(self.makeRow(title: NSLocalizedString("Scroll wheel precision", comment: "Shown in Mouse Settings"), control: self.scrollPrecision))
stack.addArrangedSubview(self.makeRow(title: NSLocalizedString("Volume feedback sound", comment: "Shown in Mouse Settings"), control: self.volumeSoundFeedback))
let infoLabel = NSTextField(labelWithString: NSLocalizedString("Move the pointer to a screen edge and use the scroll wheel to control the selected value on that screen.", comment: "Shown in Mouse Settings"))
infoLabel.textColor = .secondaryLabelColor
infoLabel.maximumNumberOfLines = 2
infoLabel.lineBreakMode = .byWordWrapping
infoLabel.translatesAutoresizingMaskIntoConstraints = false
stack.addArrangedSubview(infoLabel)
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
stack.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
stack.topAnchor.constraint(equalTo: self.view.topAnchor),
stack.bottomAnchor.constraint(lessThanOrEqualTo: self.view.bottomAnchor),
infoLabel.widthAnchor.constraint(equalToConstant: 420),
])
}
private func makeRow(title: String, control: NSView) -> NSStackView {
let label = NSTextField(labelWithString: title)
label.alignment = .right
label.translatesAutoresizingMaskIntoConstraints = false
control.translatesAutoresizingMaskIntoConstraints = false
let row = NSStackView(views: [label, control])
row.orientation = .horizontal
row.alignment = .centerY
row.spacing = 12
NSLayoutConstraint.activate([
label.widthAnchor.constraint(equalToConstant: 160),
control.widthAnchor.constraint(equalToConstant: 190),
])
return row
}
private func configureActionPopUp(_ popUpButton: NSPopUpButton) {
popUpButton.removeAllItems()
self.addItem(to: popUpButton, title: NSLocalizedString("Disabled", comment: "Shown in Mouse Settings"), tag: EdgeScrollAction.disabled.rawValue)
self.addItem(to: popUpButton, title: NSLocalizedString("Brightness", comment: "Shown in Mouse Settings"), tag: EdgeScrollAction.brightness.rawValue)
self.addItem(to: popUpButton, title: NSLocalizedString("Volume", comment: "Shown in Mouse Settings"), tag: EdgeScrollAction.volume.rawValue)
}
private func configurePrecisionPopUp(_ popUpButton: NSPopUpButton) {
popUpButton.removeAllItems()
self.addItem(to: popUpButton, title: NSLocalizedString("Standard (2%)", comment: "Shown in Mouse Settings"), tag: EdgeScrollPrecision.standard.rawValue)
self.addItem(to: popUpButton, title: NSLocalizedString("Fine (1%)", comment: "Shown in Mouse Settings"), tag: EdgeScrollPrecision.fine.rawValue)
self.addItem(to: popUpButton, title: NSLocalizedString("Very fine (0.5%)", comment: "Shown in Mouse Settings"), tag: EdgeScrollPrecision.veryFine.rawValue)
self.addItem(to: popUpButton, title: NSLocalizedString("Coarse (5%)", comment: "Shown in Mouse Settings"), tag: EdgeScrollPrecision.coarse.rawValue)
}
private func addItem(to popUpButton: NSPopUpButton, title: String, tag: Int) {
popUpButton.addItem(withTitle: title)
popUpButton.lastItem?.tag = tag
}
@objc private func leftEdgeActionChanged(_ sender: NSPopUpButton) {
prefs.set(sender.selectedTag(), forKey: PrefKey.edgeScrollLeftAction.rawValue)
self.edgeScrollSettingsChanged()
}
@objc private func rightEdgeActionChanged(_ sender: NSPopUpButton) {
prefs.set(sender.selectedTag(), forKey: PrefKey.edgeScrollRightAction.rawValue)
self.edgeScrollSettingsChanged()
}
@objc private func scrollPrecisionChanged(_ sender: NSPopUpButton) {
prefs.set(sender.selectedTag(), forKey: PrefKey.edgeScrollPrecision.rawValue)
self.edgeScrollSettingsChanged()
}
@objc private func volumeSoundFeedbackChanged(_ sender: NSButton) {
prefs.set(sender.state == .on, forKey: PrefKey.edgeScrollVolumeSoundFeedback.rawValue)
}
private func edgeScrollSettingsChanged() {
app.checkPermissions()
app.edgeScrollManager.update()
}
}

View file

@ -25,6 +25,7 @@ let mainPrefsVc = storyboard.instantiateController(withIdentifier: "MainPrefsVC"
let displaysPrefsVc = storyboard.instantiateController(withIdentifier: "DisplaysPrefsVC") as? DisplaysPrefsViewController
let menuslidersPrefsVc = storyboard.instantiateController(withIdentifier: "MenuslidersPrefsVC") as? MenuslidersPrefsViewController
let keyboardPrefsVc = storyboard.instantiateController(withIdentifier: "KeyboardPrefsVC") as? KeyboardPrefsViewController
let mousePrefsVc = MousePrefsViewController()
let aboutPrefsVc = storyboard.instantiateController(withIdentifier: "AboutPrefsVC") as? AboutPrefsViewController
let onboardingVc = storyboard.instantiateController(withIdentifier: "onboardingViewController") as? NSWindowController

View file

@ -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>LSBackgroundOnly</key>