-
Notifications
You must be signed in to change notification settings - Fork 8
Mappings
A HotCocoa mapping defines a structure that sits on top of a particular Objective-C class and simplifies its usage in MacRuby.
This structure is defined in a simple, Ruby-based DSL (domain-specific language).The HotCocoa DSL includes syntax to aid in object instantiation, constant mapping, default options, customized methods, and delegate-method mappings. The only required section is object instantiation; the other four sections are only required if the Objective-C class in question requires it. Once defined, a mapping is registered with the HotCocoa::Mappings
module, using the names of the mapping and the mapped Objective-C class.
The mappings that ship with HotCocoa are found in the 'lib/ruby/1.8/hotcocoa/mappings' directory. You can easily define your own mappings for classes by following the examples below. Place mappings in files of your own, loading them after you load the hotcocoa
library.
The basic syntax for defining a mapping is:
HotCocoa::Mappings.map(
:name => :CocoaClassNameAsSymbol,
:framework => :FrameworkName) do ... end
To create a mapping, call the map
method on HotCocoa::Mappings
, passing in an options hash. The first (and required) option is the :name
parameter. Replace :name
with the desired name of the method you want to be generated and made available to users of HotCocoa. For example, the mapping definition for an NSButton
might be:
HotCocoa::Mapping.map(
:button => :NSButton,
:framework => :Cocoa) do ... end
This creates a method on HotCocoa named #button
, which will return an instance of an NSButton
class. Because the default value for :framework
is :Cocoa
, this option can often be omitted:
HotCocoa::Mapping.map(
:button => :NSButton) do ... end
HotCocoa option values (eg, :NSButton
, :Cocoa
) are encoded as symbols, rather than constants (eg, NSButton
), so that the named class or framework will only be loaded when the method created is ''called''. At that time, HotCocoa will take care of translating each symbol into the corresponding reference.
There are two methods (init_with_options
and alloc_with_options
) that you can implement to support object instantiation. Define these methods within the block that you pass to the #map
method.
HotCocoa::Mapping.map(
:button => :NSButton) do
def init_with_options(button, options)
button.initWithFrame options.delete(:frame)
end
end
As you can see from the method definition above, the init_with_options
method is provided with an instance of the class that you declared in the mapping (NSButton)
which is created with NSButton.alloc
. This implementation just calls the proper init
method. This example calls initWithFrame
and passes the options value for the :frame
. The options hash is passed to this function when you call the #button
method:
button :frame => [0,0,20,100]
Note that the mapping must delete any option that it consumes. Every option used in the construction of the object should be removed from the hash. Any options that are left in the hash after begin processed by the instantiation methods will be dispatched to the NSButton
instance.
The second method you can implement is:
HotCocoa::Mapping.map(:button => :NSButton) do
def alloc_with_options(options)
NSButton.alloc.initWithFrame options.delete(:frame)
end
end
Here you are not provided with the alloc
'd object as the first parameter, but simply the options hash. This is helpful for classes that use class methods for instantiation, such as:
NSImage.imageNamed('my_image')
You should implement either #init_with_options
or #alloc_with_options
, but not both. If alloc_with_options
exists, it will be called and init_with_options
will be ignored.
You can provide a hash of default options in the definition of your mapping. This is very useful for many Cocoa classes, because there are so many options. Defaults are appended to the options hash that is passed into the constructor method if a value of the same key does not exist.
Supplying your defaults is simple. In the example below, if you provide a :frame
, it will be used instead of DefaultEmptyRect
:
HotCocoa::Mapping.map(
:button => :NSButton) do
defaults :bezel => :rounded,
:frame => DefaultEmptyRect,
:layout => {}
end
A few of the defaults shown above are pretty important to UI classes; specifically, :frame
and :layout
. The NSButton
example uses :frame => DefaultEmptyRect
. The DefaultEmptyRect
constant equals a rectangle of [0,0,0,0]
. The :layout =>
part is important for using the layout_view
classes, which are included in HotCocoa.
This default value is an empty hash, but if it's not passed, the default value for the layout is actually nil
. If the layout is nil
, the component is not included when the layout_view
computes the layout for the components. All of the UI mappings that ship with HotCocoa provide an empty hash as a default :layout
.
Because constant names need to be globally unique in Objective-C, they can get very long. What the constant mapping provides in HotCocoa is the ability to use short symbol names and map them to the constant names that are scoped to the wrapped class. This is an example of mapping constants to represent button state:
HotCocoa::Mapping.map(:button => :NSButton) do
constant :state, {
:on => NSOnState,
:off => NSOffState,
:mixed => NSMixedState
}
end
A constant map includes the key (:state
), followed by a hash which maps symbols to actual constants. When you provide options to the constructor method that match a constant key, it looks up the corresponding value in that hash and replaces the value in the option hash with the constant's value.
So, when you call:
button :state => :on
It's replaced with:
button :state => NSOnState
You can have as many constant maps in each class as you need. Constant maps are also inherited by subclasses. A constant map on NSView
is also available on NSControl
and NSButton
(as subclasses).
If you provide a value for a constant key that is an array rather than a single symbol, the constants in the array are OR'd with each other. This is useful when the constants are masked. For NSWindow
's mapping of style:
:style => [:titled,
:closable,
:miniaturizable,
:resizable]
is equivalent to:
:style = NSTitledWindowMask |
NSClosableWindowMask |
NSMiniaturizableWindowMask |
NSResizableWindowMask
Custom methods are simply modules that are included in the instance; they provide idiomatic Ruby methods for the mapped Objective-C class instance. Providing custom methods in your mapping is easy:
HotCocoa::Mapping.map(:button => :NSButton) do
custom_methods do
def bezel=(value)
setBezelStyle(value)
end
def on?
state == NSOnState
end
end
end
In the first method (bezel=
), we provide a better method name than setBezelStyle
. Although we could provide idiomatic Ruby methods for every Objective-C method, the number of these methods is huge. The general principle is to customize where this provides something better, not just syntactically better. Custom methods, like constant mappings, are inherited by subclasses.
In the last example, the bezel=
method serves as a corresponding method name to the constant map for bezel style:
constant :bezel, {
:rounded => NSRoundedBezelStyle,
:regular_square => NSRegularSquareBezelStyle,
:thick_square => NSThickSquareBezelStyle,
:thicker_square => NSThickerSquareBezelStyle,
:disclosure => NSDisclosureBezelStyle,
:shadowless_square => NSShadowlessSquareBezelStyle,
:circular => NSCircularBezelStyle,
:textured_square => NSTexturedSquareBezelStyle,
:help_button => NSHelpButtonBezelStyle,
:small_square => NSSmallSquareBezelStyle,
:textured_rounded => NSTexturedRoundedBezelStyle,
:round_rect => NSRoundRectBezelStyle,
:recessed => NSRecessedBezelStyle,
:rounded_disclosure => NSRoundedDisclosureBezelStyle
}
This way, you can easily create buttons of the provided bezel style:
button :bezel => :circular
If you recall from the default options section (above), you can also include default values that are constant mapped values (eg, :bezel => :rounded
is the default for a button). In this way, constant mappings and custom methods work together to provide a vastly better syntax for using constants in your code.
Delegate method mapping is a little more complex then the prior sections. Delegate methods are used pervasively in Cocoa to facilitate customization of controls. Normally, what you need to do is implement the methods that the control is looking for in a class of your own. You would then set an instance of that class as the delegate of the control, using setDelegate(instance)
.
In HotCocoa, we wanted to enable the use of Ruby blocks for delegate method calls, so the Objective-C code:
class MyDelegate
def windowWillClose(sender)
// perform something
end
end
window.setDelegate(MyDelegate.new)
is simplified to the Ruby code:
window.will_close { # perform something }
To enable this, you map individual delegate methods to a symbol name, then map parameters that are passed to that delegate method to the block parameters. For NSWindow
the definition for delegating windowWillClose
, which passes no parameters to the block, would be:
HotCocoa::Mapping.map(:window => :NSWindow) do
delegating 'windowWillClose:', :to => :will_close
end
This creates a #will_close
method that accepts the block (as above). For the sake of efficiency, it:
- creates an object
- adds the delegating method (
windowWillClose
) as a method on that object's singleton class - stores the passed-in block inside that object
The generated windowWillClose
method calls that block when Cocoa calls the windowWillClose
method. Each time a delegate method is created, the object is set as the delegate (using setDelegate
).
When a delegate needs to forward parameters to the block, the definition becomes a little more complex:
HotCocoa::Mapping.map(
:window => :NSWindow) do
delegating 'window:willPositionSheet:usingRect:',
:to => :will_position_sheet,
:parameters => [:willPositionSheet, :usingRect]
end
The :parameters
list contains the corresponding selector name from the Objective-C selector. Even though the delegate method normally has three parameters (window
, willPositionSheet
, and usingRect
), the block will only be passed the last two. Using this method would look like:
window.will_position_sheet {|sheet, rect| ... }
It's also possible to map a parameter, in cases where you have to invoke a more complex calling on the parameter:
HotCocoa::Mapping.map(:window => :NSWindow do
delegating "windowDidExpose:",
:to => :did_expose,
:parameters => ["windowDidExpose.userInfo['NSExposedRect']"]
end
Here we want to walk the first parameter's userInfo
dictionary, get the NSExposedRect
rectangle, and pass it as a parameter to the did_expose
block. Using this method would look like:
window.did_expose { | rect| ... }
Each method for a delegate has to be mapped with an individual delegating call.