Scrollbar Hacking on OS X
I have a simple OS X Lion app called LionScrollbars (on MacUpdate, on BitBucket). It lets users modify the preference settings for individual apps so scrollbars can be hidden/shown differently for different apps. The original idea was to bring back the old scrollbars; sure enough after I publically launched the app (months after writing and using it myself) I got the same request from users. Alright, so I looked into it for real.
SIMBL is a nice framework that lets you load code into other programs at runtime. Using this I thought I might be able to override scrollbar behavior. I found a post someplace that also had a user thinking it was possible.
So I looked up various ways of overriding scrollbar behavior in Cocoa. There are a bunch of posts about simulating overlay scrollbars, etc.
- Overlay NSScroller over content
- NSScrollview and transparent, overlay NSScroller subclasses
- How to draw a transparent NSScroller
- iOS-style scrollbars for NSScrollView
I made a simple project allowing for both types of scrollbars (overlays that don’t hide, and custom-styled scrolls more like pre-Lion) without too much aggrevation. But that was only half of it. I needed to be able to inject it into existing applications. I read up on SIMBL-style injection. Various tutorials exist out there (e.g., adding a menu plugin), but at least what I found did not explain what I needed (namely, overriding framework-level classes). Posing is deprecated in 10.7, so that was not an option. I finally found this awesome guide (“CocoaReverseEngineering”) on Cocoa hacking that helped a ton. It opened the door to runtime method substitution.
I rewrote my simple project as categories on NSScroller and NSScrollView. But I encountered all kinds of issues with it; though in hindsight I understand why now, at the time some methods were not being successfully swizzled. I sort of got something working, but menus were still hiding themselves due to how 10.7 implements the new scrollbars (it assumes that all subclasses are not compatible with the new overlays, but that the NSScroller class is; for the life of me I couldn’t get the method indicating compatibility to report it wasn’t fit for the task, so they always hid themselves). I now realize these troubles were because I was trying to swizzle class methods as if they were instance methods, so likely that approach would actually have worked…
I got the idea to try a combination of the simple project and categories: I created a category for NSScrollView that replaced its scrollers with my subclass:
This sort of worked. But I was replacing the scrollbars in the NSScrollView -(void) tile
method, and setting the scrollbars caused tile
to be called again before it finished, causing duplicate scrollbars. Here’s the stack trace showing this:
// first time tile is called:
NSScrollView(hacked additions) lsb_tile @
NSScrollView _tileWithoutRecursing @
// second time tile is called:
NSScrollView(hacked additions) lsb_tile @
NSScrollView _commonNewScroll:
NSScrollView setVerticalScroller @
NSScrollView(hacked additions) lsb_tile @
NSScrollView _tileWithoutRecursing @
I setup F-Script so I could look into other possible hacking points. At first glance I found the following:
In NSScrollView:
In NSScroller:
Then I tried two things:
- Replacing the active class of NSScrollbar instances using
object_setClass()
. - Overriding two methods (
setOverlayScrollerKnobAlpha
&setOverlayScrollerTrackAlpha
) to prevent fading.
I thought (1) would trick the NSScrollView into thinking the scroller wasn’t able to do new style overlays, so the scrollers wouldn’t auto disappear, but maybe due to my not having set the meta-class for the object (?), though it did hijack the class, but did not stop scrollbar fading (probably was not using my class method). (2) however, did stop the fading. Victory! I got the idea to let the scrollers fade some, but not all the way.
This approach, however, does not utilize my custom scrollers at all, which will not allow for recreating the old pre-Lion scrollbars. It does, however, work!
Poking around more (using class-dump), I found that the best approach to overriding scrollbars is probably using a custom implementation of NSScrollerImp, adding (technically invalid) values for the scrollbar style, and then swizzling the NSScrollerImp
class method to load mine if it’s one of my styles, or fall back to the default method otherwise.
This might allow for a very clean approach – when the style gets looked up, it simply returns my custom scroller class.
Video showing it at work
OS X Lion Scrollbar Hacking from Dain Kaplan on Vimeo.
Grabbing glypths
A demo app lets you browse through OS X’s glyphs using a private api call _NSGetThemeWidgetImage
. Image 0xfc, 0x34 seems to be the aqua scroller knob as small as it goes, 0x50 the repeated image of the knob, etc.
A bit more about objective-c
Interesting things discovered along the way.
Opaque types
The implementations of some of the core types (Class, Method, etc.) are not available in source. This can be circumvented by defining them yourself (as long as memory offsets can be calculated the results should be the same), but no posts seemed to explicitly mention this…
Classes and meta-classes
It looks likes a Class in objective-c contains all the instance methods (defined with -
), and the Class’s Meta-Class (also a Class) contains the class-level methods (defined with +
).
Runtime class changes?
You can change the runtime class of an existing instance! (Well, it changes the look up table where selectors are mapped to implimentions, effectively using another class…)