2. Make windows and views from Ruby

Now we’re going to start moving much faster.

When we approach Cocoa with Interface Builder, we have to do everything stepwise (pardon the pun). A Cocoa tutorial usually involves performing many steps in Interface Builder and occasionally digressing into code, as if code was a bad thing (although Objective-C source code is kind of ugly). But after a while, my short-term memory gets overloaded. “Did I just set that menu action?”, I wonder, then “have I told Interface Builder about my new class?” and “did I remember to connect that button?” This doesn’t just lead to mistakes for the reader; it’s also a frequent source of errors in Cocoa books (as I unhappily experienced with the first one I used).

Also, stepwise presentations of Cocoa sometimes require that important elements of a project appear ahead of their context, which has left me confused more than once, and leafing back and forth through a long sequence of interface building instructions is not happy programming.

But now, because I can present everything in Ruby, I can place much more of our examples in front of you at once. I think you’ll find them surprisingly self-explanatory. So in general, I’ll let the code talk more than me and refer you to the searchable online Cocoa documentation in Xcode. If you want to know about any of the “NS” classes you see here, look there first.

Finally, I’ve noticed that occasionally someone posts to a Cocoa message board asking how they can use Cocoa without Interface Builder. The answer they usually get goes something like this: “Don’t do it. You’ll write too much code, and you’ll probably get it wrong.” I’ll show you a way to do it; you can then decide for yourself.

In the next chapter, we will embed an IRB console in a text view. As a preliminary step, let’s display a text view inside a window. Put the code below in a file called “console.rb”. Save it in your project directory and run your application.

console.rb (preliminary) [ruby]
class ConsoleWindowController < OSX::NSObject
  attr_accessor :window, :textview, :console

  def initWithFrame(frame)
    init
    styleMask = OSX::NSTitledWindowMask + OSX::NSClosableWindowMask +
    OSX::NSMiniaturizableWindowMask + OSX::NSResizableWindowMask
    @window = OSX::NSWindow.alloc.initWithContentRect_styleMask_backing_defer(
    frame, styleMask, OSX::NSBackingStoreBuffered, false)
    @textview = OSX::NSTextView.alloc.initWithFrame(frame)
    @console = nil # to be completed later...
    with @window do |w|
      w.setContentView scrollableView(@textview)
      w.setTitle "RubyCocoa Console"
      w.setDelegate self
      w.center
      w.makeKeyAndOrderFront(self)
    end
    self
  end

  def run
    # to be completed later...
  end

  def windowWillClose(notification)
    OSX::NSApplication.sharedApplication.terminate(self)
  end

  def windowShouldClose(notification)
    @alert = OSX::NSAlert.alloc.init
    with @alert do |a|
      a.setMessageText(
        "Do you really want to close the console?\nYour application will exit.")
      a.setAlertStyle(OSX::NSCriticalAlertStyle)
      a.addButtonWithTitle("OK")
      a.addButtonWithTitle("Cancel")
      a.beginSheetModalForWindow_modalDelegate_didEndSelector_contextInfo(
        window, self, "alertDidEnd:returnCode:contextInfo:", nil)
    end
    false
  end

  def alertDidEnd_returnCode_contextInfo(alert, code, contextInfo)
    window.close if (code == 1000)
  end
end

def with(x)
  yield x if block_given?; x
end if not defined? with

def scrollableView(content)
  scrollview = OSX::NSScrollView.alloc.initWithFrame(content.frame)
  clipview = OSX::NSClipView.alloc.initWithFrame(scrollview.frame)
  scrollview.setContentView(clipview)
  scrollview.setDocumentView(content)
  clipview.setDocumentView(content)
  content.setFrame(clipview.frame)
  scrollview.setHasVerticalScroller(1)
  scrollview.setHasHorizontalScroller(1)
  scrollview.setAutohidesScrollers(1)
  resizingMask = OSX::NSViewWidthSizable + OSX::NSViewHeightSizable
  content.setAutoresizingMask(resizingMask)
  clipview.setAutoresizingMask(resizingMask)
  scrollview.setAutoresizingMask(resizingMask)
  scrollview
