/* * CPTheme.j * AppKit * * Created by Francisco Tolmasky. * Copyright 2009, 280 North, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ @import @import var CPThemesByName = { }, CPThemeDefaultTheme = nil; /*! @ingroup appkit */ @implementation CPTheme : CPObject { CPString _name; CPDictionary _attributes; } + (void)setDefaultTheme:(CPTheme)aTheme { CPThemeDefaultTheme = aTheme; } + (CPTheme)defaultTheme { return CPThemeDefaultTheme; } + (CPTheme)themeNamed:(CPString)aName { return CPThemesByName[aName]; } - (id)initWithName:(CPString)aName { self = [super init]; if (self) { _name = aName; _attributes = [CPDictionary dictionary]; CPThemesByName[_name] = self; } return self; } - (CPString)name { return _name; } - (_CPThemeAttribute)_attributeWithName:(CPString)aName forClass:(CPString)aClass { var attributes = [_attributes objectForKey:aClass]; if (!attributes) return nil; return [attributes objectForKey:aName]; } - (void)takeThemeFromObject:(id)anObject { var attributes = [anObject _themeAttributeDictionary], attributeName = nil, attributeNames = [attributes keyEnumerator], objectThemeClass = [[anObject class] themeClass]; while (attributeName = [attributeNames nextObject]) [self _recordAttribute:[attributes objectForKey:attributeName] forClass:objectThemeClass]; } - (void)_recordAttribute:(_CPThemeAttribute)anAttribute forClass:(CPString)aClass { if (![anAttribute hasValues]) return; var attributes = [_attributes objectForKey:aClass]; if (!attributes) { attributes = [CPDictionary dictionary]; [_attributes setObject:attributes forKey:aClass]; } var name = [anAttribute name], existingAttribute = [attributes objectForKey:name]; if (existingAttribute) [attributes setObject:[existingAttribute attributeMergedWithAttribute:anAttribute] forKey:name]; else [attributes setObject:anAttribute forKey:name]; } @end var CPThemeNameKey = @"CPThemeNameKey", CPThemeAttributesKey = @"CPThemeAttributesKey"; @implementation CPTheme (CPCoding) - (id)initWithCoder:(CPCoder)aCoder { self = [super init]; if (self) { _name = [aCoder decodeObjectForKey:CPThemeNameKey]; _attributes = [aCoder decodeObjectForKey:CPThemeAttributesKey]; CPThemesByName[_name] = self; } return self; } - (void)encodeWithCoder:(CPCoder)aCoder { [aCoder encodeObject:_name forKey:CPThemeNameKey]; [aCoder encodeObject:_attributes forKey:CPThemeAttributesKey]; } @end @implementation _CPThemeKeyedUnarchiver : CPKeyedUnarchiver { CPBundle _bundle; } - (id)initForReadingWithData:(CPData)data bundle:(CPBundle)aBundle { self = [super initForReadingWithData:data]; if (self) _bundle = aBundle; return self; } - (CPBundle)bundle { return _bundle; } - (BOOL)awakenCustomResources { return YES; } @end var CPThemeStates = {}, CPThemeStateNames = {}, CPThemeStateCount = 0; function CPThemeState(aStateName) { var state = CPThemeStates[aStateName]; if (state === undefined) { if (aStateName.indexOf('+') === -1) state = 1 << CPThemeStateCount++; else { var state = 0, states = aStateName.split('+'), count = states.length; while (count--) { var stateName = states[count], individualState = CPThemeStates[stateName]; if (individualState === undefined) { individualState = 1 << CPThemeStateCount++; CPThemeStates[stateName] = individualState; CPThemeStateNames[individualState] = stateName; } state |= individualState; } } CPThemeStates[aStateName] = state; CPThemeStateNames[state] = aStateName; } return state; } function CPThemeStateName(aState) { var name = CPThemeStateNames[aState]; if (name !== undefined) return name; if (!(aState & (aState - 1))) return ""; var state = 1, name = ""; for (; state < aState; state <<= 1) if (aState & state) name += (name.length === 0 ? '' : '+') + CPThemeStateNames[state]; CPThemeStateNames[aState] = name; return name; } CPThemeStateNames[0] = "normal"; CPThemeStateNormal = CPThemeStates["normal"] = 0; CPThemeStateDisabled = CPThemeState("disabled"); CPThemeStateHighlighted = CPThemeState("highlighted"); CPThemeStateSelected = CPThemeState("selected"); CPThemeStateBezeled = CPThemeState("bezeled"); CPThemeStateBordered = CPThemeState("bordered"); CPThemeStateEditable = CPThemeState("editable"); CPThemeStateEditing = CPThemeState("editing"); CPThemeStateVertical = CPThemeState("vertical"); CPThemeStateDefault = CPThemeState("default"); CPThemeStateCircular = CPThemeState("circular"); @implementation _CPThemeAttribute : CPObject { CPString _name; id _defaultValue; CPDictionary _values; JSObject _cache; CPThemeAttribute _parentAttribute; } - (id)initWithName:(CPString)aName defaultValue:(id)aDefaultValue { self = [super init]; if (self) { _cache = { }; _name = aName; _defaultValue = aDefaultValue; _values = [CPDictionary dictionary]; } return self; } - (CPString)name { return _name; } - (id)defaultValue { return _defaultValue; } - (BOOL)hasValues { return [_values count] > 0; } - (BOOL)isTrivial { return ([_values count] === 1) && (Number([_values allKeys][0]) === CPThemeStateNormal); } - (void)setValue:(id)aValue { _cache = {}; if (aValue === undefined || aValue === nil) _values = [CPDictionary dictionary]; else _values = [CPDictionary dictionaryWithObject:aValue forKey:String(CPThemeStateNormal)]; } - (void)setValue:(id)aValue forState:(CPThemeState)aState { _cache = { }; if ((aValue === undefined) || (aValue === nil)) [_values removeObjectForKey:String(aState)]; else [_values setObject:aValue forKey:String(aState)]; } - (id)value { return [self valueForState:CPThemeStateNormal]; } - (id)valueForState:(CPThemeState)aState { var value = _cache[aState]; // This can be nil. if (value !== undefined) return value; value = [_values objectForKey:String(aState)]; // If we don't have a value, and we have a non-normal state... if ((value === undefined || value === nil) && aState !== CPThemeStateNormal) { // If this is a composite state (not a power of 2), find the closest partial subset match. if (aState & (aState - 1)) { var highestOneCount = 0, states = [_values allKeys], count = states.length; while (count--) { // states[count] is a string! var state = Number(states[count]); // A & B = A iff A < B if ((state & aState) === state) { var oneCount = cachedNumberOfOnes[state]; if (oneCount === undefined) oneCount = numberOfOnes(state); if (oneCount > highestOneCount) { highestOneCount = oneCount; value = [_values objectForKey:String(state)]; } } } } // Still don't have a value? OK, let's use the normal value. if (value === undefined || value === nil) value = [_values objectForKey:String(CPThemeStateNormal)]; } if (value === undefined || value === nil) value = [_parentAttribute valueForState:aState]; if (value === undefined || value === nil) value = _defaultValue; _cache[aState] = value; return value; } - (void)setParentAttribute:(CPThemeAttribute)anAttribute { if (_parentAttribute === anAttribute) return; _cache = { }; _parentAttribute = anAttribute; } - (CPThemeAttribute)attributeMergedWithAttribute:(_CPThemeAttribute)anAttribute { var mergedAttribute = [[_CPThemeAttribute alloc] initWithName:_name defaultValue:_defaultValue]; mergedAttribute._values = [_values copy]; [mergedAttribute._values addEntriesFromDictionary:anAttribute._values]; return mergedAttribute; } @end @implementation _CPThemeAttribute (CPCoding) - (id)initWithCoder:(CPCoder)aCoder { self = [super init]; if (self) { _cache = {}; _name = [aCoder decodeObjectForKey:@"name"]; _values = [CPDictionary dictionary]; if ([aCoder containsValueForKey:@"value"]) { var state = CPThemeStateNormal; if ([aCoder containsValueForKey:@"state"]) state = CPThemeState([aCoder decodeObjectForKey:@"state"]); [_values setObject:[aCoder decodeObjectForKey:"value"] forKey:state]; } else { var encodedValues = [aCoder decodeObjectForKey:@"values"], keys = [encodedValues allKeys], count = keys.length; while (count--) { var key = keys[count]; [_values setObject:[encodedValues objectForKey:key] forKey:CPThemeState(key)]; } } } return self; } - (void)encodeWithCoder:(CPCoder)aCoder { [aCoder encodeObject:_name forKey:@"name"]; var keys = [_values allKeys], count = keys.length; if (count === 1) { var onlyKey = keys[0]; if (Number(onlyKey) !== CPThemeStateNormal) [aCoder encodeObject:CPThemeStateName(Number(onlyKey)) forKey:@"state"]; [aCoder encodeObject:[_values objectForKey:onlyKey] forKey:@"value"]; } else { var encodedValues = [CPDictionary dictionary]; while (count--) { var key = keys[count]; [encodedValues setObject:[_values objectForKey:key] forKey:CPThemeStateName(Number(key))]; } [aCoder encodeObject:encodedValues forKey:@"values"]; } } @end var cachedNumberOfOnes = [ 0 /*000000*/, 1 /*000001*/, 1 /*000010*/, 2 /*000011*/, 1 /*000100*/, 2 /*000101*/, 2 /*000110*/, 3 /*000111*/, 1 /*001000*/, 2 /*001001*/, 2 /*001010*/, 3 /*001011*/, 2 /*001100*/, 3 /*001101*/, 3 /*001110*/, 4 /*001111*/, 1 /*010000*/, 2 /*010001*/, 2 /*010010*/, 3 /*010011*/, 2 /*010100*/, 3 /*010101*/, 3 /*010110*/, 4 /*010111*/, 2 /*011000*/, 3 /*011001*/, 3 /*011010*/, 4 /*011011*/, 3 /*011100*/, 4 /*011101*/, 4 /*011110*/, 5 /*011111*/, 1 /*100000*/, 2 /*100001*/, 2 /*100010*/, 3 /*100011*/, 2 /*100100*/, 3 /*100101*/, 3 /*100110*/, 4 /*100111*/, 2 /*101000*/, 3 /*101001*/, 3 /*101010*/, 4 /*101011*/, 3 /*101100*/, 4 /*101101*/, 4 /*101110*/, 5 /*101111*/, 2 /*110000*/, 3 /*110001*/, 3 /*110010*/, 4 /*110011*/, 3 /*110100*/, 4 /*110101*/, 4 /*110110*/, 5 /*110111*/, 3 /*111000*/, 4 /*111001*/, 4 /*111010*/, 5 /*111011*/, 4 /*111100*/, 5 /*111101*/, 5 /*111110*/, 6 /*111111*/ ]; var numberOfOnes = function(aNumber) { var count = 0, slot = aNumber; while (aNumber) { ++count; aNumber &= (aNumber - 1); } cachedNumberOfOnes[slot] = count; return count; } numberOfOnes.displayName = "numberOfOnes"; function CPThemeAttributeEncode(aCoder, aThemeAttribute) { var values = aThemeAttribute._values, count = [values count], key = "$a" + [aThemeAttribute name]; if (count === 1) { var state = [values allKeys][0]; if (Number(state) === 0) { [aCoder encodeObject:[values objectForKey:state] forKey:key]; return YES; } } if (count >= 1) { [aCoder encodeObject:aThemeAttribute forKey:key]; return YES; } return NO; } function CPThemeAttributeDecode(aCoder, anAttributeName, aDefaultValue, aTheme, aClass) { var key = "$a" + anAttributeName; if (![aCoder containsValueForKey:key]) var attribute = [[_CPThemeAttribute alloc] initWithName:anAttributeName defaultValue:aDefaultValue]; else { var attribute = [aCoder decodeObjectForKey:key]; if (!attribute.isa || ![attribute isKindOfClass:[_CPThemeAttribute class]]) { var themeAttribute = [[_CPThemeAttribute alloc] initWithName:anAttributeName defaultValue:aDefaultValue]; [themeAttribute setValue:attribute]; attribute = themeAttribute; } } if (aTheme && aClass) [attribute setParentAttribute:[aTheme _attributeWithName:anAttributeName forClass:aClass]]; return attribute; } /* TO AUTO CREATE THESE: function bit_count(bits) { var count = 0; while (bits) { ++count; bits &= (bits - 1); } return count ; } zeros = "000000000"; function pad(string, digits) { return zeros.substr(0, digits - string.length) + string; } var str = "" str += '['; for (i = 0;i < Math.pow(2,6);++i) { str += bit_count(i) + " /*" + pad(i.toString(2),6) + "*" + "/, "; } print(str+']'); */