4. Replacing the CustomWindow class
4.1 Rewrite it in Ruby
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.
@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:
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
4.2 More pitfalls and a surprise
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.
- 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
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
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.
rubyconvert’s truncated comment was my fault. I was using the wrong datatype in my comments database. It’s fixed now.
As of today using RubyCocoa 0.11.1 including ns_overrides creates a warning when building, saying they’re no longer necessary.