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 collapse

SET =

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

Returns:

  • (String)
'set'

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(klass) ⇒ Mapper

Returns a new instance of Mapper.

Parameters:

  • klass (Class)

    the class that is being mapped



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

def initialize klass
  @control_class = klass
end

Class Attribute Details

.bindings_modulesHash{Symbol=>Module} (readonly)

Cached bindings modules.

Returns:

  • (Hash{Symbol=>Module})


31
32
33
# File 'lib/hotcocoa/mapper.rb', line 31

def bindings_modules
  @bindings_modules
end

.delegate_modulesHash{Symbol=>Module} (readonly)

Cached delegate modules.

Returns:

  • (Hash{Symbol=>Module})


37
38
39
# File 'lib/hotcocoa/mapper.rb', line 37

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)

Returns:

  • (Symbol)


61
62
63
# File 'lib/hotcocoa/mapper.rb', line 61

def builder_method
  @builder_method
end

#control_classClass (readonly)

Returns:

  • (Class)


52
53
54
# File 'lib/hotcocoa/mapper.rb', line 52

def control_class
  @control_class
end

#control_moduleClass (readonly)

Singleton class for the mapper instance

Returns:

  • (Class)


67
68
69
# File 'lib/hotcocoa/mapper.rb', line 67

def control_module
  @control_module
end

#map_bindingsBoolean

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

Returns:

  • (Boolean)


74
75
76
# File 'lib/hotcocoa/mapper.rb', line 74

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.

Parameters:

  • klass (Class)

Returns:

  • (nil)

    do not count on a return value from this method



12
13
14
# File 'lib/hotcocoa/mapper.rb', line 12

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.

Parameters:

  • klass (Class)
  • builder_method (Symbol)

Returns:



23
24
25
# File 'lib/hotcocoa/mapper.rb', line 23

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.

Returns:

  • (Module)

    the generated bindings module



309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/hotcocoa/mapper.rb', line 309

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.

Parameters:

  • control


225
226
227
228
229
230
231
# File 'lib/hotcocoa/mapper.rb', line 225

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

Returns do not count on a return value.

Returns:

  • (nil)

    do not count on a return value



289
290
291
292
293
294
# File 'lib/hotcocoa/mapper.rb', line 289

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.



236
237
238
# File 'lib/hotcocoa/mapper.rb', line 236

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.

Returns:

  • (Module)

    the generated delegate module



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/hotcocoa/mapper.rb', line 252

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

#each_control_ancestor {|a| ... } ⇒ Object

Iterates over the ancestor chain for the class being mapped and yields for each ancestor that also has a mapping.

Classes are yielded from the descending order (from the super class to the sub class).

Yields:

Yield Parameters:

  • a (Class)

    class in the inheritance chain



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

def each_control_ancestor
  control_class.ancestors.reverse.each do |ancestor|
    HotCocoa::Mappings.mappings.values.each do |mapper|
      yield mapper if mapper.control_class == ancestor
    end
  end
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.



85
86
87
88
# File 'lib/hotcocoa/mapper.rb', line 85

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.

Returns:

  • (Hash{Hash})


171
172
173
174
175
176
177
# File 'lib/hotcocoa/mapper.rb', line 171

def inherited_constants
  constants = {}
  each_control_ancestor 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.

Returns:

  • (Array<Module>)


192
193
194
195
196
197
198
199
200
# File 'lib/hotcocoa/mapper.rb', line 192

def inherited_custom_methods
  methods = []
  each_control_ancestor do |ancestor|
    if ancestor.control_module.custom_methods
      methods << ancestor.control_module.custom_methods
    end
  end
  methods
end

#inherited_delegate_methodsObject



179
180
181
182
183
184
185
# File 'lib/hotcocoa/mapper.rb', line 179

def inherited_delegate_methods
  delegate_methods = {}
  each_control_ancestor do |ancestor|
    delegate_methods.merge! ancestor.control_module.delegate_map
  end
  delegate_methods
end

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

Create the mapping method named builder_method.

Parameters:

  • builder_method (Symbol)

Yields:

Returns:



96
97
98
99
100
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
# File 'lib/hotcocoa/mapper.rb', line 96

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.

Parameters:

  • tags (Hash)

Returns:

  • (Hash)


345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
# File 'lib/hotcocoa/mapper.rb', line 345

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