end

class ApplicationDelegate < OSX::NSObject
  def applicationDidFinishLaunching(sender)
    $consoleWindowController = ConsoleWindowController.alloc.initWithFrame(
    [30,20,600,300])
    $consoleWindowController.run
  end
end

# set up the application delegate
$delegate = ApplicationDelegate.alloc.init
OSX::NSApplication.sharedApplication.setDelegate($delegate)

Whenever I look at the above code, I can’t help but think “so much functionality, so little code!”

You can probably understand everything in the code above with no further explanation. But if you’d like a guided tour, let’s begin at the end of the file, where we set the application delegate. Cocoa has a special singleton object that represents the currently-running application. You can get it from Ruby by calling OSX::NSApplication.sharedApplication. We get it and set its delegate to an instance of the ApplicationDelegate class that we defined above. That class is responsible for creating our console window when the application has finished launching. That’s how we can create objects on startup without using a nib file.

Our ConsoleWindowController class is a subclass of NSObject (there’s also a special class called NSWindowController, but it’s mainly useful for loading windows defined in nib files). It has an instance variable called @window that we set to the NSWindow object that we create (look up the initialization function if you want to know more about it). Then it creates the NSTextView that we will put in our window using the window’s setContentView method.

Now, perhaps you already know this, but it’s not so easy to put a text view in a window if you want it to scroll. Instead of directly inserting the text view, you have to put it in an NSScrollView and make the scroll view the window’s content view. But you can’t just do that, either. In between the NSTextView and the NSScrollView, you need an NSClipView to help manage things. Interface Builder hides this dirty laundry from us. But I believe I’ve worked out everything that we need to do and put it in a little reusable function that I called scrollableView in the code shown. It takes a view as an argument (typically an NSTextView) and returns a scroll view that encloses it. See the Cocoa documentation for details on individual pieces.

Back in our window’s initialization function, we set a few properties of our window and then display the window on screen. The “with” call is something that I added for appearance only; it’s defined a few lines later in the file.

That’s enough to get our window on the screen. Move it around, resize it, and type enough in it to get it to scroll. You’ll notice that no keyboard shortcuts work (except to quit) because we’re running with a stripped-down menu. We’ll fix that later.

There are a few more functions on our window controller that we haven’t discussed yet. Because we set our controller to be the window’s delegate, we can use these functions to detect significant events involving the window. For example, when the window closes, our windowWillClose method will get called, and it in turn will tell the application to exit by sending it the terminate message. More interestingly, when a person clicks on the window’s close button (the red one in the top left corner), we’ll get a windowShouldClose message that we can use to present the user with an alert sheet for confirmation. That in turn will call alertDidEnd_returnCode_contextInfo when the sheet is closed. Again, the Cocoa documentation will tell you more about this, and again, when have you had so much fun with so few lines of code?

Did you find an error? Is something missing? Post your comment or suggestion below!

