4. Replacing the CustomWindow class

Now let’s convert the CustomWindow class. It initializes our window and handles mouse events. The Objective-C source code is below. For brevity, I’ve cut out most of the comments; you can find them in the original Apple source.

Original CustomWindow source code [Objective-C]
@interface CustomWindow : NSWindow
{
    NSPoint initialLocation;
}
@end

@implementation CustomWindow
- (id)initWithContentRect:(NSRect)contentRect styleMask:(unsigned int)aStyle 
            backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag {
    NSWindow* result = [super initWithContentRect:contentRect styleMask:NSBorderlessWindowMask 
            backing:NSBackingStoreBuffered defer:NO];
    [result setBackgroundColor: [NSColor clearColor]];
    [result setLevel: NSStatusWindowLevel];
    [result setAlphaValue:1.0];
    [result setOpaque:NO];
    [result setHasShadow: YES];
    return result;
}

- (BOOL) canBecomeKeyWindow
{
    return YES;
}

- (void)mouseDragged:(NSEvent *)theEvent
{
    NSPoint currentLocation;
    NSPoint newOrigin;
    NSRect  screenFrame = [[NSScreen mainScreen] frame];
    NSRect  windowFrame = [self frame];
    currentLocation = [self convertBaseToScreen:[self mouseLocationOutsideOfEventStream]];
    newOrigin.x = currentLocation.x - initialLocation.x;
    newOrigin.y = currentLocation.y - initialLocation.y;
    if( (newOrigin.y+windowFrame.size.height) > (screenFrame.origin.y+screenFrame.size.height) ){
        newOrigin.y=screenFrame.origin.y + (screenFrame.size.height-windowFrame.size.height);
    }
    [self setFrameOrigin:newOrigin];
}

- (void)mouseDown:(NSEvent *)theEvent
{    
    NSRect  windowFrame = [self frame];
    initialLocation = [self convertBaseToScreen:[theEvent locationInWindow]];
    initialLocation.x -= windowFrame.origin.x;
    initialLocation.y -= windowFrame.origin.y;
}

Try to make the conversion to Ruby yourself. It’s fairly straightforward. For reference, here is the version I made:

CustomWindow [ruby]
class CustomWindow < OSX::NSWindow
     attr_accessor :initialLocation

     ns_overrides :initWithContentRect_styleMask_backing_defer_
     def initWithContentRect_styleMask_backing_defer(contentRect, aStyle, bufferingType, flag)
        result = super_initWithContentRect_styleMask_backing_defer_(
            contentRect, OSX::NSBorderlessWindowMask, OSX::NSBackingStoreBuffered, false)
        result.setBackgroundColor(OSX::NSColor.clearColor)
        result.setLevel(OSX::NSStatusWindowLevel)
        result.setAlphaValue(1.0)
        result.setOpaque(false)
        result.setHasShadow(true)
        result
    end

    ns_overrides :canBecomeKeyWindow
    def canBecomeKeyWindow
        true
    end
    
    ns_overrides :mouseDragged_
    def mouseDragged(theEvent)
        screenFrame = OSX::NSScreen.mainScreen.frame
        windowFrame = self.frame
        currentLocation = self.convertBaseToScreen(self.mouseLocationOutsideOfEventStream)
        newOrigin = OSX::NSPoint.new(currentLocation.x - @initialLocation.x, currentLocation.y - @initialLocation.y)
        # Don't let the window get dragged up under the menu bar
        if((newOrigin.y + windowFrame.size.height) > (screenFrame.origin.y + screenFrame.size.height))
            newOrigin.y = screenFrame.origin.y + (screenFrame.size.height - windowFrame.size.height)
        end
        self.setFrameOrigin(newOrigin)
    end
    
    ns_overrides :mouseDown_
    def mouseDown(theEvent)
        windowFrame = frame
        @initialLocation = convertBaseToScreen(theEvent.locationInWindow);
        @initialLocation.x -= windowFrame.origin.x;
        @initialLocation.y -= windowFrame.origin.y;
    end
end

By now you’ve hopefully built and tested the next stage of your conversion. Here are a couple of tips based on mistakes I’ve made in the past:

  • Be sure to use ns_overrides to tell RubyCocoa that you’ve overridden each of the four methods in your new class. If you forget it, RubyCocoa will silently ignore your methods. That’s not good, and there’s some ongoing discussion of ways to fix this in the RubyCocoa bridge. In general, it’s a good idea to put this declaration above every Cocoa method that you implement in a Ruby class, and then remove the ones that trigger error messages like these:
/Library/Frameworks/RubyCocoa.framework/Versions/A/Resources/ruby/osx/objc/oc_import.rb:77:in `objc_derived_class_method_add': could not add 'changeTransparency' to class 'Controller': Objective-C cannot find it in the superclass (RuntimeError)
    from /Library/Frameworks/RubyCocoa.framework/Versions/A/Resources/ruby/osx/objc/oc_import.rb:77:in `ns_overrides'
    from /Library/Frameworks/RubyCocoa.framework/Versions/A/Resources/ruby/osx/objc/oc_import.rb:75:in `ns_overrides'
As you can see, RubyCocoa throws an exception for any unnecessary ns_overrides statement in your code. Again, this can probably be handled more robustly; watch for future improvements.
Update (04 Aug 2006): An improvement is in the works to make ns_overrides unnecessary. The latest source code in CVS uses Ruby’s method_added hooks to detect when Ruby methods are added that override methods defined in Objective-C. When they are, it automatically performs an ns_overrides; the result is that you don’t have to do it. Currently this is only available in CVS.
  • Watch out for the difference between . and :: when you are referring to things in the OSX module. You should use . to refer to functions in the module and :: to refer to constants (including classes).

If you encounter other problems in your translation, others have probably had the same difficulties—and will again, unless we can help with improved documentation and bridge code. Please mail your questions and problems to the rubycocoa-talk list.

Finally, I was surprised to discover that my Ruby replacement classes were used even when I kept my Objective-C versions in the source code. I was planning to write a warning here to tell you to be sure to comment out the Objective-C classes you replace—but I tested and discovered that even if you forget to do that, your Ruby classes will still be used. This makes it easier to test and debug your rubified application: if you keep the Objective-C classes, then when testing a new Ruby class, you can easily drop back to the Objective-C version by temporarily renaming the Ruby class. But in practice, you should still always strip out the Objective-C classes you are replacing. When you do so, you’ll discover all the remaining places in your Objective-C code that refer to these classes by name, and if you plan to keep that code in Objective-C, you’ll have to rewrite it to get your classes dynamically using NSClassFromString.

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

Comments (4) post
  1. rubyconvert(at)nospam-yahoo(dot)com Fri Aug 04 07:45:54 +0000 2006

    I’ve been following along, and I can’t provoke any error messages by removing the “ns_overrides …”. Has the bridge been updated? Further, the application behavior seems to be unchanged, transparency, shape changing and window dragging all work as in the

  2. Tim Fri Aug 04 08:42:25 +0000 2006

    You are right. Some improvements have been checked into CVS that make ns_overrides unnecessary. I’ve added a note about that to the text above. Thanks for pointing that out.

  3. Tim Sun Aug 06 19:03:19 +0000 2006

    rubyconvert’s truncated comment was my fault. I was using the wrong datatype in my comments database. It’s fixed now.

  4. Derick Thu Jun 14 22:43:58 +0000 2007

    As of today using RubyCocoa 0.11.1 including ns_overrides creates a warning when building, saying they’re no longer necessary.