Archive for October, 2008

I Love Outline Views - Here’s Mine

Friday, October 24th, 2008

Apps like Coda are the de-facto standard for good user-interface design nowadays. You can do a lot with the standard Cocoa controls provided by Apple, but you can’t always get exactly what you want out-of-the-box. For example, the CSS edit section of Coda has a fantastic way of dividing tasks: the animated outline view.

Rather than having a monolithic view that has all the controls grouped with dividing lines; each section, “Text”, “Colors and Background”, “Dimensions”, all exist in their own collapsible view. Clicking the separating bar animatedly expands or collapses the view.


To make one of these views isn’t quite as easy as you first think, and you can quickly go down the wrong road in the design. The super-secret trick is to *not* use Core Animation, or more precisely: ignore the lure of NSViews conformance to the NSAnimatablePropertyContainer protocol. As of Mac OS X 10.5, a few of the properties of a view or window can be animated simply be replacing calls like [view setFrame:newFrame]; with [[view animator] setFrame:newFrame];. This is fine when you have one or two views that you want to move but it’s impossible to coordinate the movements of multiple view using this API. After using Core Animation, one would expect that after a call such as [[view animator] setFrame:newFrame]; requesting the frame from the view would return newFrame. Unfortunatley it returns the original frame of the view. Only when the animation is done, do you get newFrame. Furthermore, just try setting the delegate of the CABasicAnimation object that handles the implicit animation and getting any form of useful information back. You can’t. You’re in for a world of pain if you try.

The solution: use good-ol’ NSViewAnimation. That way you can create all the dictionaries containing the animations for all the views you want to, then instantiate one NSViewAnimation object to handle the lot and set them off at the same time. Simple.

The Animating Outline View

The outline view is made up of a few classes, the TLDisclosureBar, a subclass of the highly-configurable TLGradientView; the TLCollapsibleView which has a TLDisclosureBar and any NSView you like as its subviews. A TLCollapsibleView itself can be used alone, and will animate the collapse/expansion of the NSView subview (which I term the “detail view”).

The fun part comes when TLCollapsibleViews are subviews of a TLAnimatingOutlineView. In this case, the TLAnimatingOutlineView takes over all the animations, simply asking the TLCollapsibleView that the user has selected for the NSDictionary object that describes the collapse/expansion animation. With this information, it constructs the movement animations for all the subviews below the collapsing/expanding view and handles the required change of frame size to accommodate the changing content size. It’s all quite elegant, if I do say so myself.

Not being content with example code, I’ve made sure these classes are real-world useful. This view is integral to BibTeX management in Scribbler so it needs to be good. Some of the relevant parts of the NSOutlineView API are replicated and the delegate of the TLAnimatingOutlineView gets will/did/expand/collapse notifications, along with allowing the delegate to deny/allow animations, too. Each of the TLCollapsibleViews will also ask their detail views if the collapse/expansion is allowed if the TLCollapsibleDetailView protocol is implemented. The TLAnimatingOutlineView itself works when in an NSScrollView.

The code, along with an example project, is hosted on Google Code here. It is distributed under the new BSD license.

So why is is not ESAnimatingOutlineView, why the TL namespace? Well, I’m working on something big, at the same time as writing Scribbler. Start guessing.

Update

I’ve fixed a few bugs in the code, the current version can be checked out of the repository. Fixes include: proper support for hiding subviews of the TLDisclosureBars when the outline view is resized small, a correction to the autoresizing mask of the TLAnimatingOutlineView itself, declaration of keys that the TLCollapsibleView supplies with the animation dictionaries, the delegate of the outline view and subclasses of TLGradientView are no longer unregistered for notifications that client code specifies (i.e. no longer uses [[NSNotifcationCenter defaultCenter] removeObserver:_delegate];).

Scribbler’s Image Editing Opensourced Part 1

Monday, October 6th, 2008

Well since I no longer work at the NHS I’ve been working really hard on one of the great features of Scribbler, image editing. As something I wanted to put in at the very start, I was really enthusiastic to start using Apple’s Image Kit. It promised developers the chance to do very little work and still have fully-fledged image management, provided by IKImageBrowser (very good) and image editing thanks to IKImageView. Unfortunately, the latter does not live up to the hype. After battling with it for a while and trying to justify to myself that it would suffice for version 1.0 of Scribbler, it was just way too incomplete, buggy, and damn ugly to leave in. I try to work with the idea that if it bugs me, it’s not good enough.

Image Loading

Let me start with IKImageView. It loads images ok, although larger images aren’t loaded nicely: they’re drawn incrementally in a really jarring fashion, even if you pass the view a CGImageRef that’s already in memory.  That being said, it does handle quite a few different types of images, so we give it half a point.

Image Editing

The main function of such a view is not for viewing images, but for editing them within the application. Many users want to quickly change an image they’re working with and then get straight back work.  Taking our cues from iPhoto we guess that this editing will take the form of colour adjustments rotating and cropping. If we’re feeling really generous, then we can also include a straightening tool and some zooming (although the latter is relatively superfluous unless we have some sort of pixel-level editing like iPhoto’s red-eye removal or the touch-up tool).

So we’ve put IKImageView in our app, and loaded an image, we then want to change the brightness and contrast, so we double-click on the image and open up the shared instance of the IKImageEditPanel (left).

  

Compared to iPhoto’s panel (right), this is ridiculously ugly, has no icons, has an Aqua button at the bottom instead of a properly-rendered HUD button.  The biggest issue: it appears off-screen all the freaking time. Can you imagine editing a bunch of images and having to manually drag the panel to where you want it each time you open the damn thing up? No amount of setting the frame of the window or teaching it to autosave it’s position fix this, and even if we could, the first time you run the app it’s at the bottom of the screen with most of it hidden.

Lastly, you just try setting a new image in the view and getting the edit panel to do anything anymore, as far as I can tell -reloadData is a no-op, it neither reloads the data for the panel, nor makes you more attractive. The only way to fix this is to close the panel, and reopen it (and then moving it so you can see the controls).

Rotation

If we want to rotate the image we (in code) select the IKToolModeRotate mode and we get our onscreen compass:

The problem with this is two-fold: click on the image at about 3/4 the way up and the rotation compass is drawn outside the bounds of the view; secondly, the user interaction is all wrong. To rotate you click and hold ok the image and move the mouse around in a manner that’s so hard to control it’s useless. Then think about saving the image after a rotation. The view itself has no concept of a canvas, so we can’t save an angled image on a white background, and we can’t arbitrarily decide to ignore the user’s rotation setting an save an un-rotated image (rock - me - hard place). Basically the tool is broken and doesn’t provide useful functionality to users.

Cropping

Then we have IKToolModeCrop. “Fantastic!” I hear you cry. “No longer will my users have to open up another app to crop an image, and I won’t have to write that code”. You diligently select a region if the image, then call -setCurrentToolMode: and….

Nothing. Nada. Zip.

Reading through the programming guide tells us that Apple have declared the mode and not implemented it (apoordesignersayswhat). So we then have to do it, but can we get the selection rectangle? Of course we can’t! So that nice crop button we made goes to waste, unless you want to subclass, track the mouse, interrupt -drawRect:, apply the crop using core image (note to all that CICrop filter is not a magic bullet), draw a CGImage with the cropped version, then reset the image. No thanks. On it’s own that might be fine, but with all the other rubbish in this class you’re starting to flog a dead horse.

So that’s why IKImageView will *not* be making an appearance in Scribbler, and if you find it in any other apps then… well… I heard that Windows Photo Viewer Deluxe Family Millennium Media Edition is fantastic.