axmol/cocos2dx/platform/ios/FontLabel/ZAttributedString.m

598 lines
24 KiB
Objective-C

//
// ZAttributedString.m
// FontLabel
//
// Created by Kevin Ballard on 9/22/09.
// Copyright 2009 Zynga Game Networks. All rights reserved.
//
#import "ZAttributedString.h"
#import "ZAttributedStringPrivate.h"
@interface ZAttributedString ()
- (NSUInteger)indexOfEffectiveAttributeRunForIndex:(NSUInteger)index;
- (NSDictionary *)attributesAtIndex:(NSUInteger)index effectiveRange:(NSRangePointer)aRange uniquingOnName:(NSString *)attributeName;
- (NSDictionary *)attributesAtIndex:(NSUInteger)index longestEffectiveRange:(NSRangePointer)aRange
inRange:(NSRange)rangeLimit uniquingOnName:(NSString *)attributeName;
@end
@interface ZAttributedString ()
@property (nonatomic, readonly) NSArray *attributes;
@end
@implementation ZAttributedString
@synthesize string = _buffer;
@synthesize attributes = _attributes;
- (id)initWithAttributedString:(ZAttributedString *)attr {
NSParameterAssert(attr != nil);
if ((self = [super init])) {
_buffer = [attr->_buffer mutableCopy];
_attributes = [[NSMutableArray alloc] initWithArray:attr->_attributes copyItems:YES];
}
return self;
}
- (id)initWithString:(NSString *)str {
return [self initWithString:str attributes:nil];
}
- (id)initWithString:(NSString *)str attributes:(NSDictionary *)attributes {
if ((self = [super init])) {
_buffer = [str mutableCopy];
_attributes = [[NSMutableArray alloc] initWithObjects:[ZAttributeRun attributeRunWithIndex:0 attributes:attributes], nil];
}
return self;
}
- (id)init {
return [self initWithString:@"" attributes:nil];
}
- (id)initWithCoder:(NSCoder *)decoder {
if ((self = [super init])) {
_buffer = [[decoder decodeObjectForKey:@"buffer"] mutableCopy];
_attributes = [[decoder decodeObjectForKey:@"attributes"] mutableCopy];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:_buffer forKey:@"buffer"];
[aCoder encodeObject:_attributes forKey:@"attributes"];
}
- (id)copyWithZone:(NSZone *)zone {
return [self retain];
}
- (id)mutableCopyWithZone:(NSZone *)zone {
return [(ZMutableAttributedString *)[ZMutableAttributedString allocWithZone:zone] initWithAttributedString:self];
}
- (NSUInteger)length {
return [_buffer length];
}
- (NSString *)description {
NSMutableArray *components = [NSMutableArray arrayWithCapacity:[_attributes count]*2];
NSRange range = NSMakeRange(0, 0);
for (NSUInteger i = 0; i <= [_attributes count]; i++) {
range.location = NSMaxRange(range);
ZAttributeRun *run;
if (i < [_attributes count]) {
run = [_attributes objectAtIndex:i];
range.length = run.index - range.location;
} else {
run = nil;
range.length = [_buffer length] - range.location;
}
if (range.length > 0) {
[components addObject:[NSString stringWithFormat:@"\"%@\"", [_buffer substringWithRange:range]]];
}
if (run != nil) {
NSMutableArray *attrDesc = [NSMutableArray arrayWithCapacity:[run.attributes count]];
for (id key in run.attributes) {
[attrDesc addObject:[NSString stringWithFormat:@"%@: %@", key, [run.attributes objectForKey:key]]];
}
[components addObject:[NSString stringWithFormat:@"{%@}", [attrDesc componentsJoinedByString:@", "]]];
}
}
return [NSString stringWithFormat:@"%@", [components componentsJoinedByString:@" "]];
}
- (id)attribute:(NSString *)attributeName atIndex:(NSUInteger)index effectiveRange:(NSRangePointer)aRange {
NSParameterAssert(attributeName != nil);
return [[self attributesAtIndex:index effectiveRange:aRange uniquingOnName:attributeName] objectForKey:attributeName];
}
- (id)attribute:(NSString *)attributeName atIndex:(NSUInteger)index longestEffectiveRange:(NSRangePointer)aRange inRange:(NSRange)rangeLimit {
NSParameterAssert(attributeName != nil);
return [[self attributesAtIndex:index longestEffectiveRange:aRange inRange:rangeLimit uniquingOnName:attributeName] objectForKey:attributeName];
}
- (ZAttributedString *)attributedSubstringFromRange:(NSRange)aRange {
if (NSMaxRange(aRange) > [_buffer length]) {
@throw [NSException exceptionWithName:NSRangeException reason:@"range was outisde of the attributed string" userInfo:nil];
}
ZMutableAttributedString *newStr = [self mutableCopy];
if (aRange.location > 0) {
[newStr deleteCharactersInRange:NSMakeRange(0, aRange.location)];
}
if (NSMaxRange(aRange) < [_buffer length]) {
[newStr deleteCharactersInRange:NSMakeRange(aRange.length, [_buffer length] - NSMaxRange(aRange))];
}
return [newStr autorelease];
}
- (NSDictionary *)attributesAtIndex:(NSUInteger)index effectiveRange:(NSRangePointer)aRange {
return [NSDictionary dictionaryWithDictionary:[self attributesAtIndex:index effectiveRange:aRange uniquingOnName:nil]];
}
- (NSDictionary *)attributesAtIndex:(NSUInteger)index longestEffectiveRange:(NSRangePointer)aRange inRange:(NSRange)rangeLimit {
return [NSDictionary dictionaryWithDictionary:[self attributesAtIndex:index longestEffectiveRange:aRange inRange:rangeLimit uniquingOnName:nil]];
}
#if Z_BLOCKS
// Warning: this code has not been tested. The only guarantee is that it compiles.
- (void)enumerateAttribute:(NSString *)attrName inRange:(NSRange)enumerationRange options:(ZAttributedStringEnumerationOptions)opts
usingBlock:(void (^)(id, NSRange, BOOL*))block {
if (opts & ZAttributedStringEnumerationLongestEffectiveRangeNotRequired) {
[self enumerateAttributesInRange:enumerationRange options:opts usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) {
id value = [attrs objectForKey:attrName];
if (value != nil) {
block(value, range, stop);
}
}];
} else {
__block id oldValue = nil;
__block NSRange effectiveRange = NSMakeRange(0, 0);
[self enumerateAttributesInRange:enumerationRange options:opts usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) {
id value = [attrs objectForKey:attrName];
if (oldValue == nil) {
oldValue = value;
effectiveRange = range;
} else if (value != nil && [oldValue isEqual:value]) {
// combine the attributes
effectiveRange = NSUnionRange(effectiveRange, range);
} else {
BOOL innerStop = NO;
block(oldValue, effectiveRange, &innerStop);
if (innerStop) {
*stop = YES;
oldValue = nil;
} else {
oldValue = value;
}
}
}];
if (oldValue != nil) {
BOOL innerStop = NO; // necessary for the block, but unused
block(oldValue, effectiveRange, &innerStop);
}
}
}
- (void)enumerateAttributesInRange:(NSRange)enumerationRange options:(ZAttributedStringEnumerationOptions)opts
usingBlock:(void (^)(NSDictionary*, NSRange, BOOL*))block {
// copy the attributes so we can mutate the string if necessary during enumeration
// also clip the array during copy to only the subarray of attributes that cover the requested range
NSArray *attrs;
if (NSEqualRanges(enumerationRange, NSMakeRange(0, 0))) {
attrs = [NSArray arrayWithArray:_attributes];
} else {
// in this binary search, last is the first run after the range
NSUInteger first = 0, last = [_attributes count];
while (last > first+1) {
NSUInteger pivot = (last + first) / 2;
ZAttributeRun *run = [_attributes objectAtIndex:pivot];
if (run.index < enumerationRange.location) {
first = pivot;
} else if (run.index >= NSMaxRange(enumerationRange)) {
last = pivot;
}
}
attrs = [_attributes subarrayWithRange:NSMakeRange(first, last-first)];
}
if (opts & ZAttributedStringEnumerationReverse) {
NSUInteger end = [_buffer length];
for (ZAttributeRun *run in [attrs reverseObjectEnumerator]) {
BOOL stop = NO;
NSUInteger start = run.index;
// clip to enumerationRange
start = MAX(start, enumerationRange.location);
end = MIN(end, NSMaxRange(enumerationRange));
block(run.attributes, NSMakeRange(start, end - start), &stop);
if (stop) break;
end = run.index;
}
} else {
NSUInteger start = 0;
ZAttributeRun *run = [attrs objectAtIndex:0];
NSInteger offset = 0;
NSInteger oldLength = [_buffer length];
for (NSUInteger i = 1;;i++) {
NSUInteger end;
if (i >= [attrs count]) {
end = oldLength;
} else {
end = [[attrs objectAtIndex:i] index];
}
BOOL stop = NO;
NSUInteger clippedStart = MAX(start, enumerationRange.location);
NSUInteger clippedEnd = MIN(end, NSMaxRange(enumerationRange));
block(run.attributes, NSMakeRange(clippedStart + offset, clippedEnd - start), &stop);
if (stop || i >= [attrs count]) break;
start = end;
NSUInteger newLength = [_buffer length];
offset += (newLength - oldLength);
oldLength = newLength;
}
}
}
#endif
- (BOOL)isEqualToAttributedString:(ZAttributedString *)otherString {
return ([_buffer isEqualToString:otherString->_buffer] && [_attributes isEqualToArray:otherString->_attributes]);
}
- (BOOL)isEqual:(id)object {
return [object isKindOfClass:[ZAttributedString class]] && [self isEqualToAttributedString:(ZAttributedString *)object];
}
#pragma mark -
- (NSUInteger)indexOfEffectiveAttributeRunForIndex:(NSUInteger)index {
NSUInteger first = 0, last = [_attributes count];
while (last > first + 1) {
NSUInteger pivot = (last + first) / 2;
ZAttributeRun *run = [_attributes objectAtIndex:pivot];
if (run.index > index) {
last = pivot;
} else if (run.index < index) {
first = pivot;
} else {
first = pivot;
break;
}
}
return first;
}
- (NSDictionary *)attributesAtIndex:(NSUInteger)index effectiveRange:(NSRangePointer)aRange uniquingOnName:(NSString *)attributeName {
if (index >= [_buffer length]) {
@throw [NSException exceptionWithName:NSRangeException reason:@"index beyond range of attributed string" userInfo:nil];
}
NSUInteger runIndex = [self indexOfEffectiveAttributeRunForIndex:index];
ZAttributeRun *run = [_attributes objectAtIndex:runIndex];
if (aRange != NULL) {
aRange->location = run.index;
runIndex++;
if (runIndex < [_attributes count]) {
aRange->length = [[_attributes objectAtIndex:runIndex] index] - aRange->location;
} else {
aRange->length = [_buffer length] - aRange->location;
}
}
return run.attributes;
}
- (NSDictionary *)attributesAtIndex:(NSUInteger)index longestEffectiveRange:(NSRangePointer)aRange
inRange:(NSRange)rangeLimit uniquingOnName:(NSString *)attributeName {
if (index >= [_buffer length]) {
@throw [NSException exceptionWithName:NSRangeException reason:@"index beyond range of attributed string" userInfo:nil];
} else if (NSMaxRange(rangeLimit) > [_buffer length]) {
@throw [NSException exceptionWithName:NSRangeException reason:@"rangeLimit beyond range of attributed string" userInfo:nil];
}
NSUInteger runIndex = [self indexOfEffectiveAttributeRunForIndex:index];
ZAttributeRun *run = [_attributes objectAtIndex:runIndex];
if (aRange != NULL) {
if (attributeName != nil) {
id value = [run.attributes objectForKey:attributeName];
NSUInteger endRunIndex = runIndex+1;
runIndex--;
// search backwards
while (1) {
if (run.index <= rangeLimit.location) {
break;
}
ZAttributeRun *prevRun = [_attributes objectAtIndex:runIndex];
id prevValue = [prevRun.attributes objectForKey:attributeName];
if (prevValue == value || (value != nil && [prevValue isEqual:value])) {
runIndex--;
run = prevRun;
} else {
break;
}
}
// search forwards
ZAttributeRun *endRun = nil;
while (endRunIndex < [_attributes count]) {
ZAttributeRun *nextRun = [_attributes objectAtIndex:endRunIndex];
if (nextRun.index >= NSMaxRange(rangeLimit)) {
endRun = nextRun;
break;
}
id nextValue = [nextRun.attributes objectForKey:attributeName];
if (nextValue == value || (value != nil && [nextValue isEqual:value])) {
endRunIndex++;
} else {
endRun = nextRun;
break;
}
}
aRange->location = MAX(run.index, rangeLimit.location);
aRange->length = MIN((endRun ? endRun.index : [_buffer length]), NSMaxRange(rangeLimit)) - aRange->location;
} else {
// with no attribute name, we don't need to do any real searching,
// as we already guarantee each run has unique attributes.
// just make sure to clip the range to the rangeLimit
aRange->location = MAX(run.index, rangeLimit.location);
ZAttributeRun *endRun = (runIndex+1 < [_attributes count] ? [_attributes objectAtIndex:runIndex+1] : nil);
aRange->length = MIN((endRun ? endRun.index : [_buffer length]), NSMaxRange(rangeLimit)) - aRange->location;
}
}
return run.attributes;
}
- (void)dealloc {
[_buffer release];
[_attributes release];
[super dealloc];
}
@end
@interface ZMutableAttributedString ()
- (void)cleanupAttributesInRange:(NSRange)range;
- (NSRange)rangeOfAttributeRunsForRange:(NSRange)range;
- (void)offsetRunsInRange:(NSRange )range byOffset:(NSInteger)offset;
@end
@implementation ZMutableAttributedString
- (id)copyWithZone:(NSZone *)zone {
return [(ZAttributedString *)[ZAttributedString allocWithZone:zone] initWithAttributedString:self];
}
- (void)addAttribute:(NSString *)name value:(id)value range:(NSRange)range {
range = [self rangeOfAttributeRunsForRange:range];
for (ZAttributeRun *run in [_attributes subarrayWithRange:range]) {
[run.attributes setObject:value forKey:name];
}
[self cleanupAttributesInRange:range];
}
- (void)addAttributes:(NSDictionary *)attributes range:(NSRange)range {
range = [self rangeOfAttributeRunsForRange:range];
for (ZAttributeRun *run in [_attributes subarrayWithRange:range]) {
[run.attributes addEntriesFromDictionary:attributes];
}
[self cleanupAttributesInRange:range];
}
- (void)appendAttributedString:(ZAttributedString *)str {
[self insertAttributedString:str atIndex:[_buffer length]];
}
- (void)deleteCharactersInRange:(NSRange)range {
NSRange runRange = [self rangeOfAttributeRunsForRange:range];
[_buffer replaceCharactersInRange:range withString:@""];
[_attributes removeObjectsInRange:runRange];
for (NSUInteger i = runRange.location; i < [_attributes count]; i++) {
ZAttributeRun *run = [_attributes objectAtIndex:i];
ZAttributeRun *newRun = [[ZAttributeRun alloc] initWithIndex:(run.index - range.length) attributes:run.attributes];
[_attributes replaceObjectAtIndex:i withObject:newRun];
[newRun release];
}
[self cleanupAttributesInRange:NSMakeRange(runRange.location, 0)];
}
- (void)insertAttributedString:(ZAttributedString *)str atIndex:(NSUInteger)idx {
[self replaceCharactersInRange:NSMakeRange(idx, 0) withAttributedString:str];
}
- (void)removeAttribute:(NSString *)name range:(NSRange)range {
range = [self rangeOfAttributeRunsForRange:range];
for (ZAttributeRun *run in [_attributes subarrayWithRange:range]) {
[run.attributes removeObjectForKey:name];
}
[self cleanupAttributesInRange:range];
}
- (void)replaceCharactersInRange:(NSRange)range withAttributedString:(ZAttributedString *)str {
NSRange replaceRange = [self rangeOfAttributeRunsForRange:range];
NSInteger offset = [str->_buffer length] - range.length;
[_buffer replaceCharactersInRange:range withString:str->_buffer];
[_attributes replaceObjectsInRange:replaceRange withObjectsFromArray:str->_attributes];
NSRange newRange = NSMakeRange(replaceRange.location, [str->_attributes count]);
[self offsetRunsInRange:newRange byOffset:range.location];
[self offsetRunsInRange:NSMakeRange(NSMaxRange(newRange), [_attributes count] - NSMaxRange(newRange)) byOffset:offset];
[self cleanupAttributesInRange:NSMakeRange(newRange.location, 0)];
[self cleanupAttributesInRange:NSMakeRange(NSMaxRange(newRange), 0)];
}
- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str {
[self replaceCharactersInRange:range withAttributedString:[[[ZAttributedString alloc] initWithString:str] autorelease]];
}
- (void)setAttributedString:(ZAttributedString *)str {
[_buffer release], _buffer = [str->_buffer mutableCopy];
[_attributes release], _attributes = [str->_attributes mutableCopy];
}
- (void)setAttributes:(NSDictionary *)attributes range:(NSRange)range {
range = [self rangeOfAttributeRunsForRange:range];
for (ZAttributeRun *run in [_attributes subarrayWithRange:range]) {
[run.attributes setDictionary:attributes];
}
[self cleanupAttributesInRange:range];
}
#pragma mark -
// splits the existing runs to provide one or more new runs for the given range
- (NSRange)rangeOfAttributeRunsForRange:(NSRange)range {
NSParameterAssert(NSMaxRange(range) <= [_buffer length]);
// find (or create) the first run
NSUInteger first = 0;
ZAttributeRun *lastRun = nil;
for (;;first++) {
if (first >= [_attributes count]) {
// we didn't find a run
first = [_attributes count];
ZAttributeRun *newRun = [[ZAttributeRun alloc] initWithIndex:range.location attributes:lastRun.attributes];
[_attributes addObject:newRun];
[newRun release];
break;
}
ZAttributeRun *run = [_attributes objectAtIndex:first];
if (run.index == range.location) {
break;
} else if (run.index > range.location) {
ZAttributeRun *newRun = [[ZAttributeRun alloc] initWithIndex:range.location attributes:lastRun.attributes];
[_attributes insertObject:newRun atIndex:first];
[newRun release];
break;
}
lastRun = run;
}
if (((ZAttributeRun *)[_attributes lastObject]).index < NSMaxRange(range)) {
NSRange subrange = NSMakeRange(first, [_attributes count] - first);
if (NSMaxRange(range) < [_buffer length]) {
ZAttributeRun *newRun = [[ZAttributeRun alloc] initWithIndex:NSMaxRange(range)
attributes:(NSDictionary*)[(ZAttributeRun *)[_attributes lastObject] attributes]];
[_attributes addObject:newRun];
[newRun release];
}
return subrange;
} else {
// find the last run within and the first run after the range
NSUInteger lastIn = first, firstAfter = [_attributes count]-1;
while (firstAfter > lastIn + 1) {
NSUInteger idx = (firstAfter + lastIn) / 2;
ZAttributeRun *run = [_attributes objectAtIndex:idx];
if (run.index < range.location) {
lastIn = idx;
} else if (run.index > range.location) {
firstAfter = idx;
} else {
// this is definitively the first run after the range
firstAfter = idx;
break;
}
}
if ([[_attributes objectAtIndex:firstAfter] index] > NSMaxRange(range)) {
// the first after is too far after, insert another run!
ZAttributeRun *newRun = [[ZAttributeRun alloc] initWithIndex:NSMaxRange(range)
attributes:[(ZAttributeRun *)[_attributes objectAtIndex:firstAfter-1] attributes]];
[_attributes insertObject:newRun atIndex:firstAfter];
[newRun release];
}
return NSMakeRange(lastIn, firstAfter - lastIn);
}
}
- (void)cleanupAttributesInRange:(NSRange)range {
// expand the range to include one surrounding attribute on each side
if (range.location > 0) {
range.location -= 1;
range.length += 1;
}
if (NSMaxRange(range) < [_attributes count]) {
range.length += 1;
} else {
// make sure the range is capped to the attributes count
range.length = [_attributes count] - range.location;
}
if (range.length == 0) return;
ZAttributeRun *lastRun = [_attributes objectAtIndex:range.location];
for (NSUInteger i = range.location+1; i < NSMaxRange(range);) {
ZAttributeRun *run = [_attributes objectAtIndex:i];
if ([lastRun.attributes isEqualToDictionary:run.attributes]) {
[_attributes removeObjectAtIndex:i];
range.length -= 1;
} else {
lastRun = run;
i++;
}
}
}
- (void)offsetRunsInRange:(NSRange)range byOffset:(NSInteger)offset {
for (NSUInteger i = range.location; i < NSMaxRange(range); i++) {
ZAttributeRun *run = [_attributes objectAtIndex:i];
ZAttributeRun *newRun = [[ZAttributeRun alloc] initWithIndex:run.index + offset attributes:run.attributes];
[_attributes replaceObjectAtIndex:i withObject:newRun];
[newRun release];
}
}
@end
@implementation ZAttributeRun
@synthesize index = _index;
@synthesize attributes = _attributes;
+ (id)attributeRunWithIndex:(NSUInteger)idx attributes:(NSDictionary *)attrs {
return [[[self alloc] initWithIndex:idx attributes:attrs] autorelease];
}
- (id)initWithIndex:(NSUInteger)idx attributes:(NSDictionary *)attrs {
NSParameterAssert(idx >= 0);
if ((self = [super init])) {
_index = idx;
if (attrs == nil) {
_attributes = [[NSMutableDictionary alloc] init];
} else {
_attributes = [attrs mutableCopy];
}
}
return self;
}
- (id)initWithCoder:(NSCoder *)decoder {
if ((self = [super init])) {
_index = [[decoder decodeObjectForKey:@"index"] unsignedIntegerValue];
_attributes = [[decoder decodeObjectForKey:@"attributes"] mutableCopy];
}
return self;
}
- (id)init {
return [self initWithIndex:0 attributes:[NSDictionary dictionary]];
}
- (id)copyWithZone:(NSZone *)zone {
return [[ZAttributeRun allocWithZone:zone] initWithIndex:_index attributes:_attributes];
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:[NSNumber numberWithUnsignedInteger:_index] forKey:@"index"];
[aCoder encodeObject:_attributes forKey:@"attributes"];
}
- (NSString *)description {
NSMutableArray *components = [NSMutableArray arrayWithCapacity:[_attributes count]];
for (id key in _attributes) {
[components addObject:[NSString stringWithFormat:@"%@=%@", key, [_attributes objectForKey:key]]];
}
return [NSString stringWithFormat:@"<%@: %p index=%lu attributes={%@}>",
NSStringFromClass([self class]), self, (unsigned long)_index, [components componentsJoinedByString:@" "]];
}
- (BOOL)isEqual:(id)object {
if (![object isKindOfClass:[ZAttributeRun class]]) return NO;
ZAttributeRun *other = (ZAttributeRun *)object;
return _index == other->_index && [_attributes isEqualToDictionary:other->_attributes];
}
- (void)dealloc {
[_attributes release];
[super dealloc];
}
@end
NSString * const ZFontAttributeName = @"ZFontAttributeName";
NSString * const ZForegroundColorAttributeName = @"ZForegroundColorAttributeName";
NSString * const ZBackgroundColorAttributeName = @"ZBackgroundColorAttributeName";
NSString * const ZUnderlineStyleAttributeName = @"ZUnderlineStyleAttributeName";