Files
cappuccino/AppKit/CPTheme.j
T
2014-11-06 10:17:05 -08:00

827 lines
22 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/CPMutableArray.j>
@import <Foundation/CPString.j>
@import <Foundation/CPKeyedUnarchiver.j>
@class CPView
@class _CPThemeAttribute
var CPThemesByName = { },
CPThemeDefaultTheme = nil,
CPThemeDefaultHudTheme = nil;
/*!
@ingroup appkit
*/
@implementation CPTheme : CPObject
{
CPString _name;
CPDictionary _attributes;
}
+ (void)setDefaultTheme:(CPTheme)aTheme
{
CPThemeDefaultTheme = aTheme;
}
+ (CPTheme)defaultTheme
{
return CPThemeDefaultTheme;
}
/*!
Set the default HUD theme. If set to nil, the default described in defaultHudTheme
will be used.
*/
+ (void)setDefaultHudTheme:(CPTheme)aTheme
{
CPThemeDefaultHudTheme = aTheme;
}
/*!
The default HUD theme is (sometimes) used for windows with the CPHUDBackgroundWindowMask
style mask. The default is theme with the name of the default theme with -HUD appended
at the end.
*/
+ (CPTheme)defaultHudTheme
{
if (!CPThemeDefaultHudTheme)
CPThemeDefaultHudTheme = [CPTheme themeNamed:[[self defaultTheme] name] + "-HUD"];
return CPThemeDefaultHudTheme;
}
+ (CPTheme)themeNamed:(CPString)aName
{
return CPThemesByName[aName];
}
- (id)initWithName:(CPString)aName
{
self = [super init];
if (self)
{
_name = aName;
_attributes = @{};
CPThemesByName[_name] = self;
}
return self;
}
- (CPString)name
{
return _name;
}
/*!
Returns an array of names of themed classes defined in this theme, as found in its
ThemeDescriptors.j file.
NOTE: The names are not class names (such as "CPButton"), but the names returned
by the class' +defaultThemeClass method. For example, the name for CPCheckBox is "check-box",
as defined in CPCheckBox::themeClass.
*/
- (CPArray)classNames
{
return [_attributes allKeys];
}
/*!
Returns a dictionary of all theme attributes defined for the given class, as found in the
theme's ThemeDescriptors.j file. The keys of the dictionary are attribute names, and the values
are instances of _CPThemeAttribute.
For a description of valid values for \c aClass, see \ref attributeNamesForClass:.
@param aClass The themed class whose attributes you want to retrieve
@return A dictionary of attributes or nil
*/
- (CPDictionary)attributesForClass:(id)aClass
{
if (!aClass)
return nil;
var className = nil;
if ([aClass isKindOfClass:[CPString class]])
{
// See if it is a class name
var theClass = CPClassFromString(aClass);
if (theClass)
aClass = theClass;
else
className = aClass;
}
if (!className)
{
if ([aClass isKindOfClass:[CPView class]])
{
if ([aClass respondsToSelector:@selector(defaultThemeClass)])
className = [aClass defaultThemeClass];
else if ([aClass respondsToSelector:@selector(themeClass)])
{
CPLog.warn(@"%@ themeClass is deprecated in favor of defaultThemeClass", CPStringFromClass(aClass));
className = [aClass themeClass];
}
else
return nil;
}
else
[CPException raise:CPInvalidArgumentException reason:@"aClass must be a class object or a string."];
}
return [_attributes objectForKey:className];
}
/*!
Returns an array of names of all theme attributes defined for the given class, as found in the
theme's ThemeDescriptors.j file.
The \c aClass parameter can be one of the following:
- A class instance, for example the result of [CPCheckBox class]. The class must be a subclass
of CPView.
- A class name, for example "CPCheckBox".
- A themed class name, for example "check-box".
If \c aClass does not refer to a themed class in this theme, nil is returned.
@param aClass The themed class whose attributes you want to retrieve
@return An array of attribute names or nil
*/
- (CPDictionary)attributeNamesForClass:(id)aClass
{
var attributes = [self attributesForClass:aClass];
if (attributes)
return [attributes allKeys];
else
return [CPArray array];
}
/*!
Returns a theme attribute defined for the given class, as found in the
theme's ThemeDescriptors.j file.
\c aName should be the attribute name as you would pass to the method
CPView::valueForThemeAttribute:.
For a description of valid values for \c aClass, see \ref attributeNamesForClass:.
@param aName The name of the attribute you want to retrieve
@param aClass The themed class in which to look for the attribute
@return An instance of _CPThemeAttribute or nil
*/
- (_CPThemeAttribute)attributeWithName:(CPString)aName forClass:(id)aClass
{
var attributes = [self attributesForClass:aClass];
if (!attributes)
return nil;
return [attributes objectForKey:aName];
}
/*!
Returns the value for a theme attribute in its normal state, as defined for the given class
in the theme's ThemeDescriptors.j file.
\c aName should be the attribute name as you would pass to the method
CPView::valueForThemeAttribute:.
For a description of valid values for \c aClass, see \ref attributeNamesForClass:.
@param aName The name of the attribute whose value you want to retrieve
@param aClass The themed class in which to look for the attribute
@return A value or nil
*/
- (id)valueForAttributeWithName:(CPString)aName forClass:(id)aClass
{
return [self valueForAttributeWithName:aName inState:CPThemeStateNormal forClass:aClass];
}
/*!
Returns the value for a theme attribute in a given state, as defined for the given class
in the theme's ThemeDescriptors.j file. This is the equivalent of the method
CPView::valueForThemeAttribute:inState:, but retrieves the value from the theme definition as
opposed to a single view's current theme state.
For a description of valid values for \c aClass, see \ref attributeNamesForClass:.
@param aName The name of the attribute whose value you want to retrieve
@param aState The state qualifier for the attribute
@param aClass The themed class in which to look for the attribute
@return A value or nil
*/
- (id)valueForAttributeWithName:(CPString)aName inState:(ThemeState)aState forClass:(id)aClass
{
var attribute = [self attributeWithName:aName forClass:aClass];
if (!attribute)
return nil;
return [attribute valueForState:aState];
}
- (void)takeThemeFromObject:(id)anObject
{
var attributes = [anObject _themeAttributeDictionary],
attributeName = nil,
attributeNames = [attributes keyEnumerator],
objectThemeClass = [anObject themeClass];
while ((attributeName = [attributeNames nextObject]) !== nil)
[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 = @{};
[_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
/*!
* ThemeStates are immutable objects representing a particular ThemeState. Applications should never be creating
* ThemeStates directly but should instead use the CPThemeState function.
*/
function ThemeState(stateNames)
{
var stateNameKeys = [];
this._stateNames = {};
for (key in stateNames)
{
if (!stateNames.hasOwnProperty(key))
continue;
if (key !== 'normal')
{
this._stateNames[key] = true;
stateNameKeys.push(key);
}
}
if (stateNameKeys.length === 0)
{
stateNameKeys.push('normal');
this._stateNames['normal'] = true;
}
stateNameKeys.sort();
this._stateNameString = stateNameKeys[0];
var stateNameLength = stateNameKeys.length;
for (var stateIndex = 1; stateIndex < stateNameLength; stateIndex++)
this._stateNameString = this._stateNameString + "+" + stateNameKeys[stateIndex];
this._stateNameCount = stateNameLength;
}
ThemeState.prototype.toString = function()
{
return this._stateNameString;
}
ThemeState.prototype.hasThemeState = function(aState)
{
if (!aState || !aState._stateNames)
return false;
// We can do this in O(n) because both states have their stateNames already sorted.
for (var stateName in aState._stateNames)
{
if (!aState._stateNames.hasOwnProperty(stateName))
continue;
if (!this._stateNames[stateName])
return false;
}
return true;
}
ThemeState.prototype.isSubsetOf = function(aState)
{
if (aState._stateNameCount < this._stateNameCount)
return false;
for (var key in this._stateNames)
{
if (!this._stateNames.hasOwnProperty(key))
continue;
if (!aState._stateNames[key])
return false;
}
return true;
}
ThemeState.prototype.without = function(aState)
{
if (!aState || aState === [CPNull null])
return this;
var newStates = {};
for (var stateName in this._stateNames)
{
if (!this._stateNames.hasOwnProperty(stateName))
continue;
if (!aState._stateNames[stateName])
newStates[stateName] = true;
}
return ThemeState._cacheThemeState(new ThemeState(newStates));
}
ThemeState.prototype.and = function(aState)
{
return CPThemeState(this, aState);
}
var CPThemeStates = {};
ThemeState._cacheThemeState = function(aState)
{
// We do this caching so themeState equality works. Basically, doing CPThemeState('foo+bar') === CPThemeState('bar', 'foo') will return true.
var themeState = CPThemeStates[String(aState)];
if (themeState === undefined)
{
themeState = aState;
CPThemeStates[String(themeState)] = themeState;
}
return themeState;
}
/*!
* This method can be called in multiple ways:
* CPThemeState('state1') - creates a new CPThemeState that corresponds to the string 'state1'
* CPThemeState('state1', 'state2') - creates a new composite CPThemeState made up of both 'state1' or 'state2'
* CPThemeState('state1+state2') - The same as CPThemeState('state1', 'state2')
* CPThemeState(state1, state2) - creates a new composite CPThemeState made up of state1 and state2
* where state1 and state2 are not strings but are themselves CPThemeStates.
*/
function CPThemeState()
{
if (arguments.length < 1)
throw "CPThemeState() must be called with at least one string argument";
var themeState;
if (arguments.length === 1 && typeof arguments[0] === 'string')
{
themeState = CPThemeStates[arguments[0]];
if (themeState !== undefined)
return themeState;
}
var stateNames = {};
for (var argIndex = 0; argIndex < arguments.length; argIndex++)
{
if (arguments[argIndex] === [CPNull null] || !arguments[argIndex])
continue;
if (typeof arguments[argIndex] === 'object')
{
for (var stateName in arguments[argIndex]._stateNames)
{
if (!arguments[argIndex]._stateNames.hasOwnProperty(stateName))
continue;
stateNames[stateName] = true;
}
}
else
{
var allNames = arguments[argIndex].split('+');
for (var nameIndex = 0; nameIndex < allNames.length; nameIndex++)
stateNames[allNames[nameIndex]] = true;
}
}
themeState = ThemeState._cacheThemeState(new ThemeState(stateNames));
return themeState;
}
@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
CPThemeStateNormal = CPThemeState("normal");
CPThemeStateDisabled = CPThemeState("disabled");
CPThemeStateHovered = CPThemeState("hovered");
CPThemeStateHighlighted = CPThemeState("highlighted");
CPThemeStateSelected = CPThemeState("selected");
CPThemeStateTableDataView = CPThemeState("tableDataView");
CPThemeStateSelectedDataView = CPThemeState("selectedTableDataView");
CPThemeStateGroupRow = CPThemeState("CPThemeStateGroupRow");
CPThemeStateBezeled = CPThemeState("bezeled");
CPThemeStateBordered = CPThemeState("bordered");
CPThemeStateEditable = CPThemeState("editable");
CPThemeStateEditing = CPThemeState("editing");
CPThemeStateVertical = CPThemeState("vertical");
CPThemeStateDefault = CPThemeState("default");
CPThemeStateCircular = CPThemeState("circular");
CPThemeStateAutocompleting = CPThemeState("autocompleting");
CPThemeStateFirstResponder = CPThemeState("firstResponder");
CPThemeStateMainWindow = CPThemeState("mainWindow");
CPThemeStateKeyWindow = CPThemeState("keyWindow");
CPThemeStateControlSizeRegular = CPThemeState("controlSizeRegular");
CPThemeStateControlSizeSmall = CPThemeState("controlSizeSmall");
CPThemeStateControlSizeMini = CPThemeState("controlSizeMini");
@implementation _CPThemeAttribute : CPObject
{
CPString _name;
id _defaultValue;
CPDictionary _values @accessors(readonly, getter=values);
JSObject _cache;
_CPThemeAttribute _themeDefaultAttribute;
}
- (id)initWithName:(CPString)aName defaultValue:(id)aDefaultValue
{
self = [super init];
if (self)
{
_cache = { };
_name = aName;
_defaultValue = aDefaultValue;
_values = @{};
}
return self;
}
- (CPString)name
{
return _name;
}
- (id)defaultValue
{
return _defaultValue;
}
- (BOOL)hasValues
{
return [_values count] > 0;
}
- (void)setValue:(id)aValue
{
_cache = {};
if (aValue === undefined || aValue === nil)
_values = @{};
else
_values = @{ String(CPThemeStateNormal): aValue };
}
- (void)setValue:(id)aValue forState:(ThemeState)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:(ThemeState)aState
{
var stateName = String(aState),
value = _cache[stateName];
// This can be nil.
if (value !== undefined)
return value;
value = [_values objectForKey:stateName];
if (value === undefined || value === nil)
{
// If this is a composite state, find the closest partial subset match.
if (aState._stateNameCount > 1)
{
var states = [_values allKeys],
count = states.length,
largestThemeState = 0;
while (count--)
{
var stateObject = CPThemeState(states[count]);
if (stateObject.isSubsetOf(aState) && stateObject._stateNameCount > largestThemeState)
{
value = [_values objectForKey:states[count]];
largestThemeState = stateObject._stateNameCount;
}
}
}
// 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 = [_themeDefaultAttribute valueForState:aState];
if (value === undefined || value === nil)
{
value = _defaultValue;
// Class theme attributes cannot use nil because it's a dictionary.
// So transform CPNull into nil.
if (value === [CPNull null])
value = nil;
}
_cache[stateName] = value;
return value;
}
- (void)setParentAttribute:(_CPThemeAttribute)anAttribute
{
if (_themeDefaultAttribute === anAttribute)
return;
_cache = { };
_themeDefaultAttribute = 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"];
_defaultValue = [aCoder decodeObjectForKey:@"defaultValue"];
_values = @{};
if ([aCoder containsValueForKey:@"value"])
{
var state = String(CPThemeStateNormal);
if ([aCoder containsValueForKey:@"state"])
state = [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:key];
}
}
}
return self;
}
- (void)encodeWithCoder:(CPCoder)aCoder
{
[aCoder encodeObject:_name forKey:@"name"];
[aCoder encodeObject:_defaultValue forKey:@"defaultValue"];
var keys = [_values allKeys],
count = keys.length;
if (count === 1)
{
var onlyKey = keys[0];
if (onlyKey !== String(CPThemeStateNormal))
[aCoder encodeObject:onlyKey forKey:@"state"];
[aCoder encodeObject:[_values objectForKey:onlyKey] forKey:@"value"];
}
else
{
var encodedValues = @{};
while (count--)
{
var key = keys[count];
[encodedValues setObject:[_values objectForKey:key] forKey:key];
}
[aCoder encodeObject:encodedValues forKey:@"values"];
}
}
@end
function CPThemeAttributeEncode(aCoder, aThemeAttribute)
{
var values = aThemeAttribute._values,
count = [values count],
key = "$a" + [aThemeAttribute name];
if (count === 1)
{
var state = [values allKeys][0];
if (state === String(CPThemeStateNormal))
{
[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 || !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+']');
*/