Additions to the theme API to open it up to developers and provide access to theme attributes defined in ThemeDescriptors.j:

- Current theme blend is available via CPApplication -themeBlend.

- List of themes is available via CPApplication -themes.

- CPThemeBlend adds the following methods:

themes
themeNames

- CPTheme adds a number of methods to access theme attributes:

classNames
attributesForClass:
attributeNamesForClass:
attributeWithName:forClass:  (was _attributeWithName:forClass:)
valueForAttributeWithName:forClass
valueForAttributeWithName:inState:forClass:

- _CPThemeAttribute -values was added to access the dictionary of attribute values.

- Theme browser application that demonstrates the API.

- In the course of making the test app, added the function CGInsetEqualToInset().
This commit is contained in:
Aparajita Fishman authored and Klaas Pieter Annema committed 2010-09-16 10:28:06 +02:00
1 parent 9b8cadb91c
commit 0c4ce1e756
16 files changed
+1571 -15

No files matched your search

+4 -1
View File
@@ -108,6 +108,8 @@ CPRunContinuesResponse = -1002;
CPImage _applicationIconImage;
CPPanel _aboutPanel;
CPThemeBlend _themeBlend @accessors(property=themeBlend);
}
/*!
@@ -1129,6 +1131,7 @@ var _CPAppBootstrapperActions = nil;
+ (void)blendDidFinishLoading:(CPThemeBlend)aThemeBlend
{
[[CPApplication sharedApplication] setThemeBlend:aThemeBlend];
[CPTheme setDefaultTheme:[CPTheme themeNamed:[CPApplication defaultThemeName]]];
[self performActions];
@@ -1226,7 +1229,7 @@ var _CPAppBootstrapperActions = nil;
+ (void)reset
{
_CPAppBootstrapperActions = nil;
_CPAppBootstrapperActions = nil;
}
@end
+145 -8
View File
@@ -95,9 +95,105 @@ var CPThemesByName = { },
return _name;
}
- (_CPThemeAttribute)_attributeWithName:(CPString)aName forClass:(CPString)aClass
/*!
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' +themeClass method. For example, the name for CPCheckBox is "check-box",
as defined in CPCheckBox::themeClass.
*/
- (CPArray)classNames
{
var attributes = [_attributes objectForKey:aClass];
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(themeClass)])
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;
@@ -105,6 +201,47 @@ var CPThemesByName = { },
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:(CPThemeState)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],
@@ -278,10 +415,10 @@ CPThemeStateCircular = CPThemeState("circular");
{
CPString _name;
id _defaultValue;
CPDictionary _values;
CPDictionary _values @accessors(readonly, getter=values);
JSObject _cache;
CPThemeAttribute _parentAttribute;
_CPThemeAttribute _parentAttribute;
}
- (id)initWithName:(CPString)aName defaultValue:(id)aDefaultValue
@@ -402,7 +539,7 @@ CPThemeStateCircular = CPThemeState("circular");
return value;
}
- (void)setParentAttribute:(CPThemeAttribute)anAttribute
- (void)setParentAttribute:(_CPThemeAttribute)anAttribute
{
if (_parentAttribute === anAttribute)
return;
@@ -411,7 +548,7 @@ CPThemeStateCircular = CPThemeState("circular");
_parentAttribute = anAttribute;
}
- (CPThemeAttribute)attributeMergedWithAttribute:(_CPThemeAttribute)anAttribute
- (_CPThemeAttribute)attributeMergedWithAttribute:(_CPThemeAttribute)anAttribute
{
var mergedAttribute = [[_CPThemeAttribute alloc] initWithName:_name defaultValue:_defaultValue];
@@ -575,7 +712,7 @@ function CPThemeAttributeDecode(aCoder, anAttributeName, aDefaultValue, aTheme,
}
if (aTheme && aClass)
[attribute setParentAttribute:[aTheme _attributeWithName:anAttributeName forClass:aClass]];
[attribute setParentAttribute:[aTheme attributeWithName:anAttributeName forClass:aClass]];
return attribute;
}
@@ -609,4 +746,4 @@ for (i = 0;i < Math.pow(2,6);++i)
}
print(str+']');
*/
*/
+27 -4
View File
@@ -33,7 +33,7 @@
@implementation CPThemeBlend : CPObject
{
CPBundle _bundle;
CPArray _themes @accessors(readonly, getter=themes);
CPArray _themes;
id _loadDelegate;
}
@@ -49,6 +49,28 @@
return self;
}
/*!
Returns an array of names of the keyed theme archives that make up this blend.
Each item in the array will have the extension ".keyedtheme".
*/
- (CPArray)themes
{
return _themes;
}
/*!
Returns an array of names of the themes that make up this blend.
*/
- (CPArray)themeNames
{
var names = [];
for (var i = 0; i < _themes.length; ++i)
names.push(_themes[i].substring(0, _themes[i].indexOf(".keyedtheme")));
return names;
}
- (void)loadWithDelegate:(id)aDelegate
{
_loadDelegate = aDelegate;
@@ -58,12 +80,13 @@
- (void)bundleDidFinishLoading:(CPBundle)aBundle
{
var themes = [_bundle objectForInfoDictionaryKey:@"CPKeyedThemes"],
count = themes.length;
_themes = [_bundle objectForInfoDictionaryKey:@"CPKeyedThemes"];
var count = _themes.length;
while (count--)
{
var path = [aBundle pathForResource:themes[count]],
var path = [aBundle pathForResource:_themes[count]],
unarchiver = [[_CPThemeKeyedUnarchiver alloc]
initForReadingWithData:[[CPURL URLWithString:path] staticResourceData]
bundle:_bundle];
+2 -2
View File
@@ -2323,7 +2323,7 @@ setBoundsOrigin:
var attributeName = attributes[count--],
attribute = [[_CPThemeAttribute alloc] initWithName:attributeName defaultValue:attributes[count]];
[attribute setParentAttribute:[theme _attributeWithName:attributeName forClass:themeClass]];
[attribute setParentAttribute:[theme attributeWithName:attributeName forClass:themeClass]];
_themeAttributes[attributeName] = attribute;
}
@@ -2354,7 +2354,7 @@ setBoundsOrigin:
for (var attributeName in _themeAttributes)
if (_themeAttributes.hasOwnProperty(attributeName))
[_themeAttributes[attributeName] setParentAttribute:[theme _attributeWithName:attributeName forClass:themeClass]];
[_themeAttributes[attributeName] setParentAttribute:[theme attributeWithName:attributeName forClass:themeClass]];
[self setNeedsLayout];
[self setNeedsDisplay:YES];
+1
View File
@@ -63,6 +63,7 @@
#define _CGInsetMakeCopy(anInset) _CGInsetMake(anInset.top, anInset.right, anInset.bottom, anInset.left)
#define _CGInsetMakeZero() _CGInsetMake(0, 0, 0, 0)
#define _CGInsetIsEmpty(anInset) ((anInset).top === 0 && (anInset).right === 0 && (anInset).bottom === 0 && (anInset).left === 0)
#define _CGInsetEqualToInset(lhsInset, rhsInset) ((lhsInset).top === (rhsInset).top && (lhsInset).right === (rhsInset).right && (lhsInset).bottom === (rhsInset).bottom && (lhsInset).left === (rhsInset).left)
// DEPRECATED
#define _CGPointCreateCopy(aPoint) _CGPointMake(aPoint.x, aPoint.y)
+1
View File
@@ -69,6 +69,7 @@ _function(CGInsetMake(top, right, bottom, left))
_function(CGInsetMakeZero())
_function(CGInsetMakeCopy(anInset))
_function(CGInsetIsEmpty(anInset))
_function(CGInsetEqualToInset(lhsInset, rhsInset))
CGMinXEdge = 0;
CGMinYEdge = 1;
+355
View File
@@ -0,0 +1,355 @@
/*
* AppController.j
* CPButtonImageTest
*
* Created by Aparajita Fishman on August 31, 2010.
*/
@import <Foundation/Foundation.j>
@import <AppKit/AppKit.j>
@import "SCMultiLineLabel.j"
@import "SCString.j"
var BrowserColumnTheme = 0,
BrowserColumnClass = 1,
BrowserColumnAttribute = 2,
BrowserColumnState = 3,
ColumnTitles = ["Theme", "Class", "Attribute", "State"],
ImageDescriptionFormat = "<%s> {\n filename: \"%s\",\n size: { width:%f, height:%f }\n}",
ValueLabelTemplate = "Attribute value#0#:# <span style=\"font-weight:normal\">($0 / $1 / $2 / $3):</span>#";
@implementation AppController : CPObject
{
CPWindow theWindow;
CPBrowser browser;
CPArray rootNodes;
SCMultiLineLabel valueLabel;
SCMultiLineLabel valueView;
}
- (void)awakeFromCib
{
SharedController = self;
}
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
{
[theWindow setFullPlatformWindow:YES];
var contentView = [theWindow contentView],
themeNames = [[[CPApplication sharedApplication] themeBlend] themeNames],
frame = CGRectMake(20, 20, 800, 20),
browserFrame = CGRectMake(20, 50, 800, 300),
label = [[SCMultiLineLabel alloc] initWithFrame:frame];
[label setFont:[CPFont boldSystemFontOfSize:14]];
[label setStringValue:@"Browse theme attributes from ThemeDescriptors.j"];
[contentView addSubview:label];
rootNodes = [];
browser = [[CPBrowser alloc] initWithFrame:browserFrame];
for (var i = 0; i < themeNames.length; ++i)
{
var node = [[ThemeNode alloc] initWithBrowser:browser value:[CPTheme themeNamed:themeNames[i]] type:NodeTypeTheme];
rootNodes.push(node);
}
[browser setLastColumn:3];
[browser setMinColumnWidth:200];
[browser setTarget:self];
[browser setAction:@selector(browserClicked:)];
[browser setDelegate:self];
var box = [CPBox boxEnclosingView:browser];
[contentView addSubview:box];
var frame = CGRectMake(CGRectGetMinX(browserFrame), CGRectGetMaxY(browserFrame) + 20, CGRectGetWidth(browserFrame), 17),
valueLabel = [[SCMultiLineLabel alloc] initWithFrame:frame],
fonts = [[CPFontManager sharedFontManager] availableFonts],
frame = CGRectMake(CGRectGetMinX(frame), CGRectGetMaxY(frame) + 7, CGRectGetWidth(browserFrame), 250);
[valueLabel setStyled:YES];
[valueLabel setFont:[CPFont boldSystemFontOfSize:12]];
[valueLabel setColor:[CPColor colorWithHexString:@"444444"]];
[valueLabel setStringValue:[SCString stringWithTemplate:ValueLabelTemplate, @"", @"", @"", @""]];
[contentView addSubview:valueLabel];
valueView = [[SCMultiLineLabel alloc] initWithFrame:frame];
if ([fonts indexOfObjectIdenticalTo:@"Courier"])
[valueView setFont:[CPFont fontWithName:@"Courier" size:12]];
else if ([fonts indexOfObjectIdenticalTo:@"Courier New"])
[valueView setFont:[CPFont fontWithName:@"Courier New" size:12]];
[valueView setContentInset:CGInsetMake(7, 7, 7, 7)];
box = [CPBox boxEnclosingView:valueView];
[contentView addSubview:box];
}
- (void)browserClicked:(id)sender
{
var column = [browser selectedColumn];
if (column != BrowserColumnState)
return;
var row = [browser selectedRowInColumn:BrowserColumnTheme],
theme = [[browser itemAtRow:row inColumn:BrowserColumnTheme] value],
row = [browser selectedRowInColumn:BrowserColumnClass],
className = [[browser itemAtRow:row inColumn:BrowserColumnClass] value],
row = [browser selectedRowInColumn:BrowserColumnAttribute],
attribute = [[browser itemAtRow:row inColumn:BrowserColumnAttribute] value],
row = [browser selectedRowInColumn:BrowserColumnState],
state = [[browser itemAtRow:row inColumn:BrowserColumnState] value]["key"];
var value = [theme valueForAttributeWithName:[attribute name] inState:state forClass:className];
[valueLabel setStringValue:[SCString stringWithTemplate:ValueLabelTemplate, [theme name], className, [attribute name], CPThemeStateName(state)]]
[self showValueDescription:value];
}
- (void)showValueDescription:(id)aValue
{
var description = "";
if (!aValue.isa)
{
if (typeof(aValue) === "object")
{
if (aValue.hasOwnProperty("width") && aValue.hasOwnProperty("height"))
description = [CPString stringWithFormat:@"CGSize: { width:%f, height:%f }", aValue.width, aValue.height];
else if (aValue.hasOwnProperty("x") && aValue.hasOwnProperty("y"))
description = [CPString stringWithFormat:@"CGPoint: { x:%f, y:%f }", aValue.x, aValue.y];
else if (aValue.hasOwnProperty("origin") && aValue.hasOwnProperty("size"))
description = [CPString stringWithFormat:@"CGRect: { x:%f, y:%f }, { width:%f, height:%f }", aValue.origin.x, aValue.origin.y, aValue.size.width, aValue.size.height];
else if (aValue.hasOwnProperty("top") && aValue.hasOwnProperty("right") && aValue.hasOwnProperty("bottom") && aValue.hasOwnProperty("left"))
description = [CPString stringWithFormat:@"CGInset: { top:%f, right:%f, bottom:%f, left:%f }", aValue.top, aValue.right, aValue.bottom, aValue.left];
else
{
description = "Object\n{\n";
for (var property in aValue)
{
if (aValue.hasOwnProperty(property))
description += " " + property + ":" + aValue[property] + "\n";
}
description += "}";
}
}
else
description = "Unknown object";
}
else if ([aValue isKindOfClass:[CPImage class]])
description = [self imageDescription:aValue];
else if ([aValue isKindOfClass:[CPColor class]])
description = [self colorDescription:aValue];
else
description = [aValue description];
[valueView setStringValue:description];
}
- (CPString)imageDescription:(CPImage)anImage
{
var filename = [anImage filename],
size = [anImage size];
if (filename.indexOf("data:") === 0)
{
var index = filename.indexOf(",");
if (index > 0)
filename = [CPString stringWithFormat:@"%s,%s...%s", filename.substr(0, index), filename.substr(index + 1, 10), filename.substr(filename.length - 10)];
else
filename = "data:<unknown type>";
}
return [CPString stringWithFormat:ImageDescriptionFormat, [anImage className], filename, size.width, size.height];
}
- (CPString)colorDescription:(CPColor)aColor
{
var patternImage = [aColor patternImage];
if (!patternImage || !([patternImage isThreePartImage] || [patternImage isNinePartImage]))
return [aColor description];
var description = "<CPColor> {\n",
slices = [patternImage imageSlices];
if ([patternImage isThreePartImage])
description += " orientation: " + ([patternImage isVertical] ? "vertical" : "horizontal") + ",\n";
description += " patternImage: [\n";
for (var i = 0; i < slices.length; ++i)
{
var imageDescription = [self imageDescription:slices[i]];
description += imageDescription.replace(/^/mg, " ") + ",\n";
}
description = description.substr(0, description.length - 2) + "\n ]\n}";
return description;
}
- (CPString)browser:(id)aBrowser titleOfColumn:(int)column
{
return ColumnTitles[column];
}
- (id)browser:(id)aBrowser numberOfChildrenOfItem:(id)anItem
{
if (!anItem)
return [rootNodes count];
return [[anItem children] count];
}
- (id)browser:(id)aBrowser child:(int)index ofItem:(id)anItem
{
if (!anItem)
return [rootNodes objectAtIndex:index];
return [[anItem children] objectAtIndex:index];
}
- (id)browser:(id)aBrowser objectValueForItem:(id)anItem
{
return [anItem objectValue];
}
- (id)browser:(id)aBrowser isLeafItem:(id)anItem
{
return [anItem type] === NodeTypeValue;
}
@end
var NodeTypeTheme = 0,
NodeTypeClass = 1,
NodeTypeAttribute = 2,
NodeTypeValue = 3;
@implementation ThemeNode : CPObject
{
CPBrowser browser;
int type @accessors(readonly);
id value @accessors(readonly);
CPArray children @accessors(readonly);
}
- (id)initWithBrowser:(CPBrowser)aBrowser value:(id)aValue type:(int)aType
{
if (self = [super init])
{
browser = aBrowser;
value = aValue;
type = aType;
children = nil;
}
return self;
}
- (id)objectValue
{
switch (type)
{
case NodeTypeTheme:
return [value name];
case NodeTypeClass:
return value;
case NodeTypeAttribute:
return [value name];
case NodeTypeValue:
return CPThemeStateName(value["key"]);
}
}
- (CPArray)children
{
if (children)
return children;
children = [];
switch (type)
{
case NodeTypeTheme:
var classes = [value classNames].sort();
for (var i = 0; i < classes.length; ++i)
children.push([[ThemeNode alloc] initWithBrowser:browser value:classes[i] type:NodeTypeClass]);
break;
case NodeTypeClass:
var selectedRow = [browser selectedRowInColumn:0],
theme = [[browser itemAtRow:selectedRow inColumn:0] value],
attributes = [theme attributesForClass:value],
keys = [attributes allKeys].sort();
// NOTE: I could use [attributes objectForKey:keys[i]] in the loop, but I want
// to do code coverage of CPTheme.
for (var i = 0; i < keys.length; ++i)
children.push([[ThemeNode alloc] initWithBrowser:browser value:[theme attributeWithName:keys[i] forClass:value] type:NodeTypeAttribute]);
break;
case NodeTypeAttribute:
var selectedRow = [browser selectedRowInColumn:0],
theme = [[browser itemAtRow:selectedRow inColumn:0] value],
selectedRow = [browser selectedRowInColumn:1],
className = [[browser itemAtRow:selectedRow inColumn:1] value],
attributeValues = [value values],
keys = [attributeValues allKeys];
[keys sortUsingFunction:attributeCompareFunc context:nil];
// NOTE: I could use [attributes objectForKey:keys[i]] in the loop, but I want
// to do code coverage of CPTheme.
for (var i = 0; i < keys.length; ++i)
{
var nodeValue =
{
key:keys[i],
attribute:[theme valueForAttributeWithName:[value name] inState:keys[i] forClass:className]
};
children.push([[ThemeNode alloc] initWithBrowser:browser value:nodeValue type:NodeTypeValue]);
}
break;
case NodeTypeValue:
break;
}
return children;
}
@end
var attributeCompareFunc = function(lhs, rhs)
{
var lhsInt = parseInt(lhs),
rhsInt = parseInt(rhs);
if (lhsInt === rhsInt)
return CPOrderedSame;
else if (lhsInt > rhsInt)
return CPOrderedDescending;
else
return CPOrderedAscending;
};
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Main cib file base name</key>
<string>MainMenu.cib</string>
<key>CPBundleName</key>
<string>ThemeBrowser</string>
<key>CPPrincipalClass</key>
<string>CPApplication</string>
</dict>
</plist>
@@ -0,0 +1 @@
280NPLIST;1.0;D;K;4;$topD;K;18;CPCibObjectDataKeyD;K;6;CP$UIDd;1;2E;E;K;8;$objectsA;S;5;$nullD;K;10;$classnameS;16;_CPCibObjectDataK;8;$classesA;S;16;_CPCibObjectDataS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;1;1E;K;28;_CPCibObjectDataNamesKeysKeyD;K;6;CP$UIDd;1;4E;K;30;_CPCibObjectDataNamesValuesKeyD;K;6;CP$UIDd;1;5E;K;30;_CPCibObjectDataClassesKeysKeyD;K;6;CP$UIDd;1;6E;K;32;_CPCibObjectDataClassesValuesKeyD;K;6;CP$UIDd;1;7E;K;30;_CPCibObjectDataConnectionsKeyD;K;6;CP$UIDd;1;8E;K;28;_CPCibObjectDataFrameworkKeyD;K;6;CP$UIDd;1;9E;K;26;_CPCibObjectDataNextOidKeyD;K;6;CP$UIDd;2;10E;K;30;_CPCibObjectDataObjectsKeysKeyD;K;6;CP$UIDd;2;11E;K;32;_CPCibObjectDataObjectsValuesKeyD;K;6;CP$UIDd;2;12E;K;26;_CPCibObjectDataOidKeysKeyD;K;6;CP$UIDd;2;13E;K;28;_CPCibObjectDataOidValuesKeyD;K;6;CP$UIDd;2;14E;K;28;_CPCibObjectDataFileOwnerKeyD;K;6;CP$UIDd;2;16E;K;33;_CPCibObjectDataVisibleWindowsKeyD;K;6;CP$UIDd;2;18E;E;D;K;10;$classnameS;7;CPArrayK;8;$classesA;S;7;CPArrayS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;19E;D;K;6;CP$UIDd;2;21E;D;K;6;CP$UIDd;2;16E;D;K;6;CP$UIDd;2;22E;D;K;6;CP$UIDd;2;24E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;25E;D;K;6;CP$UIDd;2;26E;D;K;6;CP$UIDd;2;27E;D;K;6;CP$UIDd;2;28E;D;K;6;CP$UIDd;2;29E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;31E;D;K;6;CP$UIDd;2;32E;E;E;S;16;IBCocoaFrameworkD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;19E;D;K;6;CP$UIDd;2;21E;D;K;6;CP$UIDd;2;24E;D;K;6;CP$UIDd;2;22E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;16E;D;K;6;CP$UIDd;2;16E;D;K;6;CP$UIDd;2;21E;D;K;6;CP$UIDd;2;16E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;10;$classnameS;18;_CPCibCustomObjectK;8;$classesA;S;18;_CPCibCustomObjectS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;15E;K;27;_CPCibCustomObjectClassNameD;K;6;CP$UIDd;2;33E;E;D;K;10;$classnameS;5;CPSetK;8;$classesA;S;5;CPSetS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;17E;K;15;CPSetObjectsKeyD;K;6;CP$UIDd;2;34E;E;D;K;6;$classD;K;6;CP$UIDd;2;15E;K;27;_CPCibCustomObjectClassNameD;K;6;CP$UIDd;2;33E;E;D;K;10;$classnameS;20;_CPCibWindowTemplateK;8;$classesA;S;20;_CPCibWindowTemplateS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;20E;K;30;_CPCibWindowTemplateMaxSizeKeyD;K;6;CP$UIDd;2;35E;K;32;_CPCibWindowTemplateViewClassKeyD;K;6;CP$UIDd;1;0E;K;34;_CPCibWindowTemplateWindowClassKeyD;K;6;CP$UIDd;2;36E;K;33;_CPCibWindowTemplateWindowRectKeyD;K;6;CP$UIDd;2;37E;K;30;_CPCibWindowTempatStyleMaskKeyD;K;6;CP$UIDd;2;38E;K;34;_CPCibWindowTemplateWindowTitleKeyD;K;6;CP$UIDd;2;39E;K;33;_CPCibWindowTemplateWindowViewKeyD;K;6;CP$UIDd;2;24E;E;D;K;6;$classD;K;6;CP$UIDd;2;15E;K;27;_CPCibCustomObjectClassNameD;K;6;CP$UIDd;2;40E;E;D;K;10;$classnameS;6;CPViewK;8;$classesA;S;6;CPViewS;11;CPResponderS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;23E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;41E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;2;42E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;2;42E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;43E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;44E;E;S;11;ApplicationS;6;WindowS;12;File's OwnerS;14;App ControllerS;12;Content ViewD;K;10;$classnameS;20;CPCibOutletConnectorK;8;$classesA;S;20;CPCibOutletConnectorS;14;CPCibConnectorS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;30E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;16E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;22E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;45E;E;D;K;6;$classD;K;6;CP$UIDd;2;30E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;22E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;21E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;46E;E;S;13;CPApplicationD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;21E;E;E;S;28;{1.79769e+308, 1.79769e+308}S;8;CPWindowS;23;{{408, 63}, {569, 418}}d;1;7S;12;CPTheme TestS;13;AppControllerd;1;0S;20;{{0, 0}, {569, 418}}d;2;18S;6;normalS;8;delegateS;9;theWindowE;K;9;$archiverS;15;CPKeyedArchiverK;8;$versionS;6;100000E;
@@ -0,0 +1,165 @@
<?xml version="1.0" encoding="UTF-8"?>
<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="8.00">
<data>
<int key="IBDocument.SystemTarget">1060</int>
<string key="IBDocument.SystemVersion">10F569</string>
<string key="IBDocument.InterfaceBuilderVersion">788</string>
<string key="IBDocument.AppKitVersion">1038.29</string>
<string key="IBDocument.HIToolboxVersion">461.00</string>
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="NS.object.0">788</string>
</object>
<array class="NSMutableArray" key="IBDocument.EditedObjectIDs"/>
<array key="IBDocument.PluginDependencies">
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
</array>
<dictionary class="NSMutableDictionary" key="IBDocument.Metadata"/>
<array class="NSMutableArray" key="IBDocument.RootObjects" id="1048">
<object class="NSCustomObject" id="1021">
<string key="NSClassName">NSApplication</string>
</object>
<object class="NSCustomObject" id="1014">
<string key="NSClassName">FirstResponder</string>
</object>
<object class="NSCustomObject" id="1050">
<string key="NSClassName">NSApplication</string>
</object>
<object class="NSWindowTemplate" id="972006081">
<int key="NSWindowStyleMask">7</int>
<int key="NSWindowBacking">2</int>
<string key="NSWindowRect">{{408, 547}, {569, 418}}</string>
<int key="NSWTFlags">1946157056</int>
<string key="NSWindowTitle">CPTheme Browser</string>
<string key="NSWindowClass">NSWindow</string>
<nil key="NSViewClass"/>
<string key="NSWindowContentMaxSize">{1.79769e+308, 1.79769e+308}</string>
<object class="NSView" key="NSWindowView" id="439893737">
<reference key="NSNextResponder"/>
<int key="NSvFlags">274</int>
<string key="NSFrameSize">{569, 418}</string>
<reference key="NSSuperview"/>
<reference key="NSWindow"/>
</object>
<string key="NSScreenRect">{{0, 0}, {1680, 1028}}</string>
<string key="NSMaxSize">{1.79769e+308, 1.79769e+308}</string>
</object>
<object class="NSCustomObject" id="635946545">
<string key="NSClassName">AppController</string>
</object>
</array>
<object class="IBObjectContainer" key="IBDocument.Objects">
<array class="NSMutableArray" key="connectionRecords">
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">delegate</string>
<reference key="source" ref="1021"/>
<reference key="destination" ref="635946545"/>
</object>
<int key="connectionID">451</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">theWindow</string>
<reference key="source" ref="635946545"/>
<reference key="destination" ref="972006081"/>
</object>
<int key="connectionID">1090</int>
</object>
</array>
<object class="IBMutableOrderedSet" key="objectRecords">
<array key="orderedObjects">
<object class="IBObjectRecord">
<int key="objectID">0</int>
<array key="object" id="0"/>
<reference key="children" ref="1048"/>
<nil key="parent"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">-2</int>
<reference key="object" ref="1021"/>
<reference key="parent" ref="0"/>
<string key="objectName">File's Owner</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">-1</int>
<reference key="object" ref="1014"/>
<reference key="parent" ref="0"/>
<string key="objectName">First Responder</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">-3</int>
<reference key="object" ref="1050"/>
<reference key="parent" ref="0"/>
<string key="objectName">Application</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">371</int>
<reference key="object" ref="972006081"/>
<array class="NSMutableArray" key="children">
<reference ref="439893737"/>
</array>
<reference key="parent" ref="0"/>
<string key="objectName">Window</string>
</object>
<object class="IBObjectRecord">
<int key="objectID">372</int>
<reference key="object" ref="439893737"/>
<array class="NSMutableArray" key="children"/>
<reference key="parent" ref="972006081"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">450</int>
<reference key="object" ref="635946545"/>
<reference key="parent" ref="0"/>
</object>
</array>
</object>
<dictionary class="NSMutableDictionary" key="flattenedProperties">
<string key="-3.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="371.IBEditorWindowLastContentRect">{{531, 479}, {569, 418}}</string>
<string key="371.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="371.IBWindowTemplateEditedContentRect">{{531, 479}, {569, 418}}</string>
<integer value="1" key="371.NSWindowTemplate.visibleAtLaunch"/>
<string key="371.editorWindowContentRectSynchronizationRect">{{33, 99}, {480, 360}}</string>
<string key="372.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
</dictionary>
<dictionary class="NSMutableDictionary" key="unlocalizedProperties"/>
<nil key="activeLocalization"/>
<dictionary class="NSMutableDictionary" key="localizations"/>
<nil key="sourceID"/>
<int key="maxID">1186</int>
</object>
<object class="IBClassDescriber" key="IBDocument.Classes">
<array class="NSMutableArray" key="referencedPartialClassDescriptions">
<object class="IBPartialClassDescription">
<string key="className">AppController</string>
<string key="superclassName">NSObject</string>
<dictionary class="NSMutableDictionary" key="outlets">
<string key="browser">NSBrowser</string>
<string key="theWindow">NSWindow</string>
</dictionary>
<dictionary class="NSMutableDictionary" key="toOneOutletInfosByName">
<object class="IBToOneOutletInfo" key="browser">
<string key="name">browser</string>
<string key="candidateClassName">NSBrowser</string>
</object>
<object class="IBToOneOutletInfo" key="theWindow">
<string key="name">theWindow</string>
<string key="candidateClassName">NSWindow</string>
</object>
</dictionary>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBUserSource</string>
<string key="minorKey"/>
</object>
</object>
</array>
</object>
<int key="IBDocument.localizationMode">0</int>
<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
<bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
<string key="IBDocument.LastKnownRelativeProjectPath">../registration.xcodeproj</string>
<int key="IBDocument.defaultPropertyAccessControl">3</int>
</data>
</archive>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,141 @@
/*
* LPMultiLineLabel.j
*
* Based on LPMultiLineTextField by Ludwig Pettersson.
*
*/
@import <AppKit/CPTextField.j>
@implementation SCMultiLineLabel : CPView
{
id _DOMTextareaElement;
CPString _stringValue;
CGInset _contentInset @accessors(readonly, getter=contentInset);
CPColor _color @accessors(readonly, getter=color);
CPFont _font @accessors(readonly, getter=font);
BOOL _styled @accessors(readonly, getter=isStyled);
}
- (DOMElement)_DOMTextareaElement
{
if (!_DOMTextareaElement)
{
_DOMTextareaElement = document.createElement(_styled ? "div" : "textarea");
_DOMTextareaElement.style.position = @"absolute";
_DOMTextareaElement.style.background = @"none";
_DOMTextareaElement.style.border = @"0";
_DOMTextareaElement.style.outline = @"0";
_DOMTextareaElement.style.zIndex = @"100";
_DOMTextareaElement.style.resize = @"none";
_DOMTextareaElement.style.padding = @"0";
_DOMTextareaElement.style.margin = @"0";
_DOMTextareaElement.style.overflowX = @"hidden";
_DOMTextareaElement.onblur = function(){
[[CPTextFieldInputOwner window] makeFirstResponder:nil];
CPTextFieldInputOwner = nil;
};
self._DOMElement.appendChild(_DOMTextareaElement);
}
return _DOMTextareaElement;
}
- (id)initWithFrame:(CGRect)aFrame
{
if (self = [super initWithFrame:aFrame])
{
var theme = [CPTheme defaultTheme];
_contentInset = CGInsetMakeZero();
_color = [theme valueForAttributeWithName:@"text-color" forClass:[CPTextField class]];
_font = [theme valueForAttributeWithName:@"font" forClass:[CPTextField class]];
}
return self;
}
- (void)layoutSubviews
{
[super layoutSubviews];
var contentView = [self layoutEphemeralSubviewNamed:@"content-view"
positioned:CPWindowAbove
relativeToEphemeralSubviewNamed:@"bezel-view"];
[contentView setHidden:YES];
var DOMElement = [self _DOMTextareaElement],
bounds = [self bounds];
DOMElement.style.paddingTop = _contentInset.top + @"px";
DOMElement.style.paddingBottom = _contentInset.bottom + @"px";
DOMElement.style.paddingLeft = _contentInset.left + @"px";
DOMElement.style.paddingRight = _contentInset.right + @"px";
DOMElement.style.width = (CGRectGetWidth(bounds) - _contentInset.left - _contentInset.right) + @"px";
DOMElement.style.height = (CGRectGetHeight(bounds) - _contentInset.top - _contentInset.bottom) + @"px";
DOMElement.style.color = [_color cssString];
DOMElement.style.font = [_font cssString];
if (_styled)
DOMElement.innerHTML = _stringValue || @"";
else
DOMElement.value = _stringValue || @"";
}
- (CPString)stringValue
{
return (!!_DOMTextareaElement) ? _DOMTextareaElement.value : @"";
}
- (void)setStringValue:(CPString)aString
{
if (_stringValue === aString)
return;
_stringValue = aString;
[self setNeedsLayout];
}
- (void)setContentInset:(CGInset)inset
{
if (CGInsetEqualToInset(_contentInset, inset))
return;
_contentInset = CGInsetMakeCopy(inset);
[self setNeedsLayout];
}
- (void)setColor:(CPColor)aColor
{
if ([_color isEqual:aColor])
return;
_color = aColor;
[self setNeedsLayout];
}
- (void)setFont:(CPFont)aFont
{
if ([_font isEqual:aFont])
return;
_font = aFont;
[self setNeedsLayout];
}
- (void)setStyled:(BOOL)flag
{
flag = !!flag;
if (_styled === flag)
return;
_styled = flag;
_DOMTextareaElement = nil;
[self setNeedsLayout];
}
@end
+553
View File
@@ -0,0 +1,553 @@
/*
* SCString.j
*
* Created by Aparajita on 8/9/10.
* Copyright Victory-Heart Productions 2010. All rights reserved.
*/
@import <Foundation/CPString.j>
@implementation SCString : CPObject
/*
A string template engine.
Long ago I was poking through the resources used by the Macintosh Finder and saw strings like this:
The folder ^1 contains #2#no#^2# file|2||s|#2## for ^3 byte|3||s|#
I realized they had created a meta-language to allow a single string to allow dynamic argument
substitution and express two types of linguistic variations:
- Zero/non-zero
- Singular/plural
In most languages, the syntax changes based on those two factors. For example, let's suppose we want
to tell users how many messages they have after logging in. In natural English,
there are three distinct forms:
- Zero: "You have no messages"
- Singular: "You have 1 message"
- Plural: "You have 7 messages"
In French it would be something like this:
- Zero: "Vous n'avez aucun message"
- Singular: "Vous avez 1 message"
- Plural: "Vous avez 7 messages"
You could encode these linguistic rules in your code,
but that doesn't work very well if you need to translate your application into languages
that don't follow the same rules. Or you could just give up and do something like this:
You have 0 message(s)
If that solution doesn't satisfy you, then stringWithTemplate is the solution.
stringWithTemplate is an Objective-J port of the meta-language used in the original Mac Finder,
but with an extended and more familiar syntax.
You apply one or more arguments to a template. Argument placeholders indicated by "$key" or "${key}"
in the template are replaced with the corresponding replacement arguments. Replacement arguments
can be passed in several ways:
- You may pass one or more unnamed parameters following the last named parameter.
- If there is only unnamed parameter and it is a CPDictionary, <key> should be the key in that dictionary.
- If there is only unnamed parameter and it is an array, <key> should be a numeric, zero-based
index into that array.
- If there is only unnamed parameter and it is an object (but not a CPDictionary),
<key> should be a property of that object.
- Otherwise <key> should be a numeric, zero-based index of an unnamed parameter, with 0 being
the first unnamed parameter, 1 the second, and so on.
Note that because stringWithTemplate supports indexing, the <key> can be a number in addition to an identifier.
If <key> does not exist in an argument dictionary or is out of the range of positional arguments,
the argument placeholder is replaced with an empty string.
When using the ${key} syntax for replacement parameters, you may add a format separated by a semicolon
like this:
${key:format}
If the replacement argument is a date and the Date object has had the function dateFormat()
added to its prototype, then [argsDict objectForKey:key].dateFormat(format)
is used to format it. One such dateFormat() function is available here:
http://code.google.com/p/flexible-js-formatting/
For the available date formats of this library, see:
http://www.xaprb.com/articles/date-formatting-demo.html
The formatting options of flexible-js-formatting are basically the same as the date() function in php:
http://php.net/manual/en/function.date.php
If the argument is not a date, the equivalent of:
[SCKit stringWithFormat:format, [argsDict objectForKey:key]]
is performed.
For example:
${total:.2f}
${date:F j, Y}
is the same as:
[SCKit stringWithFormat:@"%.2f", [argsDict objectForKey:@"total"]]
[argsDict objectForKey:@"date"].dateFormat("F j, Y")
In addition to a format, you may also add a default value after a vertical bar:
${key|default} or ${key:format|default}
The default is used if [argsDict objectForKey:key] evaluates to false, which means nil, an empty string, etc.
Inline formats and defaults are nice, but the real power of stringWithTemplate is the meta-language that
allows you to deal with linguistic rules and alternatives in a single template.
In addition to argument replacement, you can specify alternate subsections of
the template to be used. Which subsection is used depends on the value of the replacement arguments.
Alternate subsections are marked in the form:
<delim><key><delim><alt1><delim><alt2><delim>
where <delim> is a delimiter character, <key> is in the same format as a replacement argument,
<alt1> is the "true" choice, and <alt2> is the "false" choice. The delimiter characters are taken from the first
two characters of the delimiters parameter in stringWithTemplate:delimiters:. If you use stringWithTemplate:
or if the length of the delimiters < 2, the default delimiters are "#|".
Three passes are made over the template:
1. Zero/non-zero (false/true) subsections (<delim> == delimiters[0]) are matched. If [argsDict objectForKey:key]
evaluates to zero, <alt1> is used, else <alt2> is used. An argument is considered zero if:
- It is a number and == 0
- !arg == true
2. Singular/plural subsections (<delim> == delimiters[1]) are matched, and if [argsDict objectForKey:key]
evaluates to 1, <alt1> is used, else <alt2> is used. An argument evaluates to 1 if:
- It is a number and == 1
- It is a string and can be converted to a number which == 1.0
- It is an object with a length property which == 1
3. Any occurrence of $key or ${key} is replaced with the corresponding replacement argument according
to the rules stated above.
IMPORTANT: Subsections with the same delimiter may not be nested.
If all of the matching rules for a subsection fail, the entire subsection is replaced with an empty string.
ESCAPING/CHANGING THE DELIMITERS
If your template string uses one of the defaults delimiters ("#" or "|") or the key marker ("$")
in its regular text, you have two options:
- Precede the literal delimiter characters with backslashes
- Change the delimiters by using stringWithTemplate:delimiters: instead of stringWithTemplate:
- Use "$$" to represent a literal "$"
EXAMPLES
Let's look at some examples to see how we might use stringWithTemplate. We'll start with a simple example. We
have a song object parsed from JSON for which we want to display the date it was composed and the name of the
composer:
var args = {composer:song.composer, date:song.dateComposed},
text = [SCKit stringWithTemplate:@"Composed by $composer on $dateComposed", args]
RESULT: "Composed by Pat Metheny on 2005-03-30"
Simple enough. Now we want to format the date to be a little more friendly:
text = [SCKit stringWithTemplate:@"Composed by $composer on ${dateComposed:F j, Y}", args];
RESULT: "Composed by Pat Metheny on March 30, 2005"
That's better. Now we want to translate it into French:
text = [SCKit stringWithTemplate:@"Composée par $composer le ${dateComposed:j F, Y}", args];
RESULT: "Composée par Pat Metheny le 30 March, 2005"
Now we realize that that we may not know the composition date, in which case the date is nil.
So we use the default value:
text = [SCKit stringWithTemplate:@"Composed by $composer ${dateComposed:\\o\\n F j, Y|(date unknown)}", args];
RESULT: "Composed by Pat Metheny (date unknown)" # if dateComposed is nil
Note that we backslash escaped the characters in "on" so they would not be interpreted as format
characters.
Now we want to add "and translated" if the song has a translation by the composer:
var args = {composer:song.composer, date:song.dateComposed, translated:song.hasTranslation},
template = @"Composed #translated##and translated #by $composer ${dateComposed:\o\n F j, Y|(date unknown)}",
text = [SCKit stringWithTemplate:template, args];
Note that we are using the zero/non-zero selector as a false/true selector in this case.
Just for reference, here is how the template would look if you wanted to pass the arguments as positional
arguments:
var template = @"Composed #0##and translated #by $1 ${2:\o\n F j, Y|(date unknown)}",
text = [SCKit stringWithTemplate:template, song.hasTranslation, song.composer, song.dateComposed];
Finally, we find that sometimes we don't know the exact date of composition, but we know the month
or year. In that case we have a text date like "February 1980". So our logic ends up like this:
- If there is a text date, display that
- If there is an exact date, format and display that
- Otherwise display "(date unknown)"
Here's how we encode all of this into the stringWithTemplate:
var args = {
composer:song.composer,
date:song.dateComposed,
textDate:song.textDate,
translated:song.hasTranslation
};
var template = @"Composed #translated##and translated #by $composer " +
@"#textDate#${dateComposed:\o\n F j, Y|(date unknown)}#$textDate#",
text = [SCKit stringWithTemplate:template, args];
Now let's see how the singular/plural selector works. Returning to a variation on the first example at the top
of this doc, we want to encode these three variations into a single template:
You don't have any messages
You have only 1 message
You have 7 messages
Here is the template:
var template = @"You #count#don't ##have #count#any#|count|only ||$count# message|count||s|.",
text = [SCKit stringWithTemplate:template, {count:messageCount};
Let's break down the pattern:
#count#don't ##
If $count evaluates to zero, insert "don't ".
#count#any#|count|only ||$count#
Here we have a one selector and argument placeholder nested inside a zero selector.
- The zero selector is evaluated first. If $count evaluates to zero, substitute "any",
else substitute "|count|only ||$count".
- Then the one selector is evaluated. If the zero selector returned "|count|only ||$count",
that is parsed. If $count evaluates to 1, "only " is substituted, otherwise nothing.
- Finally the argument placeholders are substituted.
message|count||s|
This is a common idiom for expressing singular/plural. If count evaluates to 1, add no
suffix to "message", otherwise add "s" as a suffix to make it plural.
To help you picture what is happening, let's view the steps of the transformation of the string given
a count of zero, 1 and 7.
count == 0
1. Zero selector applied -> "You don't have any message|count||s|."
2. One selector applied -> "You don't have any messages."
3. Arguments replaced -> "You don't have any messages."
count == 1
1. Zero selector applied -> "You have |count|only ||$count message|count||s|."
2. One selector applied -> "You have only $count message."
3. Arguments replaced -> "You have only 1 message."
count == 7
1. Zero selector applied -> "You have |count|only ||$count message|count||s|."
2. One selector applied -> "You have $count messages."
3. Arguments replaced -> "You have only 7 messages."
Hopefully that should be enough to give you an idea how to use stringWithTemplate. Enjoy!
*/
+ (id)stringWithTemplate:(CPString)template, ...
{
var args = [template, SCKit.TemplateDefaultDelimiters];
return SCKit.stringWithTemplate.apply(self, args.concat(Array.prototype.slice.call(arguments, 3)));
}
+ (id)stringWithTemplate:(CPString)template delimiters:(CPString)delimiters, ...
{
var args = [template, delimiters];
return SCKit.stringWithTemplate.apply(self, args.concat(Array.prototype.slice.call(arguments, 4)));
}
@end
SCKit = (function() {
var my = {},
// Template argument RE
TemplateArgREPattern =
'\\$' + // template argument indicator followed by...
'(?:' +
'(\\$)' + // another arg indicator, indicating a literal
'|(\\w+)' + // or an identifer
'|{' + // or the start of a braced identifier, followed by...
'(\\w+)' + // an identifier
'(?::([^}|]+))?' + // followed by an optional format
'(?:\\|([^}]+))?' + // followed by an optional default
'}' + // end of braced identifier
'|(\\S+)' + // invalid stuff following arg indicator
')',
TemplateArgRE = new RegExp(TemplateArgREPattern, "g"),
// Tests for an integer or float
DigitRE = new RegExp("^(?:\\d+|\\d*\\.\\d+)$");
// Default subsection delimiters. The first character delimits zero/non-zero subsections,
// the second character delimits singular/plural subsections.
TemplateDefaultDelimiters = "#|";
// Default argument delimiter
TemplateDefaultArgDelimiter = "$";
function _selectorReplace(text, selector, delimiter, argsDict)
{
// First see if the first part of the text is <identifier>#.
// If not, continue the after leading delimiter.
var re = new RegExp("^[" + delimiter + "](\\w+)[" + delimiter + "]"),
match = re.exec(text);
if (!match)
return {text:delimiter, nextIndex:1};
var identifier = match[1],
valid = NO,
lastChar = "",
alternatives = ["", ""],
haveAlternative = NO,
lastIndex = match[0].length;
// Now scan for the two alternatives
for (var alternative = 0; alternative < 2; ++alternative)
{
for (; lastIndex < text.length; ++lastIndex)
{
var c = text.charAt(lastIndex);
if (c === delimiter && lastChar !== "\\")
{
++lastIndex;
haveAlternative = YES;
break;
}
else if (c !== "\\")
alternatives[alternative] += c;
lastChar = c;
}
// Make sure we didn't exhaust the source before finding the alternative.
if (!haveAlternative)
return {text:text, endIndex:text.length};
}
// Get the specified argument, convert to a string
var arg = [argsDict objectForKey:identifier],
type = typeof arg,
result = nil;
if (selector === 0)
{
var isZero;
switch (type)
{
case "string":
if (arg.length === 0)
isZero = true;
else
isZero = DigitRE.test(arg) && parseFloat(arg) === 0;
break;
case "number":
isZero = arg === 0;
break;
case "boolean":
isZero = arg === false;
break;
case "object":
isZero = arg === nil || (arg.hasOwnProperty("length") && arg.length === 0);
break;
default:
result = "";
}
if (result === nil)
result = isZero ? alternatives[0] : alternatives[1];
}
else
{
var isOne;
switch (type)
{
case "string":
isOne = arg.length && DigitRE.test(arg) && parseFloat(arg) === 1;
break;
case "number":
isOne = arg === 1;
break;
case "boolean":
isOne = arg === true;
break;
case "object":
isOne = arg !== nil && arg.hasOwnProperty("length") && arg.length === 1;
break;
default:
result = "";
}
if (result === nil)
result = isOne ? alternatives[0] : alternatives[1];
}
return {text:result, nextIndex:lastIndex};
}
// Helper function for replacing args
function _convert(value, format, defaultValue)
{
if (!value)
return defaultValue ? defaultValue : "";
if (format)
{
if (value.constructor === Date)
{
if (Date.prototype.dateFormat)
return value.dateFormat(format);
else
return value.toLocaleString();
}
else
{
format = "%" + format;
return ObjectiveJ.sprintf(format, value);
}
}
else
return String(value, 10);
}
my.stringWithTemplate = function(/* CPString */ template, /* CPString */ delimiters, /* CPArray|CPDictionary */ args)
{
if (!template)
return "";
if (!delimiters || delimiters.length < 2)
delimiters = TemplateDefaultDelimiters;
// Normalize the arguments into a dictionary
var argsDict = null,
argsArray = [];
if (arguments.length < 3)
return "";
if (arguments.length === 3)
{
var arg = arguments[2];
if (arg.hasOwnProperty("isa"))
{
if ([arg isKindOfClass:[CPArray class]])
argsArray = arg;
else if ([arg isKindOfClass:[CPDictionary class]])
argsDict = arg;
}
else if (arg.constructor === Array)
argsArray = arg;
else if (typeof(arg) === "object")
argsDict = [CPDictionary dictionaryWithJSObject:arg];
else
argsArray = [arguments[2]];
}
else
{
argsArray = Array.prototype.slice.call(arguments, 2);
}
if (!argsDict)
{
if (argsArray.length === 0)
return template;
argsDict = [CPDictionary dictionary];
for (var i = 0; i < argsArray.length; ++i)
[argsDict setObject:argsArray[i] forKey:String(i, 10)];
}
var text = template;
// We have a zero/non-zero selector and one/non-one selector
for (var selector = 0; selector <= 1; ++selector)
{
var delim = delimiters.charAt(selector),
lastChar = "";
for (var i = 0; i < text.length; ++i)
{
var c = text.charAt(i);
if (c === delim)
{
if (lastChar !== "\\")
{
var leftContext = text.slice(0, i),
rightContext = text.slice(i),
replacement = _selectorReplace(rightContext, selector, delim, argsDict);
text = leftContext + replacement.text + rightContext.slice(replacement.nextIndex);
i += replacement.text.length - 1;
}
}
else if (c === "\\")
text = text.slice(0, i) + text.slice(i + 1);
lastChar = c;
}
}
// Define as a closure so we can access argsDict
var argReplace = function(str, escaped, named, braced, format, defaultValue, invalid)
{
named = named || braced;
if (named)
return _convert([argsDict objectForKey:named], format, defaultValue);
if (escaped)
return TemplateDefaultArgDelimiter;
// FIXME: raise
return "";
};
return text.replace(TemplateArgRE, argReplace);
};
return my;
}());
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns = "http://www.w3.org/1999/xhtml" xml:lang = "en" lang = "en">
<!--
index-debug.html
TableCibTest
Created by Francisco Tolmasky on July 5, 2009.
Copyright 2009, 280 North, Inc. All rights reserved.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7" />
<title>Theme Browser</title>
<script type = "text/javascript">
OBJJ_MAIN_FILE = "main.j";
OBJJ_INCLUDE_PATHS = ["Frameworks/Debug", "Frameworks", "SomethingElse"];
</script>
<script src = "Frameworks/Debug/Objective-J/Objective-J.js" type = "text/javascript"></script>
<script type="text/javascript">
objj_msgSend_reset();
// DEBUG OPTIONS:
// Uncomment to enable printing of backtraces on exceptions:
//objj_msgSend_decorate(objj_backtrace_decorator);
</script>
<style type = "text/css">
body{margin:0; padding:0;}
#container {position: absolute; top:50%; left:50%;}
#content {width:800px; text-align:center; margin-left: -400px; height:50px; margin-top:-25px; line-height: 50px;}
#content {font-family: "Helvetica", "Arial", sans-serif; font-size: 18px; color: black; text-shadow: 0px 1px 0px white; }
#loadgraphic {margin-right: 0.2em; margin-bottom:-2px;}
</style>
<!--[if lt IE 7]>
<STYLE type="text/css">
#container { position: relative; top: 50%; }
#content { position: relative;}
</STYLE>
<![endif]-->
</head>
<body style="">
<div id="loadingcontainer" style=" background-color: #eeeeee; overflow:hidden; width:100%; height:100%; position: absolute; top: 0; left: 0;">
<script type = "text/javascript">
document.write("<div id='container'><p id='content'>" +
"<img id='loadgraphic' width='16' height='16' src='Resources/spinner.gif' /> " +
"Loading Theme Browser...</p></div>");
</script>
<noscript>
<div id="container">
<div style="width: 440px; padding: 10px 25px 20px 25px; font-family: sans-serif; background-color: #ffffff; position: relative; left: -245px; top: -120px; text-align: center; -moz-border-radius: 20px; -webkit-border-radius: 20px; color: #555555">
<p style="line-height: 1.4em;">JavaScript is required for this site to work correctly but is either disabled or not supported by your browser.</p>
<p style="font-size:120%; padding:10px;"><a href="http://cappuccino.org/noscript">Show me how to enable JavaScript</a></p>
<p style="font-size:80%;">You may want to upgrade to a newer browser while you're at it:</p>
<ul style="margin:0;padding:0; text-align: center; font-size:80%;" >
<li style="display: inline;"><a href="http://www.apple.com/safari/download/">Safari</a></li>
<li style="display: inline;"><a href="http://www.mozilla.com/en-US/firefox/">Firefox</a></li>
<li style="display: inline;"><a href="http://www.google.com/chrome/">Chrome</a></li>
<li style="display: inline;"><a href="http://www.opera.com/download/">Opera</a></li>
<li style="display: inline;"><a href="http://www.microsoft.com/windows/downloads/ie/getitnow.mspx">Internet Explorer</a></li>
</ul>
</div>
</div>
</noscript>
</div>
</body>
</html>
+68
View File
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns = "http://www.w3.org/1999/xhtml" xml:lang = "en" lang = "en">
<!--
index.html
TableCibTest
Created by Francisco Tolmasky on July 5, 2009.
Copyright 2009, 280 North, Inc. All rights reserved.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7" />
<title>Theme Browser</title>
<script type = "text/javascript">
OBJJ_MAIN_FILE = "main.j";
</script>
<script src = "Frameworks/Objective-J/Objective-J.js" type = "text/javascript"></script>
<style type = "text/css">
body{margin:0; padding:0;}
#container {position: absolute; top:50%; left:50%;}
#content {width:800px; text-align:center; margin-left: -400px; height:50px; margin-top:-25px; line-height: 50px;}
#content {font-family: "Helvetica", "Arial", sans-serif; font-size: 18px; color: black; text-shadow: 0px 1px 0px white; }
#loadgraphic {margin-right: 0.2em; margin-bottom:-2px;}
</style>
<!--[if lt IE 7]>
<STYLE type="text/css">
#container { position: relative; top: 50%; }
#content { position: relative;}
</STYLE>
<![endif]-->
</head>
<body style="">
<div id="loadingcontainer" style=" background-color: #eeeeee; overflow:hidden; width:100%; height:100%; position: absolute; top: 0; left: 0;">
<script type = "text/javascript">
document.write("<div id='container'><p id='content'>" +
"<img id='loadgraphic' width='16' height='16' src='Resources/spinner.gif' /> " +
"Loading Theme Browser...</p></div>");
</script>
<noscript>
<div id="container">
<div style="width: 440px; padding: 10px 25px 20px 25px; font-family: sans-serif; background-color: #ffffff; position: relative; left: -245px; top: -120px; text-align: center; -moz-border-radius: 20px; -webkit-border-radius: 20px; color: #555555">
<p style="line-height: 1.4em;">JavaScript is required for this site to work correctly but is either disabled or not supported by your browser.</p>
<p style="font-size:120%; padding:10px;"><a href="http://cappuccino.org/noscript">Show me how to enable JavaScript</a></p>
<p style="font-size:80%;">You may want to upgrade to a newer browser while you're at it:</p>
<ul style="margin:0;padding:0; text-align: center; font-size:80%;" >
<li style="display: inline;"><a href="http://www.apple.com/safari/download/">Safari</a></li>
<li style="display: inline;"><a href="http://www.mozilla.com/en-US/firefox/">Firefox</a></li>
<li style="display: inline;"><a href="http://www.google.com/chrome/">Chrome</a></li>
<li style="display: inline;"><a href="http://www.opera.com/download/">Opera</a></li>
<li style="display: inline;"><a href="http://www.microsoft.com/windows/downloads/ie/getitnow.mspx">Internet Explorer</a></li>
</ul>
</div>
</div>
</noscript>
</div>
</body>
</html>
+18
View File
@@ -0,0 +1,18 @@
/*
* AppController.j
* TableCibTest
*
* Created by Francisco Tolmasky on July 5, 2009.
* Copyright 2009, 280 North, Inc. All rights reserved.
*/
@import <Foundation/Foundation.j>
@import <AppKit/AppKit.j>
@import "AppController.j"
function main(args, namedArgs)
{
CPApplicationMain(args, namedArgs);
}