Friday, January 18, 2008

Animating NSViews In RubyCocoa

Yesterday we talked about how to make nifty selectable toolbars like this:

selectable-toolbar.png

Now let's look at the finishing touch for our Preferences window; Animating the panel changes. We'll be flying through this at a pretty good clip, but don't worry. I'll provide the full source for your inspection.

First off, let's add some new outlets to our window controller:

  ib_outlets :generalPrefsView,
             :advancedPrefsView

CustomView.png

Now in Interface Builder, we'll create the views for each preference pane by dragging Custom Views from the Library onto our Preferences.nib.

Note: Be sure to drop the Custom Views on the main nib window in IB, not on the Preferences NSWindow. Your project (in IB) should look something like this:

Prefs-IB.png

Hook up the outlets to the new views, and edit your preference panels to your heart's desire. From here, we go back to the code.

Tip: Be sure to set the auto-sizing on your preference panels (the NSViews) so that it matches the NSWindow's contentView.

Picture 5.png

Next up are some helper methods for our window controller. I won't spend too much time explaining these, but they're pretty straight forward.

  def viewForTag(tag)
    case tag
      when 0: [@generalPrefsView,  "General"]
      when 1: [@advancedPrefsView, "Advanced"]
    end
  end

#viewForTag actually returns our NSView and a title string.

  def newFrameForNewContentView(view)
    newFrameRect = window.frameRectForContentRect(view.frame)
    oldFrameRect = window.frame
    newSize = newFrameRect.size
    oldSize = oldFrameRect.size
    frame = window.frame
    frame.size = newSize
    frame.origin.y = frame.origin.y - (newSize.height - oldSize.height)
    frame
  end

#newFrameForNewContentView calculates the new frame rectangle for the window based on the new view (preference pane).

Now we're ready to fill out our selectPrefPanel action:

  ib_action :selectPrefPanel do |sender|
    tag =  sender.tag
    view, title = self.viewForTag(tag)
    previousView, prevTitle = self.viewForTag(@currentViewTag)
    @currentViewTag = tag
    newFrame = self.newFrameForNewContentView(view)
    window.title = "#{title} Preferences"
    # Using an animation grouping because we may be changing the duration
    NSAnimationContext.beginGrouping
      # Call the animator instead of the view / window directly
      window.contentView.animator.replaceSubview_with(previousView, view)
      window.animator.setFrame_display newFrame, true
    NSAnimationContext.endGrouping
  end

Right on! Now we setup the initial pane when the window loads:

  def awakeFromNib
    window.setContentSize @generalPrefsView.frame.size 
    window.contentView.addSubview @generalPrefsView
    window.title = "General Preferences"
    @currentViewTag = 0
    # Will use CoreAnimation for the panel changes:
    window.contentView.wantsLayer = true
  end

That pretty much does it. Now you have a professional looking preferences window. So enough of those dang blasted NSTabViews!

Here's the completed PreferencesController.rb. Or, you can download the full Xcode project. (Requires Leopard, Xcode 3, and Interface Builder 3)

Happy coding!

4 comments:

  1. Hi Matt,
    Thanks for a great tutorial. Have you noticed that if you have an NSPopUpButton in one of the views then it renders really badly? Is there something that can be fixed in the code to stop that happening?
    Thanks,
    Jon

    ReplyDelete
  2. [Jonathan Dann:1] Renders badly? Sorry, I’m not quite sure what you mean… When it’s static, or as it’s moving between panels?

    ReplyDelete
  3. would be nice to have it translated to obj-c :)

    ReplyDelete
  4. My attempt:
    http://pastie.org/283115

    ReplyDelete