Class: HotCocoa::Mappings::Mapper

Inherits:
Object
  • Object
show all
Defined in:
lib/hotcocoa/mapper.rb

Overview

Does most of the heavy lifiting when it comes to HotCocoa mappings.

Constant Summary

SET =

Performance hack. Put mutable objects that are constant into a constant to avoid having to #dup.

'set'

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(klass) ⇒ Mapper



81
82
83
# File 'lib/hotcocoa/mapper.rb', line 81

def initialize klass
  @control_class = klass
end

Class Attribute Details

.bindings_modulesHash{Symbol=>Module} (readonly)

Cached bindings modules.



35
36
37
# File 'lib/hotcocoa/mapper.rb', line 35

def bindings_modules
  @bindings_modules
end

.delegate_modulesHash{Symbol=>Module} (readonly)

Cached delegate modules.



41
42
43
# File 'lib/hotcocoa/mapper.rb', line 41

def delegate_modules
  @delegate_modules
end

Instance Attribute Details

#builder_methodSymbol (readonly)

TODO:

We do not use the cached builder_method attribute unless you count tests. So maybe we should get rid of it?

The name which the mapping goes by (e.g. :window for NSWindow)



65
66
67
# File 'lib/hotcocoa/mapper.rb', line 65

def builder_method
  @builder_method
end

#control_classClass (readonly)



56
57
58
# File 'lib/hotcocoa/mapper.rb', line 56

def control_class
  @control_class
end

#control_moduleClass (readonly)

Singleton class for the mapper instance



71
72
73
# File 'lib/hotcocoa/mapper.rb', line 71

def control_module
  @control_module
end

#map_bindingsBoolean

Whether or not bindings should be mapped for an instance of the mapped class.



78
79
80
# File 'lib/hotcocoa/mapper.rb', line 78

def map_bindings
  @map_bindings
end

Class Method Details

.map_class(klass) ⇒ nil

Add mappings to a class so instances of the class can benefit from HotCocoa features. Usually called by Behaviors.included.



16
17
18
# File 'lib/hotcocoa/mapper.rb', line 16

def map_class klass
  new(klass).include_in_class
end

.map_instances_of(klass, builder_method, &block) ⇒ HotCocoa::Mappings::Mapper

Create a mapper for the given klass and assign it to the given builder_method.



27
28
29
# File 'lib/hotcocoa/mapper.rb', line 27

def map_instances_of klass, builder_method, &block
  new(klass).map_method(builder_method, &block)
end

Instance Method Details

#bindings_module_for_control(control) ⇒ Module

Create a module to hold all bindings setters. The bindings module is meant to assist with setting up Cocoa Bindings by providing a simplified and more Ruby-ish interface.

Read more about Key-Value Binding.

If the control has no exposed bindings, then an empty module will be generated.

In either case, once a module is generated, it is cached for later use.



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/hotcocoa/mapper.rb', line 295

def bindings_module_for_control control
  bindings_module = HotCocoa::Mappings::Mapper.bindings_modules[control_class]
  return bindings_module if bindings_module

  instance = if control == control_class
               control_class.alloc.init
             else
               control
             end

  bindings_module = Module.new
  instance.exposedBindings.each do |exposed_binding|
    p = Proc.new do |value|
      if value.kind_of? Hash
        options = value.delete :options
        bind "#{exposed_binding}", toObject: value.keys.first,
                                withKeyPath: value.values.first,
                                    options: options
      else
        send "set#{exposed_binding.camel_case}", value
      end
    end
    bindings_module.send :define_method, "#{exposed_binding.underscore}=", p
  end

  HotCocoa::Mappings::Mapper.bindings_modules[control_class] = bindings_module
end

#customize(control) ⇒ Object

Apply customizations to defined in a mapping to the control. The control is either an instance of the class or the class itself, depending on how things were setup.



211
212
213
214
215
216
217
# File 'lib/hotcocoa/mapper.rb', line 211

def customize control
  inherited_custom_methods.each do |custom_methods|
    control.send @extension_method, custom_methods
  end
  decorate_with_delegate_methods control
  decorate_with_bindings_methods control
end

#decorate_with_bindings_methods(control) ⇒ nil



275
276
277
278
279
280
# File 'lib/hotcocoa/mapper.rb', line 275

def decorate_with_bindings_methods control
  return if control_class == NSApplication
  if @map_bindings
    control.send @extension_method, bindings_module_for_control(control)
  end
end

#decorate_with_delegate_methods(control) ⇒ Object

Add the delegate method hooks. For #include they become instance methods and for #extend they become singleton methods.



222
223
224
# File 'lib/hotcocoa/mapper.rb', line 222

def decorate_with_delegate_methods control
  control.send @extension_method, delegate_module_for_control_class
end

#delegate_module_for_control_classModule

Create a module to hold the delegate object. The module can then be mixed in so that a control instance can use HotCocoa style delegation.

The style of delegation that HotCocoa supports works by creating an Object instance and then defining delegate methods as singleton methods on that object. Then the object is set to be the delegate of the control.

The generated module is cached for later reuse.



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/hotcocoa/mapper.rb', line 238

