PageRenderTime 26ms CodeModel.GetById 14ms app.highlight 7ms RepoModel.GetById 1ms app.codeStats 0ms

/AppKit/CPTheme.j

http://github.com/cacaodev/cappuccino
Unknown | 826 lines | 658 code | 168 blank | 0 comment | 0 complexity | 1351dd3d497dce175bec78205625c8c9 MD5 | raw file
  1/*
  2 * CPTheme.j
  3 * AppKit
  4 *
  5 * Created by Francisco Tolmasky.
  6 * Copyright 2009, 280 North, Inc.
  7 *
  8 * This library is free software; you can redistribute it and/or
  9 * modify it under the terms of the GNU Lesser General Public
 10 * License as published by the Free Software Foundation; either
 11 * version 2.1 of the License, or (at your option) any later version.
 12 *
 13 * This library is distributed in the hope that it will be useful,
 14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 16 * Lesser General Public License for more details.
 17 *
 18 * You should have received a copy of the GNU Lesser General Public
 19 * License along with this library; if not, write to the Free Software
 20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 21 */
 22
 23@import <Foundation/CPObject.j>
 24@import <Foundation/CPMutableArray.j>
 25@import <Foundation/CPString.j>
 26@import <Foundation/CPKeyedUnarchiver.j>
 27
 28@class CPView
 29@class _CPThemeAttribute
 30
 31var CPThemesByName          = { },
 32    CPThemeDefaultTheme     = nil,
 33    CPThemeDefaultHudTheme  = nil;
 34
 35
 36/*!
 37    @ingroup appkit
 38*/
 39
 40@implementation CPTheme : CPObject
 41{
 42    CPString        _name;
 43    CPDictionary    _attributes;
 44}
 45
 46+ (void)setDefaultTheme:(CPTheme)aTheme
 47{
 48    CPThemeDefaultTheme = aTheme;
 49}
 50
 51+ (CPTheme)defaultTheme
 52{
 53    return CPThemeDefaultTheme;
 54}
 55
 56/*!
 57    Set the default HUD theme. If set to nil, the default described in defaultHudTheme
 58    will be used.
 59*/
 60+ (void)setDefaultHudTheme:(CPTheme)aTheme
 61{
 62    CPThemeDefaultHudTheme = aTheme;
 63}
 64
 65/*!
 66    The default HUD theme is (sometimes) used for windows with the CPHUDBackgroundWindowMask
 67    style mask. The default is theme with the name of the default theme with -HUD appended
 68    at the end.
 69*/
 70+ (CPTheme)defaultHudTheme
 71{
 72    if (!CPThemeDefaultHudTheme)
 73        CPThemeDefaultHudTheme = [CPTheme themeNamed:[[self defaultTheme] name] + "-HUD"];
 74    return CPThemeDefaultHudTheme;
 75}
 76
 77+ (CPTheme)themeNamed:(CPString)aName
 78{
 79    return CPThemesByName[aName];
 80}
 81
 82- (id)initWithName:(CPString)aName
 83{
 84    self = [super init];
 85
 86    if (self)
 87    {
 88        _name = aName;
 89        _attributes = @{};
 90
 91        CPThemesByName[_name] = self;
 92    }
 93
 94    return self;
 95}
 96
 97- (CPString)name
 98{
 99    return _name;
100}
101
102/*!
103    Returns an array of names of themed classes defined in this theme, as found in its
104    ThemeDescriptors.j file.
105
106    NOTE: The names are not class names (such as "CPButton"), but the names returned
107    by the class' +defaultThemeClass method. For example, the name for CPCheckBox is "check-box",
108    as defined in CPCheckBox::themeClass.
109*/
110- (CPArray)classNames
111{
112    return [_attributes allKeys];
113}
114
115/*!
116    Returns a dictionary of all theme attributes defined for the given class, as found in the
117    theme's ThemeDescriptors.j file. The keys of the dictionary are attribute names, and the values
118    are instances of _CPThemeAttribute.
119
120    For a description of valid values for \c aClass, see \ref attributeNamesForClass:.
121
122    @param aClass The themed class whose attributes you want to retrieve
123    @return       A dictionary of attributes or nil
124*/
125- (CPDictionary)attributesForClass:(id)aClass
126{
127    if (!aClass)
128        return nil;
129
130    var className = nil;
131
132    if ([aClass isKindOfClass:[CPString class]])
133    {
134        // See if it is a class name
135        var theClass = CPClassFromString(aClass);
136
137        if (theClass)
138            aClass = theClass;
139        else
140            className = aClass;
141    }
142
143    if (!className)
144    {
145        if ([aClass isKindOfClass:[CPView class]])
146        {
147            if ([aClass respondsToSelector:@selector(defaultThemeClass)])
148                className = [aClass defaultThemeClass];
149            else if ([aClass respondsToSelector:@selector(themeClass)])
150            {
151                CPLog.warn(@"%@ themeClass is deprecated in favor of defaultThemeClass", CPStringFromClass(aClass));
152                className = [aClass themeClass];
153            }
154            else
155                return nil;
156        }
157        else
158            [CPException raise:CPInvalidArgumentException reason:@"aClass must be a class object or a string."];
159    }
160
161    return [_attributes objectForKey:className];
162}
163
164/*!
165    Returns an array of names of all theme attributes defined for the given class, as found in the
166    theme's ThemeDescriptors.j file.
167
168    The \c aClass parameter can be one of the following:
169
170    - A class instance, for example the result of [CPCheckBox class]. The class must be a subclass
171      of CPView.
172    - A class name, for example "CPCheckBox".
173    - A themed class name, for example "check-box".
174
175    If \c aClass does not refer to a themed class in this theme, nil is returned.
176
177    @param aClass The themed class whose attributes you want to retrieve
178    @return       An array of attribute names or nil
179*/
180- (CPDictionary)attributeNamesForClass:(id)aClass
181{
182    var attributes = [self attributesForClass:aClass];
183
184    if (attributes)
185        return [attributes allKeys];
186    else
187        return [CPArray array];
188}
189
190/*!
191    Returns a theme attribute defined for the given class, as found in the
192    theme's ThemeDescriptors.j file.
193
194    \c aName should be the attribute name as you would pass to the method
195    CPView::valueForThemeAttribute:.
196
197    For a description of valid values for \c aClass, see \ref attributeNamesForClass:.
198
199    @param aName  The name of the attribute you want to retrieve
200    @param aClass The themed class in which to look for the attribute
201    @return       An instance of _CPThemeAttribute or nil
202*/
203- (_CPThemeAttribute)attributeWithName:(CPString)aName forClass:(id)aClass
204{
205    var attributes = [self attributesForClass:aClass];
206
207    if (!attributes)
208        return nil;
209
210    return [attributes objectForKey:aName];
211}
212
213/*!
214    Returns the value for a theme attribute in its normal state, as defined for the given class
215    in the theme's ThemeDescriptors.j file.
216
217    \c aName should be the attribute name as you would pass to the method
218    CPView::valueForThemeAttribute:.
219
220    For a description of valid values for \c aClass, see \ref attributeNamesForClass:.
221
222    @param aName  The name of the attribute whose value you want to retrieve
223    @param aClass The themed class in which to look for the attribute
224    @return       A value or nil
225*/
226- (id)valueForAttributeWithName:(CPString)aName forClass:(id)aClass
227{
228    return [self valueForAttributeWithName:aName inState:CPThemeStateNormal forClass:aClass];
229}
230
231/*!
232    Returns the value for a theme attribute in a given state, as defined for the given class
233    in the theme's ThemeDescriptors.j file. This is the equivalent of the method
234    CPView::valueForThemeAttribute:inState:, but retrieves the value from the theme definition as
235    opposed to a single view's current theme state.
236
237    For a description of valid values for \c aClass, see \ref attributeNamesForClass:.
238
239    @param aName  The name of the attribute whose value you want to retrieve
240    @param aState The state qualifier for the attribute
241    @param aClass The themed class in which to look for the attribute
242    @return       A value or nil
243*/
244- (id)valueForAttributeWithName:(CPString)aName inState:(ThemeState)aState forClass:(id)aClass
245{
246    var attribute = [self attributeWithName:aName forClass:aClass];
247
248    if (!attribute)
249        return nil;
250
251    return [attribute valueForState:aState];
252}
253
254- (void)takeThemeFromObject:(id)anObject
255{
256    var attributes = [anObject _themeAttributeDictionary],
257        attributeName = nil,
258        attributeNames = [attributes keyEnumerator],
259        objectThemeClass = [anObject themeClass];
260
261    while ((attributeName = [attributeNames nextObject]) !== nil)
262        [self _recordAttribute:[attributes objectForKey:attributeName] forClass:objectThemeClass];
263}
264
265- (void)_recordAttribute:(_CPThemeAttribute)anAttribute forClass:(CPString)aClass
266{
267    if (![anAttribute hasValues])
268        return;
269
270    var attributes = [_attributes objectForKey:aClass];
271
272    if (!attributes)
273    {
274        attributes = @{};
275
276        [_attributes setObject:attributes forKey:aClass];
277    }
278
279    var name = [anAttribute name],
280        existingAttribute = [attributes objectForKey:name];
281
282    if (existingAttribute)
283        [attributes setObject:[existingAttribute attributeMergedWithAttribute:anAttribute] forKey:name];
284    else
285        [attributes setObject:anAttribute forKey:name];
286}
287
288@end
289
290var CPThemeNameKey          = @"CPThemeNameKey",
291    CPThemeAttributesKey    = @"CPThemeAttributesKey";
292
293@implementation CPTheme (CPCoding)
294
295- (id)initWithCoder:(CPCoder)aCoder
296{
297    self = [super init];
298
299    if (self)
300    {
301        _name = [aCoder decodeObjectForKey:CPThemeNameKey];
302        _attributes = [aCoder decodeObjectForKey:CPThemeAttributesKey];
303
304        CPThemesByName[_name] = self;
305    }
306
307    return self;
308}
309
310- (void)encodeWithCoder:(CPCoder)aCoder
311{
312    [aCoder encodeObject:_name forKey:CPThemeNameKey];
313    [aCoder encodeObject:_attributes forKey:CPThemeAttributesKey];
314}
315
316@end
317
318/*!
319 * ThemeStates are immutable objects representing a particular ThemeState.  Applications should never be creating
320 * ThemeStates directly but should instead use the CPThemeState function.
321 */
322function ThemeState(stateNames)
323{
324    var stateNameKeys = [];
325    this._stateNames = {};
326
327    for (key in stateNames)
328    {
329        if (!stateNames.hasOwnProperty(key))
330            continue;
331        if (key !== 'normal')
332        {
333            this._stateNames[key] = true;
334            stateNameKeys.push(key);
335        }
336    }
337
338    if (stateNameKeys.length === 0)
339    {
340        stateNameKeys.push('normal');
341        this._stateNames['normal'] = true;
342    }
343
344    stateNameKeys.sort();
345    this._stateNameString = stateNameKeys[0];
346
347    var stateNameLength = stateNameKeys.length;
348    for (var stateIndex = 1; stateIndex < stateNameLength; stateIndex++)
349        this._stateNameString = this._stateNameString + "+" + stateNameKeys[stateIndex];
350    this._stateNameCount = stateNameLength;
351}
352
353ThemeState.prototype.toString = function()
354{
355    return this._stateNameString;
356}
357
358ThemeState.prototype.hasThemeState = function(aState)
359{
360    if (!aState || !aState._stateNames)
361        return false;
362
363    // We can do this in O(n) because both states have their stateNames already sorted.
364    for (var stateName in aState._stateNames)
365    {
366        if (!aState._stateNames.hasOwnProperty(stateName))
367            continue;
368
369        if (!this._stateNames[stateName])
370            return false;
371    }
372    return true;
373}
374
375ThemeState.prototype.isSubsetOf = function(aState)
376{
377    if (aState._stateNameCount < this._stateNameCount)
378        return false;
379
380    for (var key in this._stateNames)
381    {
382        if (!this._stateNames.hasOwnProperty(key))
383            continue;
384
385        if (!aState._stateNames[key])
386            return false;
387    }
388    return true;
389}
390
391ThemeState.prototype.without = function(aState)
392{
393    if (!aState || aState === [CPNull null])
394        return this;
395
396    var newStates = {};
397    for (var stateName in this._stateNames)
398    {
399        if (!this._stateNames.hasOwnProperty(stateName))
400            continue;
401
402        if (!aState._stateNames[stateName])
403            newStates[stateName] = true;
404    }
405
406    return ThemeState._cacheThemeState(new ThemeState(newStates));
407}
408
409ThemeState.prototype.and  = function(aState)
410{
411    return CPThemeState(this, aState);
412}
413
414var CPThemeStates = {};
415
416ThemeState._cacheThemeState = function(aState)
417{
418    // We do this caching so themeState equality works.  Basically, doing CPThemeState('foo+bar') === CPThemeState('bar', 'foo') will return true.
419    var themeState = CPThemeStates[String(aState)];
420    if (themeState === undefined)
421    {
422        themeState = aState;
423        CPThemeStates[String(themeState)] = themeState;
424    }
425    return themeState;
426}
427
428/*!
429 * This method can be called in multiple ways:
430 *    CPThemeState('state1') - creates a new CPThemeState that corresponds to the string 'state1'
431 *    CPThemeState('state1', 'state2') - creates a new composite CPThemeState made up of both 'state1' or 'state2'
432 *    CPThemeState('state1+state2') - The same as CPThemeState('state1', 'state2')
433 *    CPThemeState(state1, state2) - creates a new composite CPThemeState made up of state1 and state2
434 *                                   where state1 and state2 are not strings but are themselves CPThemeStates.
435 */
436function CPThemeState()
437{
438    if (arguments.length < 1)
439        throw "CPThemeState() must be called with at least one string argument";
440
441    var themeState;
442    if (arguments.length === 1 && typeof arguments[0] === 'string')
443    {
444        themeState = CPThemeStates[arguments[0]];
445        if (themeState !== undefined)
446            return themeState;
447    }
448
449    var stateNames = {};
450    for (var argIndex = 0; argIndex < arguments.length; argIndex++)
451    {
452        if (arguments[argIndex] === [CPNull null] || !arguments[argIndex])
453            continue;
454
455        if (typeof arguments[argIndex] === 'object')
456        {
457            for (var stateName in arguments[argIndex]._stateNames)
458            {
459                if (!arguments[argIndex]._stateNames.hasOwnProperty(stateName))
460                    continue;
461                stateNames[stateName] = true;
462            }
463        }
464        else
465        {
466            var allNames = arguments[argIndex].split('+');
467            for (var nameIndex = 0; nameIndex < allNames.length; nameIndex++)
468                stateNames[allNames[nameIndex]] = true;
469        }
470    }
471
472    themeState = ThemeState._cacheThemeState(new ThemeState(stateNames));
473    return themeState;
474}
475
476@implementation _CPThemeKeyedUnarchiver : CPKeyedUnarchiver
477{
478    CPBundle    _bundle;
479}
480
481- (id)initForReadingWithData:(CPData)data bundle:(CPBundle)aBundle
482{
483    self = [super initForReadingWithData:data];
484
485    if (self)
486        _bundle = aBundle;
487
488    return self;
489}
490
491- (CPBundle)bundle
492{
493    return _bundle;
494}
495
496- (BOOL)awakenCustomResources
497{
498    return YES;
499}
500
501@end
502
503CPThemeStateNormal              = CPThemeState("normal");
504CPThemeStateDisabled            = CPThemeState("disabled");
505CPThemeStateHovered             = CPThemeState("hovered");
506CPThemeStateHighlighted         = CPThemeState("highlighted");
507CPThemeStateSelected            = CPThemeState("selected");
508CPThemeStateTableDataView       = CPThemeState("tableDataView");
509CPThemeStateSelectedDataView    = CPThemeState("selectedTableDataView");
510CPThemeStateGroupRow            = CPThemeState("CPThemeStateGroupRow");
511CPThemeStateBezeled             = CPThemeState("bezeled");
512CPThemeStateBordered            = CPThemeState("bordered");
513CPThemeStateEditable            = CPThemeState("editable");
514CPThemeStateEditing             = CPThemeState("editing");
515CPThemeStateVertical            = CPThemeState("vertical");
516CPThemeStateDefault             = CPThemeState("default");
517CPThemeStateCircular            = CPThemeState("circular");
518CPThemeStateAutocompleting      = CPThemeState("autocompleting");
519CPThemeStateFirstResponder      = CPThemeState("firstResponder");
520CPThemeStateMainWindow          = CPThemeState("mainWindow");
521CPThemeStateKeyWindow           = CPThemeState("keyWindow");
522CPThemeStateControlSizeRegular  = CPThemeState("controlSizeRegular");
523CPThemeStateControlSizeSmall    = CPThemeState("controlSizeSmall");
524CPThemeStateControlSizeMini     = CPThemeState("controlSizeMini");
525
526@implementation _CPThemeAttribute : CPObject
527{
528    CPString            _name;
529    id                  _defaultValue;
530    CPDictionary        _values @accessors(readonly, getter=values);
531
532    JSObject            _cache;
533    _CPThemeAttribute   _themeDefaultAttribute;
534}
535
536- (id)initWithName:(CPString)aName defaultValue:(id)aDefaultValue
537{
538    self = [super init];
539
540    if (self)
541    {
542        _cache = { };
543        _name = aName;
544        _defaultValue = aDefaultValue;
545        _values = @{};
546    }
547
548    return self;
549}
550
551- (CPString)name
552{
553    return _name;
554}
555
556- (id)defaultValue
557{
558    return _defaultValue;
559}
560
561- (BOOL)hasValues
562{
563    return [_values count] > 0;
564}
565
566- (void)setValue:(id)aValue
567{
568    _cache = {};
569
570    if (aValue === undefined || aValue === nil)
571        _values = @{};
572    else
573        _values = @{ String(CPThemeStateNormal): aValue };
574}
575
576- (void)setValue:(id)aValue forState:(ThemeState)aState
577{
578    _cache = { };
579
580    if ((aValue === undefined) || (aValue === nil))
581        [_values removeObjectForKey:String(aState)];
582    else
583        [_values setObject:aValue forKey:String(aState)];
584}
585
586- (id)value
587{
588    return [self valueForState:CPThemeStateNormal];
589}
590
591- (id)valueForState:(ThemeState)aState
592{
593    var stateName = String(aState),
594        value = _cache[stateName];
595
596    // This can be nil.
597    if (value !== undefined)
598        return value;
599
600    value = [_values objectForKey:stateName];
601
602    if (value === undefined || value === nil)
603    {
604        // If this is a composite state, find the closest partial subset match.
605        if (aState._stateNameCount > 1)
606        {
607            var states = [_values allKeys],
608                count = states.length,
609                largestThemeState = 0;
610
611            while (count--)
612            {
613                var stateObject = CPThemeState(states[count]);
614
615                if (stateObject.isSubsetOf(aState) && stateObject._stateNameCount > largestThemeState)
616                {
617                    value = [_values objectForKey:states[count]];
618                    largestThemeState = stateObject._stateNameCount;
619                }
620            }
621        }
622
623        // Still don't have a value? OK, let's use the normal value.
624        if (value === undefined || value === nil)
625            value = [_values objectForKey:String(CPThemeStateNormal)];
626    }
627
628    if (value === undefined || value === nil)
629        value = [_themeDefaultAttribute valueForState:aState];
630
631    if (value === undefined || value === nil)
632    {
633        value = _defaultValue;
634
635        // Class theme attributes cannot use nil because it's a dictionary.
636        // So transform CPNull into nil.
637        if (value === [CPNull null])
638            value = nil;
639    }
640
641    _cache[stateName] = value;
642
643    return value;
644}
645
646- (void)setParentAttribute:(_CPThemeAttribute)anAttribute
647{
648    if (_themeDefaultAttribute === anAttribute)
649        return;
650
651    _cache = { };
652    _themeDefaultAttribute = anAttribute;
653}
654
655- (_CPThemeAttribute)attributeMergedWithAttribute:(_CPThemeAttribute)anAttribute
656{
657    var mergedAttribute = [[_CPThemeAttribute alloc] initWithName:_name defaultValue:_defaultValue];
658
659    mergedAttribute._values = [_values copy];
660    [mergedAttribute._values addEntriesFromDictionary:anAttribute._values];
661
662    return mergedAttribute;
663}
664
665@end
666
667@implementation _CPThemeAttribute (CPCoding)
668
669- (id)initWithCoder:(CPCoder)aCoder
670{
671    self = [super init];
672
673    if (self)
674    {
675        _cache = {};
676
677        _name = [aCoder decodeObjectForKey:@"name"];
678        _defaultValue = [aCoder decodeObjectForKey:@"defaultValue"];
679        _values = @{};
680
681        if ([aCoder containsValueForKey:@"value"])
682        {
683            var state = String(CPThemeStateNormal);
684
685            if ([aCoder containsValueForKey:@"state"])
686                state = [aCoder decodeObjectForKey:@"state"];
687
688            [_values setObject:[aCoder decodeObjectForKey:"value"] forKey:state];
689        }
690        else
691        {
692            var encodedValues = [aCoder decodeObjectForKey:@"values"],
693                keys = [encodedValues allKeys],
694                count = keys.length;
695
696            while (count--)
697            {
698                var key = keys[count];
699
700                [_values setObject:[encodedValues objectForKey:key] forKey:key];
701            }
702        }
703    }
704
705    return self;
706}
707
708- (void)encodeWithCoder:(CPCoder)aCoder
709{
710    [aCoder encodeObject:_name forKey:@"name"];
711    [aCoder encodeObject:_defaultValue forKey:@"defaultValue"];
712
713    var keys = [_values allKeys],
714        count = keys.length;
715
716    if (count === 1)
717    {
718        var onlyKey = keys[0];
719
720        if (onlyKey !== String(CPThemeStateNormal))
721            [aCoder encodeObject:onlyKey forKey:@"state"];
722
723        [aCoder encodeObject:[_values objectForKey:onlyKey] forKey:@"value"];
724    }
725    else
726    {
727        var encodedValues = @{};
728
729        while (count--)
730        {
731            var key = keys[count];
732
733            [encodedValues setObject:[_values objectForKey:key] forKey:key];
734        }
735
736        [aCoder encodeObject:encodedValues forKey:@"values"];
737    }
738}
739
740@end
741
742function CPThemeAttributeEncode(aCoder, aThemeAttribute)
743{
744    var values = aThemeAttribute._values,
745        count = [values count],
746        key = "$a" + [aThemeAttribute name];
747
748    if (count === 1)
749    {
750        var state = [values allKeys][0];
751
752        if (state === String(CPThemeStateNormal))
753        {
754            [aCoder encodeObject:[values objectForKey:state] forKey:key];
755
756            return YES;
757        }
758    }
759
760    if (count >= 1)
761    {
762        [aCoder encodeObject:aThemeAttribute forKey:key];
763
764        return YES;
765    }
766
767    return NO;
768}
769
770function CPThemeAttributeDecode(aCoder, anAttributeName, aDefaultValue, aTheme, aClass)
771{
772    var key = "$a" + anAttributeName;
773
774    if (![aCoder containsValueForKey:key])
775        var attribute = [[_CPThemeAttribute alloc] initWithName:anAttributeName defaultValue:aDefaultValue];
776
777    else
778    {
779        var attribute = [aCoder decodeObjectForKey:key];
780
781        if (!attribute || !attribute.isa || ![attribute isKindOfClass:[_CPThemeAttribute class]])
782        {
783            var themeAttribute = [[_CPThemeAttribute alloc] initWithName:anAttributeName defaultValue:aDefaultValue];
784
785            [themeAttribute setValue:attribute];
786
787            attribute = themeAttribute;
788        }
789    }
790
791    if (aTheme && aClass)
792        [attribute setParentAttribute:[aTheme attributeWithName:anAttributeName forClass:aClass]];
793
794    return attribute;
795}
796
797/* TO AUTO CREATE THESE:
798function bit_count(bits)
799    {
800        var count = 0;
801
802        while (bits)
803        {
804            ++count;
805            bits &= (bits - 1);
806        }
807
808        return count ;
809    }
810
811zeros = "000000000";
812
813function pad(string, digits)
814{
815    return zeros.substr(0, digits - string.length) + string;
816}
817
818var str = ""
819str += '[';
820for (i = 0;i < Math.pow(2,6);++i)
821{
822    str += bit_count(i) + " /*" + pad(i.toString(2),6) + "*" + "/, ";
823}
824print(str+']');
825
826*/