diff --git a/DiscreteScroll.xcodeproj/project.pbxproj b/DiscreteScroll.xcodeproj/project.pbxproj index 8906b44..f40c02f 100644 --- a/DiscreteScroll.xcodeproj/project.pbxproj +++ b/DiscreteScroll.xcodeproj/project.pbxproj @@ -249,14 +249,14 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; INFOPLIST_FILE = DiscreteScroll/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.9; - MARKETING_VERSION = 1.0.1; + MARKETING_VERSION = 1.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.emreyolcu.DiscreteScroll; PRODUCT_NAME = "$(TARGET_NAME)"; }; @@ -267,14 +267,14 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; INFOPLIST_FILE = DiscreteScroll/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.9; - MARKETING_VERSION = 1.0.1; + MARKETING_VERSION = 1.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.emreyolcu.DiscreteScroll; PRODUCT_NAME = "$(TARGET_NAME)"; }; diff --git a/DiscreteScroll/main.c b/DiscreteScroll/main.c index 3eb6c2d..95d2fe1 100644 --- a/DiscreteScroll/main.c +++ b/DiscreteScroll/main.c @@ -3,9 +3,16 @@ #define DEFAULT_LINES 3 #define SIGN(x) (((x) > 0) - ((x) < 0)) +static const CFStringRef AX_NOTIFICATION = CFSTR("com.apple.accessibility.api"); +static bool TRUSTED; + +static CFMachPortRef TAP; +static CFRunLoopSourceRef SOURCE; + static int LINES; -CGEventRef callback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *userInfo) +static CGEventRef tapCallback(CGEventTapProxy proxy, + CGEventType type, CGEventRef event, void *userInfo) { if (CGEventGetIntegerValueField(event, kCGScrollWheelEventIsContinuous) == 0) { int delta = (int)CGEventGetIntegerValueField(event, kCGScrollWheelEventPointDeltaAxis1); @@ -15,7 +22,7 @@ CGEventRef callback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, v return event; } -void displayNoticeAndExit(CFStringRef alertHeader) +static void displayNoticeAndExit(CFStringRef alertHeader) { CFUserNotificationDisplayNotice( 0, kCFUserNotificationCautionAlertLevel, @@ -26,44 +33,74 @@ void displayNoticeAndExit(CFStringRef alertHeader) exit(EXIT_FAILURE); } +static void notificationCallback(CFNotificationCenterRef center, void *observer, + CFNotificationName name, const void *object, + CFDictionaryRef userInfo) +{ + if (CFStringCompare(name, AX_NOTIFICATION, 0) == kCFCompareEqualTo) { + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + CFRunLoopPerformBlock( + runLoop, kCFRunLoopDefaultMode, ^{ + bool previouslyTrusted = TRUSTED; + if ((TRUSTED = AXIsProcessTrusted()) != previouslyTrusted) { + CFRunLoopStop(runLoop); + if (SOURCE && CFRunLoopContainsSource(runLoop, SOURCE, kCFRunLoopDefaultMode)) { + CGEventTapEnable(TAP, TRUSTED); + CFRunLoopRun(); + } else if (!TRUSTED) { + CFRunLoopRun(); + } + } + } + ); + } +} + +static bool getIntPreference(CFStringRef key, int *valuePtr) +{ + CFNumberRef number = (CFNumberRef)CFPreferencesCopyAppValue( + key, kCFPreferencesCurrentApplication + ); + bool got = false; + if (number) { + if (CFGetTypeID(number) == CFNumberGetTypeID()) + got = CFNumberGetValue(number, kCFNumberIntType, valuePtr); + CFRelease(number); + } + + return got; +} + int main(void) { + CFNotificationCenterAddObserver( + CFNotificationCenterGetDistributedCenter(), NULL, + notificationCallback, AX_NOTIFICATION, NULL, + CFNotificationSuspensionBehaviorDeliverImmediately + ); CFDictionaryRef options = CFDictionaryCreate( kCFAllocatorDefault, (const void **)&kAXTrustedCheckOptionPrompt, (const void **)&kCFBooleanTrue, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks ); - bool trusted = AXIsProcessTrustedWithOptions(options); + TRUSTED = AXIsProcessTrustedWithOptions(options); CFRelease(options); - if (!trusted) - displayNoticeAndExit( - CFSTR("Restart DiscreteScroll after granting it access to accessibility features.") - ); + if (!TRUSTED) + CFRunLoopRun(); - CFNumberRef value = (CFNumberRef)CFPreferencesCopyAppValue( - CFSTR("lines"), kCFPreferencesCurrentApplication - ); - bool got = false; - if (value) { - if (CFGetTypeID(value) == CFNumberGetTypeID()) - got = CFNumberGetValue(value, kCFNumberIntType, &LINES); - CFRelease(value); - } - if (!got) + if (!getIntPreference(CFSTR("lines"), &LINES)) LINES = DEFAULT_LINES; - CFMachPortRef tap = CGEventTapCreate( + TAP = CGEventTapCreate( kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, - CGEventMaskBit(kCGEventScrollWheel), callback, NULL + CGEventMaskBit(kCGEventScrollWheel), tapCallback, NULL ); - if (!tap) + if (!TAP) displayNoticeAndExit(CFSTR("DiscreteScroll could not create an event tap.")); - CFRunLoopSourceRef source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0); - if (!source) + SOURCE = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, TAP, 0); + if (!SOURCE) displayNoticeAndExit(CFSTR("DiscreteScroll could not create a run loop source.")); - CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode); - CFRelease(tap); - CFRelease(source); + CFRunLoopAddSource(CFRunLoopGetCurrent(), SOURCE, kCFRunLoopDefaultMode); CFRunLoopRun(); return EXIT_SUCCESS; diff --git a/README.md b/README.md index 9049c08..0da69ba 100644 --- a/README.md +++ b/README.md @@ -14,16 +14,28 @@ As of May 2024, this application works on macOS versions 10.9–14.0. ### Installation -You may download the binary [here](https://github.com/emreyolcu/discrete-scroll/releases/download/v1.0.1/DiscreteScroll.zip). - -It needs to be run each time you boot. -If you want this to be automatic, do the following: +You may download the binary [here](https://github.com/emreyolcu/discrete-scroll/releases/download/v1.1.0/DiscreteScroll.zip). +DiscreteScroll requires access to accessibility features. +Upon startup, if it does not have access, it will prompt you and wait. +You do not need to restart the application +after you grant it access to accessibility features. + +> [!CAUTION] +> You may safely toggle accessibility access +> for DiscreteScroll while it is running. +> *However, you should not remove it from the list of trusted applications +> while it is running without first unchecking the box next to its name. +> Otherwise, your mouse might become unresponsive.* + +If you want the application to run automatically when you log in, +do the following: 1. On macOS 13.0 and later, go to `System Settings > General > Login Items`; otherwise, go to `System Preferences > Users & Groups > Login Items`. 2. Add `DiscreteScroll` to the list. -If you want to quit the application, do the following: +If you want to quit the application, either run `killall DiscreteScroll` +or do the following: 1. Launch `Activity Monitor`. 2. Search for `DiscreteScroll` and select it. @@ -40,11 +52,22 @@ This number may even be negative, which inverts scrolling direction. defaults write com.emreyolcu.DiscreteScroll lines -int LINES ``` -If you set the key `lines` to some value other than an integer, -the default value of 3 is used as a fallback. +> [!WARNING] +> If you set `lines` to some value other than an integer, +> then the default value of 3 is used as a fallback. You should restart the application for the setting to take effect. +### Uninstallation + +To uninstall DiscreteScroll, quit the application, move it to trash, +and remove it from the lists for accessibility access and login items. +You can remove any stored preferences by running the following: + +``` +defaults delete com.emreyolcu.DiscreteScroll +``` + ### Potential problems Recent versions of macOS have made it difficult to run unsigned binaries.