Comments (4) post
  1. gvdl Sat Apr 07 21:02:11 +0000 2007

    Using Ruby-1.8.6, RubyCocoa-0.10.1 & Tiger 10.4.9(Intel)

    Easily reproducible. Hit the close window icon and then click on a button watch it explode with the backtrace

    Program received signal EXC_BAD_ACCESS, Could not access memory. Reason: KERN_PROTECTION_FAILURE at address: 0×000003e8 0×90a59378 in objc_msgSend ()

    0 0×90a59378 in objc_msgSend ()

    1 0×0005a9ea in ocid_get_rbobj (ocid=0×3e8) at …/projects/ruby-programming/rubycocoa-0.10.1/framework/src/objc/mdl_osxobjc.m:313

    2 0×00054944 in ocid_to_rbobj (context_obj=4, ocid=0×3e8) at …/projects/ruby-programming/rubycocoa-0.10.1/framework/src/objc/ocdata_conv.m:522

    3 0×00053dc7 in ocdata_to_rbobj (context_obj=4, octype=64, oc

    .../projects/ruby-programming/rubycocoa-0.10.1/framework/src/objc/ocdata_conv.m:211 #4 0×0005760d in -[RBObject fetchForwardArgumentsOf:] (self=0×6a4bb0, _cmd=0×647ec, an_inv=0xc776d30) at …/projects/ruby-programming/rubycocoa-0.10.1/framework/src/objc/RBObject.m:119 [snipped]

    This code causes my ‘console’ to crash a.beginSheetModalForWindow_modalDelegate_didEndSelector_contextInfo( window, self, “alertDidEnd:returnCode:contextInfo:”, nil)

    The seggy on 0×000003e8 is suggestive as this is 1000 decimal, i.e. a valid return code sent to the NSAlert delegate after clicking a button.

    -[RBObject fetchForwardArgumentsOf:] crashes when processing the second argument of alertDidEnd:returnCode:contextInfo: which is an integer but the NSMethodSignature thinks it is an ‘id’. When the ‘id’ is dereferenced a crash occurs.

    Now this points to a fundamental flaw in RubyCocoa. How do you insert valid method signatures into the objc runtime for Ruby’s untyped delegate methods?

  2. gvdl Sat Apr 07 21:17:28 +0000 2007

    More readable backtrace, where f0 means frame 0, etc f0 0×90a59378 in objc_msgSend () f1 0×0005a9ea in ocid_get_rbobj (ocid=0×3e8) at …/projects/ruby-programming/rubycocoa-0.10.1/framework/src/objc/mdl_osxobjc.m:313 f2 0×00054944 in ocid_to_rbobj (context_obj=4, ocid=0×3e8) at …/projects/ruby-programming/rubycocoa-0.10.1/framework/src/objc/ocdata_conv.m:522 f3 0×00053dc7 in ocdata_to_rbobj (context_obj=4, octype=64, oc .../projects/ruby-programming/rubycocoa-0.10.1/framework/src/objc/ocdata_conv.m:211 f4 0×0005760d in -[RBObject fetchForwardArgumentsOf:] (self=0×6a4bb0, _cmd=0×647ec, an_inv=0xc776d30) at …/projects/ruby-programming/rubycocoa-0.10.1/framework/src/objc/RBObject.m:119 [snipped]

    This code causes my ‘console’ to crash a.beginSheetModalForWindow_modalDelegate_didEndSelector_contextInfo( window, self, “alertDidEnd:returnCode:contextInfo:”, nil)

    The seggy on 0×000003e8 is suggestive as this is 1000 decimal, i.e. a valid return code sent to the NSAlert delegate after clicking a button.

    -[RBObject fetchForwardArgumentsOf:] crashes when processing the second argument of alertDidEnd:returnCode:contextInfo: which is an integer but the NSMethodSignature thinks it is an ‘id’. When the ‘id’ is dereferenced a crash occurs.

    Now this points to a fundamental flaw in RubyCocoa. How do you insert valid method signatures into the objc runtime for Ruby’s untyped delegate methods?

  3. gvdl Sat Apr 07 23:28:48 +0000 2007

    Found it, it is a bug in console.rb, doesn’t declare delagation signature as I had surmised earlier, easily fixed with objc_export (btw is there any doc on this stuff?)

    You need to insert objc_export :alertDidEnd_returnCode_contextInfo, [:void,:id,:int,:ulong] before its method definition.

    Yet more documentation problems, how do you post code fragments? Giving up on posting a patch.

  4. DekuDekuplex Wed Jun 27 01:52:03 +0000 2007

    gvdl, what happens when you try to post your patch?

    Without a patch, we’re all left with buggy code and an incomplete tutorial.