Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to programatically change the cursor size on a Mac

I'd like to be able to set the system preference for cursor size (As seen in the Accessibility Preferences) on a Mac from within my program, then set it back once the program quits.

Is there a way to set the cursor size (specifically) or system preferences in general from an app?

like image 365
SimonClark Avatar asked Jan 24 '13 21:01

SimonClark


People also ask

How do you change the size of your cursor on a Mac?

Click Display, then click Pointer. Set any of these options: Shake mouse pointer to locate: Make the pointer larger when you quickly move your finger on the trackpad or quickly move the mouse. Pointer size: Drag the slider until the pointer size is right for you.

Does custom cursor work on Mac?

Apple has simplified mouse cursor customization with macOS Monterey. Now, if you wish to change your mouse pointer to any color or change the color of its outline so that it is easily visible while passing over crosshairs, shapes, or insertion points, you can do it with a few quick and simple steps.


1 Answers

First, if you're just trying to get a larger cursor when the cursor is pointing at your window/view/widget, you're going about this the wrong say. Read Introduction to Cursor Manager for the right way.

Second, even if you think you actually want to set the system-wide cursor while you're program is running, think about it more carefully before you go forward. The cursor will stay large even if your app is in the background, or hidden. If you've made any moves at all toward the transparent lifecycle idea (that a user shouldn't usually notice, or care about, the difference between your app not being visible and your app having quit), this will be even more confusing. If two apps try to do this, what should happen? And so on. (Needless to say, Apple would reject any app from the App Store that did this.)

Third, setting the system preference doesn't actually do anything, until the new time the system reads that preference. And there's no guarantee on when that will happen. So, unless your app is content to change a preference that may not take effect until the user, say, logs out and back in again (and then change it back after you quit), it's not all that useful.

But if this is really what you want to do…


It's very easy to set the system preference. Most of the values modified by System Preferences are in the defaults storage. Most of the values in the Accessibility pane are in the com.apple.universalaccess domain. The particular key for the cursor size is mouseDriverCursorSize.

So, to change the cursor to max size from bash:

defaults write com.apple.universalaccess mouseDriverCursorSize 4.0

It's a bit more tedious from ObjC, but something like this (untested):

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSDictionary *olddict = [defaults persistentDomainForName:@"com.apple.universalaccess"];
NSMutableDictionary *newdict = [olddict mutableCopy];
[newdict setObject:@4.0 forKey:@"mouseDriverCursorSize"];
[defaults setPersistentDomain:newdict forName:@"com.apple.universalaccess"];
[defaults synchronize]

So, what if you want to set the preference, and then force the system to notice the change? Obviously the System Preferences app is doing something, and you could always trace it do see exactly what it does.

More often than not, what it does is call some private function that isn't documented or exposed. And it may be different between different OS versions. And what it does may not be the best thing to do anyway. But from a quick test:

It looks like calling CGSShowCursor works, as long as it's acceptable to un-hide the cursor if it was hidden. Calling CGSGetGlobalCursorData twice in a row also seems to work, although I have no idea why it should.

Of course these are CGSPrivate functions that aren't documented or exposed, but at least other people have reverse engineered them, so you don't have to. All you have to do is borrow the code from some open source project (iTerm2 has one of the more complete sets of headers), and test after every new minor OS release from Apple, and debug the black magic that doesn't work for 25% of your users even though it works for the other 75% (without having access to the machines those 25% are getting, and usually without even being able to get decent questions or answers from them).

If you want to trace System Preferences, and you have no experience tracing processes in OS X, the easiest way is through the GUI tool Instruments:

  • Launch System Preferences and navigate to Accessibility, Display.
  • Launch Xcode 4.4 or later, go to the "Xcode" menu, select "Open Developer Tool", then "Instruments".
  • In Instruments, choose the "Mac OS X | All" section, then "System Trace".
  • In the "Target" pulldown, attach to the "System Preferences" process.
  • Click the "Record" button, and wait a few seconds for it to stop beachballing.
  • Drag the Cursor Size slider.
  • Click the "Stop" button, and wait even longer for it to finish analyzing.
  • Read the extension documentation on Instruments to figure out how to find what you want.

However, keep in mind that System Preferences might not be calling a special syscall to do what it needs; it may be, e.g., sending a specific mach message to the Window Server task. Fortunately, you can walk backward from anything that seems likely. By doing this, I found that it seems to be calling UACursorSetScale in UniversalAccessCore, which calls UAPreferencesSetValue in /System/Library/PrivateFrameworks/UniversalAccess.framework/Versions/A/Libraries/libUAPreferences.dylib, a function which seems to do a CFPreferencesSetValue and send a CFNotificationCenterPostNotification. Maybe it's just that notification that matters? You could test that by putting breakpoints on the relevant functions in Xcode/gdb/lldb and seeing what the parameters are. Or you could just figure out how to call UAPreferencesSetValue yourself (my first guess would be that the params are the same as CFPreferencesSetValue).

As a quick check: the notification it sends is "UniversalAccessDomainMouseSettingsDidChangeNotification" with a nil object and a userInfo dictionary like @{@"mouseDriverCursorSize": @1.8327533, @"pid": @12345} to the default distributed notification center, and doing the same thing yourself after changing the NSUserDefaults preference has no effect. Also, UAPreferencesSetValue apparently takes different params than CFPreferencesSetValue, because if you pass the obvious values you get a crash within CFNotificationCenterPostNotification, so you'll probably need to breakpoint the call in System Preferences to see what it sends.

If you're comfortable moving forward from this start, great. If not, you have a whole lot to learn before you should consider trying to make this work.


Another way to go about this is by scripting. If you can make the System Preferences app do the same thing the mouse makes it do, you're set, right?

As long as UI scripting is enabled (see the "Enable access for assistive devices" checkbox in the same pane you're already looking at in System Preferences, or google how to turn it on and off programmatically if you have root access), that's tedious, but easy, through System Events.

In fact, although System Preferences doesn't expose enough detail to actually change anything, it does expose enough to get us navigated to the right pane, which saves a lot of UI scripting steps. So, here's the AppleScript to do what you want:

tell application "System Preferences"
    reveal anchor "Seeing_Display" of pane id "com.apple.preference.universalaccess"
end tell
tell application "System Events"
    set theSlider to slider "Cursor Size:" of group 1 of window 1 of application process "System Preferences"
    set stash to value of theSlider
    set value of theSlider to 4.0
    stash
end tell

Run that from ObjC with NSAppleScript—or, if you prefer, translate it to ScriptingBridge, Appscript, or something else that you can run natively—and you're done.

like image 86
abarnert Avatar answered Oct 12 '22 17:10

abarnert