mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-08-25 21:17:03 +00:00
590 lines
14 KiB
Plaintext
590 lines
14 KiB
Plaintext
/*
|
|
* 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 <Foundation/CPObject.j>
|
|
@import <Foundation/CPString.j>
|
|
@import <Foundation/CPKeyedUnarchiver.j>
|
|
|
|
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");
|
|
CPThemeStateTableDataView = CPThemeState("tableDataView");
|
|
CPThemeStateSelectedDataView = CPThemeStateSelectedTableDataView = CPThemeState("selectedTableDataView");
|
|
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+']');
|
|
|
|
*/ |