def delegate_module_for_control_class
  delegate_module = HotCocoa::Mappings::Mapper.delegate_modules[control_class]
  return delegate_module if delegate_module

  delegate_module  = Module.new
  required_methods = []
  delegate_methods = inherited_delegate_methods

  if delegate_methods.size > 0
    delegate_methods.each do |delegate_method, mapping|
      required_methods << delegate_method if mapping[:required]
    end

    delegate_methods.each do |delegate_method, mapping|
      parameters = mapping[:parameters] ? mapping[:parameters] : []

      # kind of a hack, giving a block directly to define_method is not working
      # for some odd reason, possibly a bug in MacRuby
      callback = Proc.new do |&block|
        raise 'Must pass in a block to use this delegate method' unless block

        @_delegate_builder ||= HotCocoa::DelegateBuilder.new(self, required_methods)
        @_delegate_builder.add_delegated_method(block, delegate_method, *parameters)
      end
      delegate_module.send :define_method, mapping[:to], callback
    end

    delegate_module.send :define_method, :delegate_to do |object|
      @_delegate_builder ||= HotCocoa::DelegateBuilder.new(self, required_methods)
      @_delegate_builder.delegate_to(object, *delegate_methods.values.map { |method| method[:to].to_sym })
    end
  end

  HotCocoa::Mappings::Mapper.delegate_modules[control_class] = delegate_module
end

#include_in_classObject

Add HotCocoa features to a class. The control_class that the mapper was initialized with will receive features for all ancestors that have mappings.



89
90
91
92
# File 'lib/hotcocoa/mapper.rb', line 89

def include_in_class
  @extension_method = :include
  customize @control_class
end

#inherited_constantsHash{Hash}

Returns a hash of constant hashes that were inherited from ancestors that have also been mapped.



176
177
178
179
180
181
182
# File 'lib/hotcocoa/mapper.rb', line 176

def inherited_constants
  constants = {}
  control_class.hotcocoa_mappers.each do |ancestor|
    constants.merge! ancestor.control_module.constants_map
  end
  constants
end

#inherited_custom_methodsArray<Module>

Return the custom_methods module for the class we are instantiating, as well as all of its ancestors.



197
198
199
200
201
202
203
# File 'lib/hotcocoa/mapper.rb', line 197

def inherited_custom_methods
  control_class.hotcocoa_mappers.map! { |ancestor|
    if ancestor.control_module.custom_methods
      ancestor.control_module.custom_methods
    end
  }.compact
end

#inherited_delegate_methodsObject



184
185
186
187
188
189
190
# File 'lib/hotcocoa/mapper.rb', line 184

def inherited_delegate_methods
  delegate_methods = {}
  control_class.hotcocoa_mappers.each do |ancestor|
    delegate_methods.merge! ancestor.control_module.delegate_map
  end
  delegate_methods
end

#map_method(builder_method) { ... } ⇒ HotCocoa::Mappings::Mapper

Create the constructor named builder_method for the HotCocoa module.

Yields:



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/hotcocoa/mapper.rb', line 101

def map_method builder_method, &block
  @extension_method = :extend
  @builder_method   = builder_method

  # @todo use self.singleton_class instead (not implemented in MacRuby yet)
  mod = (class << self; self; end)
  mod.extend HotCocoa::MappingMethods
  mod.module_eval &block

  @control_module = mod
  # put self in a variable, because context of self changes inside the define_method block
  inst = self
  HotCocoa.send :define_method, builder_method do |args = {}, &control_block|
    map = inst.remap_constants args

    inst.map_bindings = map.delete :map_bindings
    default_empty_rect_used = (CGRectZero == map[:frame])

    control = if inst.respond_to? :init_with_options
                inst.init_with_options(inst.control_class.alloc, map)
              else
                inst.alloc_with_options(map)
              end

    inst.customize control

    map.each do |key, value|
      if control.respond_to? "#{key}="
        control.send "#{key}=", value

      elsif control.respond_to? key
        new_key = (key.start_with?(SET) ? key : "set#{key[0].capitalize}#{key[1..-1]}")
        if control.respond_to? new_key
          control.send new_key, value

        else
          control.send key

        end
      elsif control.respond_to? "set#{key.camel_case}"
        control.send "set#{key.camel_case}", value

      else
        NSLog("Unable to map #{key} as a method")

      end
    end

    if default_empty_rect_used
      control.sizeToFit if control.respondsToSelector :sizeToFit
    end

    if control_block
      if inst.respond_to? :handle_block
        inst.handle_block control, &control_block
      else
        control_block.call control
      end
    end

    control
  end

  # make the function callable using HotCocoa.xxxx
  HotCocoa.send :module_function, builder_method
  # module_function makes the instance method private, but we want it to stay public
  HotCocoa.send :public, builder_method
  self
end

#remap_constants(tags) ⇒ Hash

Takes a hash and processes symbols, if the symbol is a mapped constant then it will be swapped with the value of the constant.

This is how constant mappings are used in Hot Cocoa.



331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
# File 'lib/hotcocoa/mapper.rb', line 331

def remap_constants tags
  constants = inherited_constants
  if control_module.defaults
    control_module.defaults.each do |key, value|
      tags[key] = value unless tags.has_key? key
    end
  end

  result = {}
  tags.each do |tag, value|
    if constants[tag]
      result[tag] = value.kind_of?(Array) ?
        value.inject(0) { |a, i| a|constants[tag][i] } :
        constants[tag][value]
    else
      result[tag] = value
    end
  end
  result
end