PageRenderTime 49ms CodeModel.GetById 15ms app.highlight 15ms RepoModel.GetById 2ms app.codeStats 0ms

/AppKit/CPKeyValueBinding.j

http://github.com/cacaodev/cappuccino
Unknown | 958 lines | 756 code | 202 blank | 0 comment | 0 complexity | 5c9d2d4bfa694b937b7ab070096891b7 MD5 | raw file
  1/*
  2 * CPKeyValueBinding.j
  3 * AppKit
  4 *
  5 * Created by Ross Boucher 1/13/09
  6 * Copyright 280 North, Inc.
  7 *
  8 * Adapted from GNUStep
  9 * Released under the LGPL.
 10 *
 11 * This library is free software; you can redistribute it and/or
 12 * modify it under the terms of the GNU Lesser General Public
 13 * License as published by the Free Software Foundation; either
 14 * version 2.1 of the License, or (at your option) any later version.
 15 *
 16 * This library is distributed in the hope that it will be useful,
 17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 19 * Lesser General Public License for more details.
 20 *
 21 * You should have received a copy of the GNU Lesser General Public
 22 * License along with this library; if not, write to the Free Software
 23 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 24 */
 25
 26@import <Foundation/CPObject.j>
 27@import <Foundation/CPArray.j>
 28@import <Foundation/CPDictionary.j>
 29@import <Foundation/CPInvocation.j>
 30@import <Foundation/CPValueTransformer.j>
 31@import <Foundation/CPKeyValueObserving.j>
 32
 33@class CPButton
 34
 35var exposedBindingsMap = @{},
 36    bindingsMap = @{};
 37
 38@typedef CPBindingOperationKind
 39var CPBindingOperationAnd = 0,
 40    CPBindingOperationOr  = 1;
 41
 42@implementation CPBinder : CPObject
 43{
 44    CPDictionary    _info;
 45    id              _source @accessors(getter=source);
 46
 47    JSObject        _suppressedNotifications;
 48    JSObject        _placeholderForMarker;
 49}
 50
 51+ (void)exposeBinding:(CPString)aBinding forClass:(Class)aClass
 52{
 53    var bindings = [exposedBindingsMap objectForKey:[aClass UID]];
 54
 55    if (!bindings)
 56    {
 57        bindings = [];
 58        [exposedBindingsMap setObject:bindings forKey:[aClass UID]];
 59    }
 60
 61    bindings.push(aBinding);
 62}
 63
 64+ (CPArray)exposedBindingsForClass:(Class)aClass
 65{
 66    return [[exposedBindingsMap objectForKey:[aClass UID]] copy];
 67}
 68
 69+ (CPBinder)getBinding:(CPString)aBinding forObject:(id)anObject
 70{
 71    return [[bindingsMap objectForKey:[anObject UID]] objectForKey:aBinding];
 72}
 73
 74+ (void)_reverseSetValueFromExclusiveBinderForObject:(id)anObject
 75{
 76    var bindersByBindingName = [bindingsMap objectForKey:[anObject UID]];
 77
 78    [bindersByBindingName enumerateKeysAndObjectsUsingBlock:function(binding, binder, stop)
 79    {
 80        if ([binder isKindOfClass:[self class]])
 81        {
 82            [binder reverseSetValueFor:binding];
 83            stop(YES);
 84        }
 85    }];
 86}
 87
 88+ (CPDictionary)infoForBinding:(CPString)aBinding forObject:(id)anObject
 89{
 90    var theBinding = [self getBinding:aBinding forObject:anObject];
 91
 92    if (theBinding)
 93        return theBinding._info;
 94
 95    return nil;
 96}
 97
 98+ (CPDictionary)allBindingsForObject:(id)anObject
 99{
100    return [bindingsMap objectForKey:[anObject UID]];
101}
102
103+ (void)unbind:(CPString)aBinding forObject:(id)anObject
104{
105    var bindings = [bindingsMap objectForKey:[anObject UID]];
106
107    if (!bindings)
108        return;
109
110    var theBinding = [bindings objectForKey:aBinding];
111
112    if (!theBinding)
113        return;
114
115    var info = theBinding._info,
116        observedObject = [info objectForKey:CPObservedObjectKey],
117        keyPath = [info objectForKey:CPObservedKeyPathKey];
118
119    [observedObject removeObserver:theBinding forKeyPath:keyPath];
120    [bindings removeObjectForKey:aBinding];
121}
122
123+ (void)unbindAllForObject:(id)anObject
124{
125    var bindings = [bindingsMap objectForKey:[anObject UID]];
126
127    if (!bindings)
128        return;
129
130    var allKeys = [bindings allKeys],
131        count = allKeys.length;
132
133    while (count--)
134        [anObject unbind:allKeys[count]];
135
136    [bindingsMap removeObjectForKey:[anObject UID]];
137}
138
139- (id)initWithBinding:(CPString)aBinding name:(CPString)aName to:(id)aDestination keyPath:(CPString)aKeyPath options:(CPDictionary)options from:(id)aSource
140{
141    // We use [self init] here because subclasses override init. We can't override this method
142    // because their initialization has to occur before this method in this class is executed.
143    self = [self init];
144
145    if (self)
146    {
147        _source = aSource;
148        _info = @{
149                CPObservedObjectKey: aDestination,
150                CPObservedKeyPathKey: aKeyPath,
151            };
152        _suppressedNotifications = {};
153        _placeholderForMarker = {};
154
155        if (options)
156            [_info setObject:options forKey:CPOptionsKey];
157
158        [self _updatePlaceholdersWithOptions:options forBinding:aName];
159
160        [aDestination addObserver:self forKeyPath:aKeyPath options:0 context:aBinding];
161
162        var bindings = [bindingsMap objectForKey:[_source UID]];
163
164        if (!bindings)
165        {
166            bindings = @{};
167            [bindingsMap setObject:bindings forKey:[_source UID]];
168        }
169
170        [bindings setObject:self forKey:aName];
171        [self setValueFor:aBinding];
172    }
173
174    return self;
175}
176
177+ (BOOL)isBindingAllowed:(CPString)aBinding forObject:(id)anObject
178{
179    if ([[anObject class] isBindingExclusive:aBinding])
180    {
181        var bindingsForObject = [bindingsMap objectForKey:[anObject UID]],
182            allBindings = [bindingsForObject allKeys],
183            count = [allBindings count];
184
185        while(count--)
186        {
187            if ([[anObject class] isBindingExclusive:allBindings[count]])
188                return NO;
189        }
190    }
191
192    return YES;
193}
194
195- (void)raiseIfNotApplicable:(id)aValue forKeyPath:(CPString)keyPath options:(CPDictionary)options
196{
197    if (aValue === CPNotApplicableMarker && [options objectForKey:CPRaisesForNotApplicableKeysBindingOption])
198    {
199       [CPException raise:CPGenericException
200                   reason:@"Cannot transform non-applicable key on: " + _source + " key path: " + keyPath + " value: " + aValue];
201    }
202}
203
204- (void)setValueFor:(CPString)theBinding
205{
206    var destination = [_info objectForKey:CPObservedObjectKey],
207        keyPath = [_info objectForKey:CPObservedKeyPathKey],
208        options = [_info objectForKey:CPOptionsKey],
209        newValue = [destination valueForKeyPath:keyPath];
210
211    if (CPIsControllerMarker(newValue))
212    {
213        [self raiseIfNotApplicable:newValue forKeyPath:keyPath options:options];
214
215        var value = [self _placeholderForMarker:newValue];
216        [self setPlaceholderValue:value withMarker:newValue forBinding:theBinding];
217    }
218    else
219    {
220        var value = [self transformValue:newValue withOptions:options];
221        [self setValue:value forBinding:theBinding];
222    }
223}
224
225- (void)setPlaceholderValue:(id)aValue withMarker:(CPString)aMarker forBinding:(CPString)aBinding
226{
227    [_source setValue:aValue forKey:aBinding];
228}
229
230- (void)setValue:(id)aValue forBinding:(CPString)aBinding
231{
232    [_source setValue:aValue forKey:aBinding];
233}
234
235- (void)reverseSetValueFor:(CPString)aBinding
236{
237    var destination = [_info objectForKey:CPObservedObjectKey],
238        keyPath = [_info objectForKey:CPObservedKeyPathKey],
239        options = [_info objectForKey:CPOptionsKey],
240        newValue = [self valueForBinding:aBinding];
241
242    newValue = [self reverseTransformValue:newValue withOptions:options];
243
244    [self suppressSpecificNotificationFromObject:destination keyPath:keyPath];
245    [destination setValue:newValue forKeyPath:keyPath];
246    [self unsuppressSpecificNotificationFromObject:destination keyPath:keyPath];
247}
248
249- (id)valueForBinding:(CPString)aBinding
250{
251    return [_source valueForKeyPath:aBinding];
252}
253
254- (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)changes context:(id)context
255{
256    if (!changes)
257        return;
258
259    var objectSuppressions = _suppressedNotifications[[anObject UID]];
260
261    if (objectSuppressions && objectSuppressions[aKeyPath])
262        return;
263
264    [self setValueFor:context];
265}
266
267- (id)transformValue:(id)aValue withOptions:(CPDictionary)options
268{
269    var valueTransformerName = [options objectForKey:CPValueTransformerNameBindingOption],
270        valueTransformer;
271
272    if (valueTransformerName)
273    {
274        valueTransformer = [CPValueTransformer valueTransformerForName:valueTransformerName];
275
276        if (!valueTransformer)
277        {
278            var valueTransformerClass = CPClassFromString(valueTransformerName);
279
280            if (valueTransformerClass)
281            {
282                valueTransformer = [[valueTransformerClass alloc] init];
283                [valueTransformerClass setValueTransformer:valueTransformer forName:valueTransformerName];
284            }
285        }
286    }
287    else
288        valueTransformer = [options objectForKey:CPValueTransformerBindingOption];
289
290    if (valueTransformer)
291        aValue = [valueTransformer transformedValue:aValue];
292
293    // If the value is nil AND the source doesn't respond to setPlaceholderString: then
294    // we set the value to the placeholder. Otherwise, we do not want to short cut the process
295    // of setting the placeholder that is based on the fact that the value is nil.
296    if ((aValue === undefined || aValue === nil || aValue === [CPNull null])
297        && ![_source respondsToSelector:@selector(setPlaceholderString:)])
298        aValue = [options objectForKey:CPNullPlaceholderBindingOption] || nil;
299
300    return aValue;
301}
302
303- (id)reverseTransformValue:(id)aValue withOptions:(CPDictionary)options
304{
305    var valueTransformerName = [options objectForKey:CPValueTransformerNameBindingOption],
306        valueTransformer;
307
308    if (valueTransformerName)
309        valueTransformer = [CPValueTransformer valueTransformerForName:valueTransformerName];
310    else
311        valueTransformer = [options objectForKey:CPValueTransformerBindingOption];
312
313    if (valueTransformer && [[valueTransformer class] allowsReverseTransformation])
314        aValue = [valueTransformer reverseTransformedValue:aValue];
315
316    return aValue;
317}
318
319- (BOOL)continuouslyUpdatesValue
320{
321    var options = [_info objectForKey:CPOptionsKey];
322    return [[options objectForKey:CPContinuouslyUpdatesValueBindingOption] boolValue];
323}
324
325- (BOOL)handlesContentAsCompoundValue
326{
327    var options = [_info objectForKey:CPOptionsKey];
328    return [[options objectForKey:CPHandlesContentAsCompoundValueBindingOption] boolValue];
329}
330
331/*!
332    Use this to avoid reacting to the notifications coming out of a reverseTransformedValue:.
333*/
334- (void)suppressSpecificNotificationFromObject:(id)anObject keyPath:(CPString)aKeyPath
335{
336    if (!anObject)
337        return;
338
339    var uid = [anObject UID],
340        objectSuppressions = _suppressedNotifications[uid];
341
342    if (!objectSuppressions)
343        _suppressedNotifications[uid] = objectSuppressions = {};
344
345    objectSuppressions[aKeyPath] = YES;
346}
347
348/*!
349    Use this to cancel suppressSpecificNotificationFromObject:keyPath:.
350*/
351- (void)unsuppressSpecificNotificationFromObject:(id)anObject keyPath:(CPString)aKeyPath
352{
353    if (!anObject)
354        return;
355
356    var uid = [anObject UID],
357        objectSuppressions = _suppressedNotifications[uid];
358
359    if (!objectSuppressions)
360        return;
361
362    delete objectSuppressions[aKeyPath];
363}
364
365- (void)_updatePlaceholdersWithOptions:(CPDictionary)options
366{
367    var count = [CPBinderPlaceholderMarkers count];
368
369    while (count--)
370    {
371        var marker = CPBinderPlaceholderMarkers[count],
372            optionName = CPBinderPlaceholderOptions[count],
373            isExplicit = [options containsKey:optionName],
374            placeholder = isExplicit ? [options objectForKey:optionName] : nil;
375
376        [self _setPlaceholder:placeholder forMarker:marker isDefault:!isExplicit];
377    }
378}
379
380- (void)_updatePlaceholdersWithOptions:(CPDictionary)options forBinding:(CPString)aBinding
381{
382    [self _updatePlaceholdersWithOptions:options];
383}
384
385- (JSObject)_placeholderForMarker:(id)aMarker
386{
387    var placeholder = _placeholderForMarker[[aMarker UID]];
388
389    if (placeholder)
390        return placeholder.value;
391
392    return nil;
393}
394
395- (void)_setPlaceholder:(id)aPlaceholder forMarker:(id)aMarker isDefault:(BOOL)isDefault
396{
397    if (isDefault)
398    {
399        var existingPlaceholder = _placeholderForMarker[[aMarker UID]];
400
401        // Don't overwrite an explicitly set placeholder with a default.
402        if (existingPlaceholder && !existingPlaceholder.isDefault)
403            return;
404    }
405
406    _placeholderForMarker[[aMarker UID]] = { 'isDefault': isDefault, 'value': aPlaceholder };
407}
408
409@end
410
411@implementation CPObject (KeyValueBindingCreation)
412
413+ (void)exposeBinding:(CPString)aBinding
414{
415    [CPBinder exposeBinding:aBinding forClass:[self class]];
416}
417
418+ (Class)_binderClassForBinding:(CPString)aBinding
419{
420    return [CPBinder class];
421}
422
423+ (BOOL)isBindingExclusive:(CPString)aBinding
424{
425    return NO;
426}
427
428- (CPArray)exposedBindings
429{
430    var exposedBindings = [],
431        theClass = [self class];
432
433    while (theClass)
434    {
435        var temp = [CPBinder exposedBindingsForClass:theClass];
436
437        if (temp)
438            [exposedBindings addObjectsFromArray:temp];
439
440        theClass = [theClass superclass];
441    }
442
443    return exposedBindings;
444}
445
446- (Class)valueClassForBinding:(CPString)binding
447{
448    return [CPString class];
449}
450
451- (void)bind:(CPString)aBinding toObject:(id)anObject withKeyPath:(CPString)aKeyPath options:(CPDictionary)options
452{
453    if (!anObject || !aKeyPath)
454        return CPLog.error("Invalid object or path on " + self + " for " + aBinding);
455
456    if (![CPBinder isBindingAllowed:aBinding forObject:self])
457    {
458        CPLog.warn([self description] + " : cannot bind " + aBinding + " because another binding with the same functionality is already in use.");
459        return;
460    }
461    //if (![[self exposedBindings] containsObject:aBinding])
462    //    CPLog.warn("No binding exposed on " + self + " for " + aBinding);
463
464    var binderClass = [[self class] _binderClassForBinding:aBinding];
465
466    [self unbind:aBinding];
467    [[binderClass alloc] initWithBinding:[self _replacementKeyPathForBinding:aBinding] name:aBinding to:anObject keyPath:aKeyPath options:options from:self];
468}
469
470- (CPDictionary)infoForBinding:(CPString)aBinding
471{
472    return [CPBinder infoForBinding:aBinding forObject:self];
473}
474
475- (void)unbind:(CPString)aBinding
476{
477    var binderClass = [[self class] _binderClassForBinding:aBinding];
478    [binderClass unbind:aBinding forObject:self];
479}
480
481- (CPString)_replacementKeyPathForBinding:(CPString)binding
482{
483    return binding;
484}
485
486@end
487
488/*!
489    @ignore
490    Provides stub implementations that simply calls super for the "objectValue" binding.
491    This class should not be necessary but assures backwards compliance with our old way of doing bindings.
492
493    IMPORTANT!
494    Every class with a value binding should implement a subclass to handle its specific value binding logic.
495*/
496@implementation _CPValueBinder : CPBinder
497
498- (void)setValueFor:(CPString)theBinding
499{
500    [super setValueFor:@"objectValue"];
501}
502
503- (void)reverseSetValueFor:(CPString)theBinding
504{
505    [super reverseSetValueFor:@"objectValue"];
506}
507
508@end
509
510@implementation _CPMultipleValueBooleanBinding : CPBinder
511{
512    CPBindingOperationKind _operation;
513}
514
515- (void)setValueFor:(CPString)aBinding
516{
517    var bindings = [bindingsMap valueForKey:[_source UID]];
518
519    if (!bindings)
520        return;
521
522    var baseBinding = aBinding.replace(/\d$/, "");
523
524    [_source setValue:[self resolveMultipleValuesForBinding:baseBinding bindings:bindings booleanOperation:_operation] forKey:baseBinding];
525}
526
527- (void)reverseSetValueFor:(CPString)theBinding
528{
529    // read-only
530}
531
532- (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)changes context:(id)context
533{
534    [self setValueFor:context];
535}
536
537- (BOOL)resolveMultipleValuesForBinding:(CPString)aBinding bindings:(CPDictionary)bindings booleanOperation:(CPBindingOperationKind)operation
538{
539    var bindingName = aBinding,
540        theBinding,
541        count = 2;
542
543    while (theBinding = [bindings objectForKey:bindingName])
544    {
545        var info    = theBinding._info,
546            object  = [info objectForKey:CPObservedObjectKey],
547            keyPath = [info objectForKey:CPObservedKeyPathKey],
548            options = [info objectForKey:CPOptionsKey],
549            value   = [object valueForKeyPath:keyPath];
550
551        if (CPIsControllerMarker(value))
552        {
553            [self raiseIfNotApplicable:value forKeyPath:keyPath options:options];
554            value = [theBinding _placeholderForMarker:value];
555        }
556        else
557            value = [theBinding transformValue:value withOptions:options];
558
559        if (operation === CPBindingOperationOr)
560        {
561            // Any true condition means true for OR
562            if (value)
563                return YES;
564        }
565
566        // Any false condition means false for AND
567        else if (!value)
568            return NO;
569
570        bindingName = aBinding + (count++);
571    }
572
573    // If we get here, all OR conditions were false or all AND conditions were true
574    return operation === CPBindingOperationOr ? NO : YES;
575}
576
577@end
578
579@implementation CPMultipleValueAndBinding : _CPMultipleValueBooleanBinding
580
581- (id)init
582{
583    if (self = [super init])
584        _operation = CPBindingOperationAnd;
585
586    return self;
587}
588
589@end
590
591@implementation CPMultipleValueOrBinding : _CPMultipleValueBooleanBinding
592
593- (id)init
594{
595    if (self = [super init])
596        _operation = CPBindingOperationOr;
597
598    return self;
599}
600
601@end
602
603@implementation _CPMultipleValueActionBinding : CPBinder
604{
605    CPString _argumentBinding;
606    CPString _targetBinding;
607}
608
609- (void)setValueFor:(CPString)theBinding
610{
611    // Called when the binding is first created
612    [self checkForNullBinding:theBinding initializing:YES];
613}
614
615- (void)reverseSetValueFor:(CPString)theBinding
616{
617    // no-op
618}
619
620- (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)changes context:(id)context
621{
622    // context is the binding name
623    [self checkForNullBinding:context initializing:NO];
624}
625
626/*!
627    @ignore
628
629    When the value of a multiple-value argument binding changes,
630    if the binding is marked not to allow null arguments, we have to check
631    if the binding's value is nil and disable the button if it is.
632    Otherwise the button is enabled.
633*/
634- (void)checkForNullBinding:(CPString)theBinding initializing:(BOOL)isInitializing
635{
636    // Only done for buttons
637    if (![_source isKindOfClass:CPButton])
638        return;
639
640    // We start with the button enabled for the first argument during init,
641    // and subsequent checks can disable it.
642    if (isInitializing && theBinding === CPArgumentBinding)
643        [_source setEnabled:YES];
644
645    var bindings = [bindingsMap valueForKey:[_source UID]],
646        binding  = [bindings objectForKey:theBinding],
647        info     = binding._info,
648        options  = [info objectForKey:CPOptionsKey];
649
650    if (![options valueForKey:CPAllowsNullArgumentBindingOption])
651    {
652        var object  = [info objectForKey:CPObservedObjectKey],
653            keyPath = [info objectForKey:CPObservedKeyPathKey],
654            value   = [object valueForKeyPath:keyPath];
655
656        if (value === nil || value === undefined)
657        {
658            [_source setEnabled:NO];
659            return;
660        }
661    }
662
663    // If a binding value changed and did not fail the null test, enable the button
664    if (!isInitializing)
665        [_source setEnabled:YES];
666}
667
668- (void)invokeAction
669{
670    var bindings = [bindingsMap valueForKey:[_source UID]],
671        theBinding = [bindings objectForKey:CPTargetBinding],
672
673        info     = theBinding._info,
674        object   = [info objectForKey:CPObservedObjectKey],
675        keyPath  = [info objectForKey:CPObservedKeyPathKey],
676        options  = [info objectForKey:CPOptionsKey],
677
678        target   = [object valueForKeyPath:keyPath],
679        selector = [options objectForKey:CPSelectorNameBindingOption];
680
681    if (!target || !selector)
682        return;
683
684    var invocation = [CPInvocation invocationWithMethodSignature:[target methodSignatureForSelector:selector]],
685        bindingName = CPArgumentBinding,
686        count = 1;
687
688    while (theBinding = [bindings objectForKey:bindingName])
689    {
690        info   = theBinding._info;
691        object = [info objectForKey:CPObservedObjectKey];
692        keyPath  = [info objectForKey:CPObservedKeyPathKey];
693
694        [invocation setArgument:[object valueForKeyPath:keyPath] atIndex:++count];
695
696        bindingName = CPArgumentBinding + count;
697    }
698
699    [invocation setSelector:selector];
700    [invocation invokeWithTarget:target];
701}
702
703@end
704
705@implementation CPActionBinding : _CPMultipleValueActionBinding
706
707- (id)init
708{
709    if (self = [super init])
710    {
711        _argumentBinding = CPArgumentBinding;
712        _targetBinding = CPTargetBinding;
713    }
714
715    return self;
716}
717
718@end
719
720@implementation CPDoubleClickActionBinding : _CPMultipleValueActionBinding
721
722- (id)init
723{
724    if (self = [super init])
725    {
726        _argumentBinding = CPArgumentBinding;
727        _targetBinding = CPTargetBinding;
728    }
729
730    return self;
731}
732
733@end
734
735/*!
736    Abstract superclass for CPValueWithPatternBinding and CPTitleWithPatternBinding.
737*/
738@implementation _CPPatternBinding : CPBinder
739{
740    CPString _bindingKey;
741    CPString _patternPlaceholder;
742}
743
744- (void)setValueFor:(CPString)aBinding
745{
746    var bindings = [bindingsMap valueForKey:[_source UID]];
747
748    if (!bindings)
749        return;
750
751    // Strip off any trailing number from the binding name
752    var baseBinding = aBinding.replace(/\d$/, ""),
753        result = [self resolveMultipleValuesForBindings:bindings];
754
755    if (result.isPlaceholder)
756        [self setPlaceholderValue:result.value withMarker:result.marker forBinding:baseBinding];
757    else
758        [self setValue:result.value forBinding:baseBinding];
759}
760
761- (void)reverseSetValueFor:(CPString)theBinding
762{
763    // read-only
764}
765
766- (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)changes context:(id)context
767{
768    [self setValueFor:context];
769}
770
771- (JSObject)resolveMultipleValuesForBindings:(CPDictionary)bindings
772{
773    var theBinding,
774        result = { value:@"", isPlaceholder:NO, marker:nil };
775
776    for (var count = 1; theBinding = [bindings objectForKey:_bindingKey + count]; ++count)
777    {
778        var info    = theBinding._info,
779            object  = [info objectForKey:CPObservedObjectKey],
780            keyPath = [info objectForKey:CPObservedKeyPathKey],
781            options = [info objectForKey:CPOptionsKey],
782            value   = [object valueForKeyPath:keyPath];
783
784        if (count === 1)
785            result.value = [options objectForKey:CPDisplayPatternBindingOption];
786
787        if (CPIsControllerMarker(value))
788        {
789            [self raiseIfNotApplicable:value forKeyPath:keyPath options:options];
790
791            result.isPlaceholder = YES;
792            result.marker = value;
793
794            value = [theBinding _placeholderForMarker:value];
795        }
796        else
797            value = [theBinding transformValue:value withOptions:options];
798
799        if (value === nil || value === undefined)
800            value = @"";
801
802        result.value = result.value.replace("%{" + _patternPlaceholder + count + "}@", [value description]);
803    }
804
805    return result;
806}
807
808@end
809
810
811/*!
812    Users of this class must override setValue:forKey: and if the
813    key is CPDisplayPatternValueBinding, set the appropriate value
814    for the control class. For example, CPTextField uses setObjectValue.
815*/
816@implementation CPValueWithPatternBinding : _CPPatternBinding
817
818- (id)init
819{
820    if (self = [super init])
821    {
822        _bindingKey = CPDisplayPatternValueBinding;
823        _patternPlaceholder = @"value";
824    }
825
826    return self;
827}
828
829@end
830
831
832/*!
833    Users of this class must override setValue:forKey: and if the
834    key is CPDisplayPatternTitleBinding, set the appropriate value
835    for the control class. For example, CPBox uses setTitle.
836*/
837@implementation CPTitleWithPatternBinding : _CPPatternBinding
838
839- (id)init
840{
841    if (self = [super init])
842    {
843        _bindingKey = CPDisplayPatternTitleBinding;
844        _patternPlaceholder = @"title";
845    }
846
847    return self;
848}
849
850@end
851
852@implementation _CPStateMarker : CPObject
853{
854    CPString _name;
855}
856
857- (id)initWithName:(CPString)aName
858{
859    if (self = [super init])
860        _name = aName
861
862    return self;
863}
864
865- (CPString)description
866{
867    return "<" + _name + ">";
868}
869
870@end
871
872
873// Keys in options dictionary
874
875// Keys in dictionary returned by infoForBinding
876CPObservedObjectKey     = @"CPObservedObjectKey";
877CPObservedKeyPathKey    = @"CPObservedKeyPathKey";
878CPOptionsKey            = @"CPOptionsKey";
879
880// special markers
881CPNoSelectionMarker     = [[_CPStateMarker alloc] initWithName:@"NO SELECTION MARKER"];
882CPMultipleValuesMarker  = [[_CPStateMarker alloc] initWithName:@"MULTIPLE VALUES MARKER"];
883CPNotApplicableMarker   = [[_CPStateMarker alloc] initWithName:@"NOT APPLICABLE MARKER"];
884CPNullMarker            = [[_CPStateMarker alloc] initWithName:@"NULL MARKER"];
885
886// Binding name constants
887CPAlignmentBinding                        = @"alignment";
888CPArgumentBinding                         = @"argument";
889CPContentArrayBinding                     = @"contentArray";
890CPContentBinding                          = @"content";
891CPContentObjectBinding                    = @"contentObject";
892CPContentObjectsBinding                   = @"contentObjects";
893CPContentValuesBinding                    = @"contentValues";
894CPDisplayPatternTitleBinding              = @"displayPatternTitle";
895CPDisplayPatternValueBinding              = @"displayPatternValue";
896CPDoubleClickArgumentBinding              = @"doubleClickArgument";
897CPDoubleClickTargetBinding                = @"doubleClickTarget";
898CPEditableBinding                         = @"editable";
899CPEnabledBinding                          = @"enabled";
900CPFontBinding                             = @"font";
901CPFontNameBinding                         = @"fontName";
902CPFontBoldBinding                         = @"fontBold";
903CPHiddenBinding                           = @"hidden";
904CPFilterPredicateBinding                  = @"filterPredicate";
905CPMaxValueBinding                         = @"maxValue";
906CPMinValueBinding                         = @"minValue";
907CPPredicateBinding                        = @"predicate";
908CPSelectedIndexBinding                    = @"selectedIndex";
909CPSelectedLabelBinding                    = @"selectedLabel";
910CPSelectedObjectBinding                   = @"selectedObject";
911CPSelectedObjectsBinding                  = @"selectedObjects";
912CPSelectedTagBinding                      = @"selectedTag";
913CPSelectedValueBinding                    = @"selectedValue";
914CPSelectedValuesBinding                   = @"selectedValues";
915CPSelectionIndexesBinding                 = @"selectionIndexes";
916CPTargetBinding                           = @"target";
917CPTextColorBinding                        = @"textColor";
918CPTitleBinding                            = @"title";
919CPToolTipBinding                          = @"toolTip";
920CPValueBinding                            = @"value";
921CPValueURLBinding                         = @"valueURL";
922CPValuePathBinding                        = @"valuePath";
923CPDataBinding                             = @"data";
924
925// Binding options constants
926CPAllowsEditingMultipleValuesSelectionBindingOption = @"CPAllowsEditingMultipleValuesSelection";
927CPAllowsNullArgumentBindingOption                   = @"CPAllowsNullArgument";
928CPConditionallySetsEditableBindingOption            = @"CPConditionallySetsEditable";
929CPConditionallySetsEnabledBindingOption             = @"CPConditionallySetsEnabled";
930CPConditionallySetsHiddenBindingOption              = @"CPConditionallySetsHidden";
931CPContinuouslyUpdatesValueBindingOption             = @"CPContinuouslyUpdatesValue";
932CPCreatesSortDescriptorBindingOption                = @"CPCreatesSortDescriptor";
933CPDeletesObjectsOnRemoveBindingsOption              = @"CPDeletesObjectsOnRemove";
934CPDisplayNameBindingOption                          = @"CPDisplayName";
935CPDisplayPatternBindingOption                       = @"CPDisplayPattern";
936CPHandlesContentAsCompoundValueBindingOption        = @"CPHandlesContentAsCompoundValue";
937CPInsertsNullPlaceholderBindingOption               = @"CPInsertsNullPlaceholder";
938CPInvokesSeparatelyWithArrayObjectsBindingOption    = @"CPInvokesSeparatelyWithArrayObjects";
939CPMultipleValuesPlaceholderBindingOption            = @"CPMultipleValuesPlaceholder";
940CPNoSelectionPlaceholderBindingOption               = @"CPNoSelectionPlaceholder";
941CPNotApplicablePlaceholderBindingOption             = @"CPNotApplicablePlaceholder";
942CPNullPlaceholderBindingOption                      = @"CPNullPlaceholder";
943CPPredicateFormatBindingOption                      = @"CPPredicateFormat";
944CPRaisesForNotApplicableKeysBindingOption           = @"CPRaisesForNotApplicableKeys";
945CPSelectorNameBindingOption                         = @"CPSelectorName";
946CPSelectsAllWhenSettingContentBindingOption         = @"CPSelectsAllWhenSettingContent";
947CPValidatesImmediatelyBindingOption                 = @"CPValidatesImmediately";
948CPValueTransformerNameBindingOption                 = @"CPValueTransformerName";
949CPValueTransformerBindingOption                     = @"CPValueTransformer";
950
951CPIsControllerMarker = function(/*id*/anObject)
952{
953    return anObject === CPMultipleValuesMarker || anObject === CPNoSelectionMarker || anObject === CPNotApplicableMarker || anObject === CPNullMarker;
954};
955
956var CPBinderPlaceholderMarkers = [CPMultipleValuesMarker, CPNoSelectionMarker, CPNotApplicableMarker, CPNullMarker],
957    CPBinderPlaceholderOptions = [CPMultipleValuesPlaceholderBindingOption, CPNoSelectionPlaceholderBindingOption, CPNotApplicablePlaceholderBindingOption, CPNullPlaceholderBindingOption];
958