Cocoa and Tab-Modality

Thursday, December 17, 2009 at 12:00 AM



(Note: as regular readers know, we occasionally publish extra-geeky technical posts here on the Google Mac Blog. If this isn't your thing, don't worry; our usual non-technical stuff will be back soon.)

I'm part of the Chromium team working on Mac issues, and as I wrote on our blog, for Chromium we needed a way to provide modality to individual tabs of our web browser. Specifically, we needed to attach sheets to a Cocoa view rather than to just a Cocoa window. How would we accomplish this?

Like always, it's half following what's possible, and half sudden inspiration. What's possible? Putting a sheet on a window. That's done several ways. For an arbitrary sheet you can use
-[NSApplication beginSheet:modalForWindow:modalDelegate:didEndSelector:contextInfo:]. But nearly every class that can put up a sheet has its own -beginSheet: method. NSAlert has
-beginSheetModalForWindow:modalDelegate:didEndSelector:contextInfo:. IKPictureTaker has
-beginPictureTakerSheetForWindow:withDelegate:didEndSelector:contextInfo:.

So we know that we need to hang our sheet on a window. That's where the inspiration comes in. I was talking with a fellow Mac team member who offhandedly mentioned invisible windows. Of course! If you have an invisible window that has a sheet attached, then for all practical purposes you have an independent sheet. Plus, if you make the invisible window the child window of the window that hosts the view that appears to run the sheet, then you can size the invisible window to cover the view and eat all the clicks, achieving the desired modality as well.

That was the easy part. The first implementation was quick, but quickly uncovered issues.

First, how do you hide the sheet when the view is hidden? At first I tried hiding the invisible window, but when you -orderOut: a window you kill any sheets on it. That wouldn't do. Then I remembered the good old days of the Mac Toolbox (and Cocoa before 10.3 when NSView got -setHidden:), where you'd just move windows or views off to infinity (or (-15000,-15000), whichever was closer). Exposé quickly revealed the folly of that approach. Turning the sheet's opacity to 0% worked under Leopard, but under Snow Leopard the sheet blurring effect stayed present. And if I resized the window to NSZeroSize, resizing it back to the original size wrecked the layout.

Eventually I settled on a combination that worked. First I set autoresizesSubviews of the content view of the sheet to NO, and then I resized the sheet down to nothing. Then I set the opacity to 0%. Once I set the invisible window to stop eating clicks, it all worked.

The second problem was all the different classes that provided methods to show a sheet. Even if you could get the sheet window from them, if you ran it using the NSApplication sheet method, it didn't work. A little (actually a lot) of NSInvocation magic helped smooth that issue over.

That's basically it. The API is really simple and the implementation is, if nothing else, amusing to read.

Should you go ahead and use this in your app? Probably not. This is a very specific tool for solving a very specific modality problem that we had. But if you have a similar modality problem, perhaps this is right for you. Give it a try and let us know what you think.