/* |
File: CoreTextScrollView.m |
Abstract: Manages the display of pages for a given document to display. This is the view that you will use in Interface Builder or create directly to display and interact with an AttributedStringDoc in an application. |
It is also fair to say that this view acts as a controller for CoreTextViews. You can change the page to display, manage text selection, and change font parameters for the document. |
|
Version: 1.1 |
|
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple |
Inc. ("Apple") in consideration of your agreement to the following |
terms, and your use, installation, modification or redistribution of |
this Apple software constitutes acceptance of these terms. If you do |
not agree with these terms, please do not use, install, modify or |
redistribute this Apple software. |
|
In consideration of your agreement to abide by the following terms, and |
subject to these terms, Apple grants you a personal, non-exclusive |
license, under Apple's copyrights in this original Apple software (the |
"Apple Software"), to use, reproduce, modify and redistribute the Apple |
Software, with or without modifications, in source and/or binary forms; |
provided that if you redistribute the Apple Software in its entirety and |
without modifications, you must retain this notice and the following |
text and disclaimers in all such redistributions of the Apple Software. |
Neither the name, trademarks, service marks or logos of Apple Inc. may |
be used to endorse or promote products derived from the Apple Software |
without specific prior written permission from Apple. Except as |
expressly stated in this notice, no other rights or licenses, express or |
implied, are granted by Apple herein, including but not limited to any |
patent rights that may be infringed by your derivative works or by other |
works in which the Apple Software may be incorporated. |
|
The Apple Software is provided by Apple on an "AS IS" basis. APPLE |
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION |
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS |
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND |
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. |
|
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL |
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, |
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED |
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), |
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE |
POSSIBILITY OF SUCH DAMAGE. |
|
Copyright (C) 2014 Apple Inc. All Rights Reserved. |
|
*/ |
|
#import "CoreTextScrollView.h" |
#import "TextAccessibilityElement.h" |
|
// Various hard-coded constants for this sample |
|
#define COLUMN_COUNT_MIN 1 |
#define COLUMN_COUNT_MAX 3 |
|
#define IPAD_HORIZONTAL_MARGIN 40 |
#define IPAD_VERTICAL_MARGIN 30 |
|
#define IPHONE_HORIZONTAL_MARGIN 10 |
#define IPHONE_VERTICAL_MARGIN 10 |
|
#pragma mark - |
#pragma mark CoreTextViewFrameInfo definition and implementation |
|
// CoreTextViewFrameInfo caches a CTFrame and related info |
@interface CoreTextViewFrameInfo : NSObject { |
@private |
ASDFrameType frameType; |
CGPathRef path; |
// stringOffsetForSetter keeps track of the offset into the attributed string |
// where the framesetter got created. We need this offset as setters are likely |
// shared between frames |
NSUInteger stringOffsetForSetter; |
CTFramesetterRef setter; |
CTFrameRef frame; |
NSRange stringRange; |
id value; // holder for AttributedStringDoc ref |
} |
|
// NSObject:description for debugging |
- (NSString *)description; |
|
- (CoreTextViewFrameInfo*)initWithFrameType:(ASDFrameType)type path:(CGPathRef)path; |
|
// Cache string offset where framesetter was created |
- (NSUInteger)setFramesetterForStringOffset:(NSUInteger)stringOffset previousFreeFlowFrame:(CoreTextViewFrameInfo*)prevFreeFlowFrame; |
|
// Accessors |
- (CGPathRef)path; |
- (CTFramesetterRef)setter; |
- (CTFrameRef)frame; |
- (void)setPath:(CGPathRef)pathValue; |
- (void)setSetter:(CTFramesetterRef)setterValue; |
- (void)setFrame:(CTFrameRef)frameValue; |
|
// Refresh the cached CTFrame for current info (will re-layout) |
- (void)refreshTextFrame; |
|
@property (nonatomic) ASDFrameType frameType; |
@property (nonatomic) NSRange stringRange; |
@property (nonatomic, retain) id value; |
@property (nonatomic) NSUInteger stringOffsetForSetter; |
@end |
|
@implementation CoreTextViewFrameInfo |
|
@synthesize frameType; |
@synthesize value; |
@synthesize stringRange; |
@synthesize stringOffsetForSetter; |
|
- (CGPathRef)path { |
return path; |
} |
- (CTFramesetterRef)setter { |
return setter; |
} |
- (CTFrameRef)frame { |
return frame; |
} |
|
- (void)setPath:(CGPathRef)pathValue { |
if (pathValue != path) { |
if (path) CGPathRelease(path); |
path = pathValue; |
if (path) CGPathRetain(path); |
} |
} |
|
- (void)setSetter:(CTFramesetterRef)setterValue { |
if (setterValue != setter) { |
if (setter) CFRelease(setter); |
setter = setterValue; |
if (setter) CFRetain(setter); |
} |
} |
|
- (void)setFrame:(CTFrameRef)frameValue { |
if (frameValue != frame) { |
if (frame) CFRelease(frame); |
frame = frameValue; |
if (frame) CFRetain(frame); |
} |
} |
|
- (CoreTextViewFrameInfo*)initWithFrameType:(ASDFrameType)type path:(CGPathRef)thePath { |
if (self = [super init]) { |
frameType = type; |
path = CGPathRetain(thePath); |
setter = NULL; |
frame = NULL; |
value = nil; |
stringRange.location = 0; |
stringRange.length = 0; |
stringOffsetForSetter = 0; |
} |
return self; |
} |
|
- (NSString*)description { |
return [NSString stringWithFormat:@"<CoreTextViewFrameInfo %p> type: %u CGPath: %p CTFramesetter %p: value: %@", |
self, frameType, path, setter, [value description]]; |
} |
|
- (NSUInteger)setFramesetterForStringOffset:(NSUInteger)stringOffset previousFreeFlowFrame:(CoreTextViewFrameInfo*)prevFreeFlowFrame { |
static CGFloat iWidth = 0.; |
static CGFloat helveticaLineHeight = 0.; |
AttributedStringDoc* document = value; |
|
// Estimate how much text we can fit into the frame. We do so by getting the glyph metrics for the letter 'x' in Helvetica |
// and see how many of these glyphs we can fit into the frame. Obviously this is not exact (and a bit more precise would be |
// to use the font being used in the attributed string), but this is a conservative first approximation |
CGRect frameRect = CGPathGetBoundingBox(path); |
if (iWidth == 0) { |
// First time here, so use Helvetica 'x' to approximate as described above |
const UniChar iChar = 'x'; |
CGGlyph iGlyph; |
CTFontRef font = CTFontCreateWithName(CFSTR("Helvetica"), 12.0, NULL); |
if( CTFontGetGlyphsForCharacters(font, &iChar, &iGlyph, 1) ) { |
CGRect iBoundRect; |
CTFontGetBoundingRectsForGlyphs(font, kCTFontHorizontalOrientation, &iGlyph, &iBoundRect, 1); |
iWidth = iBoundRect.size.width; |
} |
else |
iWidth = 3.0; // should have found the glyph width - be conservative and assume something small |
|
helveticaLineHeight = CTFontGetAscent(font) + CTFontGetDescent(font) + CTFontGetLeading(font); |
|
CFRelease(font); |
} |
|
NSUInteger maxLength = [document.attributedString length] - stringOffset; |
NSUInteger len = (frameRect.size.width / iWidth) * (frameRect.size.height / helveticaLineHeight); |
if (len > maxLength) { |
len = maxLength; |
} |
NSRange range = NSMakeRange(stringOffset, len); |
CTFramesetterRef framesetter = nil; |
CTFrameRef workFrame = nil; |
CFRange visibleRange = {0, 0}; |
|
NSDictionary* frameAttributes = nil; |
if (document.verticalOrientation) { |
frameAttributes = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCTFrameProgressionRightToLeft] forKey:(id)kCTFrameProgressionAttributeName]; |
} |
|
// Figure out if the previous setter can be used to render the current frame |
if (prevFreeFlowFrame) { |
CTFrameRef prevFrame = [prevFreeFlowFrame frame]; |
CFRange prevVisRange = CTFrameGetVisibleStringRange(prevFrame); |
CFRange prevStringRange = CTFrameGetStringRange(prevFrame); |
CFRange newFrameRange = { prevVisRange.location + prevVisRange.length, 0 }; |
newFrameRange.length = (prevStringRange.location + prevStringRange.length) - newFrameRange.location; |
if (newFrameRange.length > 0) { |
// Generate CTFrame using previous setter to get visible range |
framesetter = [prevFreeFlowFrame setter]; |
workFrame = AUTO_RELEASED_CTREF(CTFramesetterCreateFrame(framesetter, newFrameRange, path, (CFDictionaryRef)frameAttributes)); |
visibleRange = CTFrameGetVisibleStringRange(workFrame); |
if (visibleRange.length < newFrameRange.length || newFrameRange.length >= maxLength) { |
stringOffsetForSetter = prevFreeFlowFrame.stringOffsetForSetter; |
} |
else { |
// Will need to use new framesetter below |
framesetter = nil; |
workFrame = nil; |
} |
|
} |
} |
|
if (framesetter == nil) { |
do { |
// Create new setter and frame to get visible range |
framesetter = AUTO_RELEASED_CTREF(CTFramesetterCreateWithAttributedString((CFAttributedStringRef) [document.attributedString attributedSubstringFromRange:range])); |
workFrame = AUTO_RELEASED_CTREF(CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, (CFDictionaryRef)frameAttributes)); |
visibleRange = CTFrameGetVisibleStringRange(workFrame); |
|
if (visibleRange.length < range.length || range.length >= maxLength) { |
stringOffsetForSetter = range.location; |
break; |
} |
|
range.length *= 2; // pad |
if (range.length > maxLength) { |
range.length = maxLength; |
} |
|
} while (TRUE); |
} |
|
[self setSetter:framesetter]; |
[self setFrame:workFrame]; |
|
stringRange.location = stringOffset; |
stringRange.length = visibleRange.length; |
|
return (visibleRange.length + stringOffset); |
} |
|
|
- (void)refreshTextFrame { |
if (!(frameType == ASDFrameTypeTextFlow || frameType == ASDFrameTypeText)) |
return; |
|
NSAttributedString* attrString = value; |
BOOL isVertical = NO; |
|
if (frameType == ASDFrameTypeTextFlow) { |
AttributedStringDoc* document = value; |
attrString = [[(AttributedStringDoc*)value attributedString] attributedSubstringFromRange:stringRange]; |
isVertical = document.verticalOrientation; |
} |
// Create setter with current attributed string |
[self setSetter:AUTO_RELEASED_CTREF(CTFramesetterCreateWithAttributedString((CFAttributedStringRef) attrString))]; |
|
NSDictionary* frameAttributes = nil; |
if (isVertical) { |
frameAttributes = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCTFrameProgressionRightToLeft] forKey:(id)kCTFrameProgressionAttributeName]; |
} |
|
// Create and cache CTFrame for current path |
[self setFrame:AUTO_RELEASED_CTREF(CTFramesetterCreateFrame([self setter], CFRangeMake(0, 0), path, (CFDictionaryRef)frameAttributes))]; |
} |
|
- (void)dealloc { |
if (path) { |
CGPathRelease(path); |
} |
if (setter) { |
CFRelease(setter); |
} |
if (frame) { |
CFRelease(frame); |
} |
[value release]; |
|
[super dealloc]; |
} |
|
|
@end |
|
#pragma mark - |
#pragma mark CoreTextViewPageInfo definition and implementation |
|
// CoreTextViewPageInfo caches info about a document page |
@interface CoreTextViewPageInfo : NSObject { |
@private |
NSInteger pageNumber; |
NSArray* framesToDraw; |
NSUInteger rangeStart; |
NSUInteger rangeEnd; |
CoreTextViewFrameInfo* lastFreeFlowFrame; |
CALayer* pageLayer; |
BOOL needsRedrawOnLoad; |
BOOL needsReLayout; |
} |
|
// NSObject:description for debugging |
- (NSString *)description; |
|
- (CoreTextViewPageInfo*)initWithFramesToDraw:(NSArray*)frames pageNumber:(NSInteger)page rangeStart:(NSUInteger)rangeStart rangeEnd:(NSUInteger)rangeEnd layer:(CALayer*)layer; |
- (CoreTextViewPageInfo*)initWithLayer:(CALayer*)layer pageNumber:(NSInteger)page; |
|
@property (nonatomic) NSInteger pageNumber; |
@property (nonatomic, retain) NSArray* framesToDraw; |
@property (nonatomic, retain) CALayer* pageLayer; |
@property (nonatomic) NSUInteger rangeStart; |
@property (nonatomic) NSUInteger rangeEnd; |
@property (nonatomic) BOOL needsRedrawOnLoad; |
@property (nonatomic) BOOL needsReLayout; |
@property (nonatomic, retain) CoreTextViewFrameInfo* lastFreeFlowFrame; |
|
@end |
|
@implementation CoreTextViewPageInfo |
|
@synthesize framesToDraw; |
@synthesize rangeStart; |
@synthesize rangeEnd; |
@synthesize pageLayer; |
@synthesize needsRedrawOnLoad; |
@synthesize needsReLayout; |
@synthesize pageNumber; |
@synthesize lastFreeFlowFrame; |
|
- (CoreTextViewPageInfo*)initWithFramesToDraw:(NSArray*)frames pageNumber:(NSInteger)page rangeStart:(NSUInteger)start rangeEnd:(NSUInteger)end layer:(CALayer*)layer { |
if (self = [super init]) { |
pageNumber = page; |
framesToDraw = [frames retain]; |
rangeStart = start; |
rangeEnd = end; |
pageLayer = [layer retain]; |
needsRedrawOnLoad = NO; |
needsReLayout = NO; |
lastFreeFlowFrame = nil; |
} |
return self; |
} |
|
- (CoreTextViewPageInfo*)initWithLayer:(CALayer*)layer pageNumber:(NSInteger)page { |
if (self = [super init]) { |
pageNumber = page; |
framesToDraw = nil; |
rangeStart = 0; |
rangeEnd = 0; |
pageLayer = [layer retain]; |
needsRedrawOnLoad = NO; |
needsReLayout = NO; |
lastFreeFlowFrame = nil; |
} |
return self; |
} |
|
- (NSString*)description { |
return [NSString stringWithFormat:@"<CoreTextViewPageInfo %p> start: %u end %u layer: %p frames: %@", |
self, rangeStart, rangeEnd, pageLayer, [framesToDraw description]]; |
} |
|
- (void)dealloc { |
[lastFreeFlowFrame release]; |
[framesToDraw release]; |
|
[pageLayer release]; |
|
[super dealloc]; |
} |
@end |
|
#pragma mark - |
#pragma mark Local enum to index cached CoreTextViews |
|
enum { |
prevCoreTextView = 0, |
currCoreTextView = 1, |
nextCoreTextView = 2, |
|
coreTextViewCount = 3 |
}; |
|
#pragma mark - |
#pragma mark CoreTextView definition |
|
// CoreTextView is our UIView subclass that handles content drawing |
// Note that implementation continues below CoreTextScrollView as the |
// classes refer to each other. |
@interface CoreTextView : UIView { |
CoreTextScrollView* scrollView; |
CoreTextViewPageInfo* pageInfo; |
NSUInteger selectedFrame; |
CoreTextViewDraw* caLayerDrawDelegate; |
BOOL layoutOnlyOnDraw; |
@private |
NSMutableArray *_accessibleElements; |
} |
|
- (id)initWithScrollView:(CoreTextScrollView*)theScrollView; |
|
// Drawing methods |
- (NSArray*)framesToDrawForPage; |
- (void)drawIntoLayer:(CALayer *)theLayer inContext:(CGContextRef)context; |
|
// Marks frame closest to given position as selected |
- (void)selectFrameAtPosition:(CGPoint)position; |
|
// Accessibility-related methods |
- (CGRect)accessibilityBoundingBoxForRange:(NSRange)range forFrame:(CoreTextViewFrameInfo *)frameInfo withContext:(CGContextRef)context; |
- (NSMutableArray *)accessibleElements; |
- (void)accessibilityUpdateElements; |
|
// NSObject:description for logging |
- (NSString*)description; |
//- (void)dealloc; |
|
@property (nonatomic) NSUInteger selectedFrame; |
@property (nonatomic, retain) CoreTextScrollView* scrollView; |
@property (nonatomic, retain) CoreTextViewPageInfo* pageInfo; |
@property (nonatomic, readonly) CoreTextViewDraw* caLayerDrawDelegate; |
@property (nonatomic) BOOL layoutOnlyOnDraw; |
@end |
|
#pragma mark - |
#pragma mark AsyncLayerOperation definition |
|
// AsyncLayerOperation is our NSOperation subclass for rendering |
// CoreTextViews on a secondary thread. |
// Note that implementation continues below CoreTextScrollView as the |
// classes refer to each other. |
@interface AsyncLayerOperation : NSOperation |
{ |
@private |
CoreTextScrollView* _scrollView; |
NSInteger _nextPageToLoad; |
} |
|
-(id)initWithCoreTextScrollView:(CoreTextScrollView*)scrollView forNextPageToLoad:(NSInteger)nextPageToLoad; |
+(id)operationWithCoreTextScrollView:(CoreTextScrollView*)scrollView forNextPageToLoad:(NSInteger)nextPageToLoad; |
@end |
|
#pragma mark - |
#pragma mark CoreTextScrollView implementation |
|
// Private class extension interface |
@interface CoreTextScrollView () |
- (CoreTextView*)loadPage:(NSInteger)pageToLoad; |
- (void)loadAsyncPageAfter:(NSInteger)currentPage; |
- (void)addPageToScrollView; |
- (void)switchPageMovingUp:(BOOL)up; |
- (void)relayoutDocFromPage:(NSInteger)pageStart; |
@end |
|
@implementation CoreTextScrollView |
|
#pragma mark Initialization and member assignments |
|
@synthesize document; |
@synthesize pagesInfo; |
@synthesize selectionRanges; |
@synthesize viewOptions; |
@synthesize pageCount; |
@synthesize pageDisplayed; |
|
- (void)setDocument:(id)newDocument { |
if (document != newDocument) { |
[document release]; |
document = [newDocument retain]; |
} |
} |
|
- (NSString *)description |
{ |
return [document fileName]; |
} |
|
|
- (void)reset:(AttributedStringDoc*)theDoc withDelegate:(id)scrollViewDelegate { |
// Don't reset until any pending operations on previous doc are done |
[operationQueue waitUntilAllOperationsAreFinished]; |
|
self.delegate = scrollViewDelegate; |
|
if (pagesInfo) { |
[pagesInfo removeAllObjects]; |
} else { |
pagesInfo = [[NSMutableDictionary alloc] init]; |
} |
|
// Cache the new AttributedStringDoc |
[self setDocument:theDoc]; |
|
pageDisplayed = 0; |
pageCount = 0; |
[selectionRanges release]; |
selectionRanges = nil; |
|
// Remove any existing cached CoreTextViews |
for (int idx=0; idx<coreTextViewCount; idx++) { |
if (pageViews[idx]) { |
[pageViews[idx] removeFromSuperview]; |
[pageViews[idx] release]; |
pageViews[idx] = [[CoreTextView alloc] initWithScrollView:self]; |
} |
} |
|
// Load first page of doc and asynch load next page (if any) |
if (theDoc != nil) { |
[self loadPage:0]; |
[self loadPage:1]; |
[self loadAsyncPageAfter:1]; |
} |
|
// Flag for redisplay |
[self setNeedsDisplay]; |
} |
|
|
- (id)initWithFrame:(CGRect)frame { |
if ((self = [super initWithFrame:frame])) { |
// Initialization code |
[self reset:nil withDelegate:nil]; |
} |
return self; |
} |
|
|
-(id)initWithCoder:(NSCoder *)aDecoder |
{ |
self = [super initWithCoder:aDecoder]; |
if(self != nil) |
{ |
document = nil; |
|
pageDisplayed = 0; |
pageCount = 0; |
|
selectionRanges = nil; |
viewOptions = 0; |
|
pagesInfo = [[NSMutableDictionary alloc] init]; |
|
// Alloc cached current and prev/next CoreTextViews |
pageViews[prevCoreTextView] = [[CoreTextView alloc] initWithScrollView:self]; |
pageViews[currCoreTextView] = [[CoreTextView alloc] initWithScrollView:self]; |
pageViews[nextCoreTextView] = [[CoreTextView alloc] initWithScrollView:self]; |
|
self.pagingEnabled = YES; |
self.showsHorizontalScrollIndicator = NO; |
self.showsVerticalScrollIndicator = NO; |
self.scrollsToTop = NO; |
|
// init our asynch operation queue for secondary thread rendering |
operationQueue = [[NSOperationQueue alloc] init]; |
} |
return self; |
} |
|
- (void)awakeFromNib |
{ |
CALayer *layer = [self layer]; |
|
[self reset:nil withDelegate:nil]; |
|
// clear the view's background color so that our background |
// fits within the rounded border |
CGColorRef backgroundColor = [self.backgroundColor CGColor]; |
self.backgroundColor = [UIColor clearColor]; |
layer.backgroundColor = backgroundColor; |
|
[self setNeedsDisplay]; |
} |
|
|
- (void)dealloc { |
|
[pageViews[prevCoreTextView] release]; |
[pageViews[currCoreTextView] release]; |
[pageViews[nextCoreTextView] release]; |
|
[pagesInfo release]; // no need to release the sublayers as they will be released with the main layer |
[document release]; |
[operationQueue release]; |
|
[super dealloc]; |
} |
|
#pragma mark - |
#pragma mark Selection Ranges |
|
- (void)addSelectionRange:(NSRange)range |
{ |
NSArray* rangeArray = [NSArray arrayWithObjects:[NSNumber numberWithUnsignedInt:range.location], [NSNumber numberWithUnsignedInt:range.length], nil]; |
if (selectionRanges == nil) { |
selectionRanges = [[NSMutableArray alloc] init]; |
} |
|
[selectionRanges addObject:rangeArray]; |
|
// Page needs to update display to show selections, if any |
[self pageNeedsDisplay:[self pageForStringOffset:range.location]]; |
} |
|
- (void)clearSelectionRanges |
{ |
if (selectionRanges) { |
for (NSArray* rangeArr in selectionRanges) { |
// Pages with selection info will need redisplay to remove selection rects |
[self pageNeedsDisplay:[self pageForStringOffset:[[rangeArr objectAtIndex:0] unsignedIntValue]]]; |
} |
|
[selectionRanges release]; |
selectionRanges = nil; |
} |
} |
|
|
#pragma mark - |
#pragma mark Layer/Page management |
|
|
- (NSRange)stringRangeForCurrentPage { |
// Get the text range from the appropriate CoreTextViewPageInfo |
CoreTextViewPageInfo* info = [pagesInfo objectForKey:[NSNumber numberWithInt:pageDisplayed]]; |
NSRange result = { info.rangeStart, info.rangeEnd - info.rangeStart}; |
return result; |
} |
|
|
- (NSInteger)pageForStringOffset:(NSUInteger)offset { |
// Find the appropriate CoreTextViewPageInfo for the text offset |
for (NSNumber* pageNumber in pagesInfo) { |
CoreTextViewPageInfo* page = [pagesInfo objectForKey:pageNumber]; |
if (offset >= page.rangeStart && offset < page.rangeEnd) { |
return [pageNumber unsignedIntValue]; |
} |
} |
|
// Page for offset not found, return something indicating error condition |
return NSUIntegerMax; |
} |
|
|
- (void)pageNeedsDisplay:(NSInteger)pageNumber { |
// Flag that page needs redisplay via its CoreTextViewPageInfo |
if ([pagesInfo count] > pageNumber) { |
((CoreTextViewPageInfo*)[pagesInfo objectForKey:[NSNumber numberWithInt:pageNumber]]).needsRedrawOnLoad = YES; |
} |
} |
|
|
- (void)addPageToScrollView { |
pageCount += 1; |
|
// Note that self.contentSize likely triggers a call into the delegate method scrollViewDidScroll |
self.contentSize = CGSizeMake(self.frame.size.width * pageCount, self.frame.size.height); |
} |
|
|
- (void)loadAsyncPageAfter:(NSInteger)currentPage { |
// Add page to be loaded to our operationQueue, which will process it on the queue thread |
[operationQueue addOperation:[AsyncLayerOperation operationWithCoreTextScrollView:self forNextPageToLoad:currentPage]]; |
} |
|
|
- (CoreTextView*)loadPage:(NSInteger)pageToLoad { |
|
// Sanity check page index |
|
if (pageToLoad<0) { |
return NULL; |
} |
|
if (pageToLoad > 0) { |
if (pageToLoad > pageCount) { |
return NULL; |
} |
|
NSUInteger rangeMax = ((CoreTextViewPageInfo*)[pagesInfo objectForKey:[NSNumber numberWithInt:pageToLoad-1]]).rangeEnd; |
if (rangeMax) { |
if ( rangeMax >= (document.attributedString).length && ([document framesForPage:pageToLoad] == nil)) |
return NULL; |
} |
else { |
if ([document framesForPage:pageToLoad] == nil) { |
return NULL; |
} |
} |
|
} |
|
BOOL isNotScratch = YES; |
CoreTextView* ctView = nil; |
// Find cached CoreTextView for page index, if applicable |
if (pageToLoad == pageDisplayed) { |
ctView = pageViews[currCoreTextView]; |
} |
else if (pageToLoad < pageDisplayed && (pageToLoad+1) == pageDisplayed) { |
ctView = pageViews[prevCoreTextView]; |
} |
else if (pageToLoad > pageDisplayed && (pageToLoad-1) == pageDisplayed) { |
ctView = pageViews[nextCoreTextView]; |
} |
else { |
// Need to create new CoreTextView |
ctView = [[[CoreTextView alloc] initWithScrollView:self] autorelease]; |
isNotScratch = NO; |
} |
|
if (ctView.pageInfo.needsReLayout) { |
ctView.pageInfo.needsRedrawOnLoad = YES; |
} |
|
if (ctView.pageInfo && ctView.pageInfo.needsRedrawOnLoad == NO) { |
return ctView; |
} |
|
// If we've already loaded & drawn the page, return the CoreTextView |
NSNumber* pageToLoadNum = [NSNumber numberWithInt:pageToLoad]; |
CoreTextViewPageInfo* pageInfoEntry = (CoreTextViewPageInfo*)[pagesInfo objectForKey:pageToLoadNum]; |
if (pageInfoEntry) { |
ctView.pageInfo = pageInfoEntry; |
if (pageInfoEntry.needsRedrawOnLoad == NO && pageInfoEntry.needsReLayout == NO) { |
return ctView; |
} |
} |
|
// Create and setup CALayer for our CoreTextView |
|
CALayer* theLayer = NULL; |
theLayer = [CALayer layer]; |
#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 40000 |
theLayer.contentsScale = [[UIScreen mainScreen] scale]; |
#endif |
|
CGRect bounds = [ctView bounds]; |
theLayer.position = CGPointMake(bounds.size.width/2, bounds.size.height/2); |
|
theLayer.bounds = bounds; |
theLayer.backgroundColor = [[UIColor clearColor] CGColor]; |
|
[theLayer setDelegate: ctView.caLayerDrawDelegate]; |
|
CALayer* viewLayer = [ctView layer]; |
NSArray* subLayers = [viewLayer sublayers]; |
// Remove any previous CALayer |
for (CALayer* aSubLayer in subLayers) { |
[aSubLayer removeFromSuperlayer]; |
} |
[[ctView layer] addSublayer:theLayer]; |
|
theLayer.name = [NSString stringWithFormat:@"%d", pageToLoad, nil]; |
|
if (ctView.pageInfo.needsReLayout || pageInfoEntry == nil) { |
// Need to re-layout text for new CoreTextViewPageInfo |
BOOL newEntry = pageInfoEntry == nil; |
pageInfoEntry = [[[CoreTextViewPageInfo alloc] initWithLayer:theLayer pageNumber:pageToLoad] autorelease]; |
[pagesInfo setObject:pageInfoEntry forKey:pageToLoadNum]; |
if (newEntry) { |
pageInfoEntry.needsReLayout = YES; |
} |
} |
|
ctView.pageInfo = pageInfoEntry; |
|
if (isNotScratch) { |
// Re-using a CoreTextView, so refresh |
[ctView setNeedsDisplay]; |
[theLayer setNeedsDisplay]; |
[theLayer display]; // displays the layer and fills out the rest of the info for the pageInfoEntry |
|
[ctView removeFromSuperview]; |
|
CGRect frame = [self frame]; |
frame.origin.x = frame.size.width * pageToLoad; |
frame.origin.y = 0; |
ctView.frame = frame; |
[self addSubview:ctView]; |
|
if (pageToLoad == pageCount) { |
[self addPageToScrollView]; // only add a page to the scrollView when it is a brand new page and it is fully drawn |
} |
} |
|
return ctView; |
} |
|
- (void)setPage:(NSInteger)pageToDisplay { |
if (pageToDisplay == pageDisplayed) { |
return; |
} |
else if (pageToDisplay == pageDisplayed+1) { |
// Display next page view, which we should already have cached |
[pageViews[prevCoreTextView] removeFromSuperview]; |
pageViews[prevCoreTextView].pageInfo.pageLayer = nil; |
[pageViews[prevCoreTextView] release]; |
pageViews[prevCoreTextView] = nil; |
|
// Next page view is now current, previous current is now previous |
pageViews[prevCoreTextView] = pageViews[currCoreTextView]; |
pageViews[currCoreTextView] = pageViews[nextCoreTextView]; |
pageViews[nextCoreTextView] = [[CoreTextView alloc] initWithScrollView:self]; |
|
pageDisplayed = pageToDisplay; |
if ([self loadPage:pageToDisplay+1] == NULL) { |
// Could not load next page (possibly not present) |
[pageViews[nextCoreTextView] removeFromSuperview]; |
pageViews[nextCoreTextView].pageInfo.pageLayer = nil; |
[pageViews[nextCoreTextView] release]; |
pageViews[nextCoreTextView] = [[CoreTextView alloc] initWithScrollView:self]; |
} |
} |
else if (pageToDisplay == pageDisplayed-1) { |
// Display previous page view, which we should already have cached |
[pageViews[nextCoreTextView] removeFromSuperview]; |
pageViews[nextCoreTextView].pageInfo.pageLayer = nil; |
[pageViews[nextCoreTextView] release]; |
pageViews[nextCoreTextView] = nil; |
|
// Previous page view is now current, previous current is now next |
pageViews[nextCoreTextView] = pageViews[currCoreTextView]; |
pageViews[currCoreTextView] = pageViews[prevCoreTextView]; |
pageViews[prevCoreTextView] = [[CoreTextView alloc] initWithScrollView:self]; |
|
pageDisplayed = pageToDisplay; |
if (pageToDisplay>0) { |
[self loadPage:pageToDisplay-1]; |
} |
else { |
// No previous page because we are at the start of the document |
[pageViews[prevCoreTextView] removeFromSuperview]; |
pageViews[prevCoreTextView].pageInfo.pageLayer = nil; |
[pageViews[prevCoreTextView] release]; |
pageViews[prevCoreTextView] = [[CoreTextView alloc] initWithScrollView:self]; |
} |
} |
else { |
// Loading general page that we don't have cached |
for (NSUInteger idx=0; idx<coreTextViewCount; idx++) { |
[pageViews[idx] removeFromSuperview]; |
pageViews[idx].pageInfo.pageLayer = nil; |
[pageViews[idx] release]; |
pageViews[idx] = [[CoreTextView alloc] initWithScrollView:self]; |
} |
|
pageDisplayed = pageToDisplay; |
if (pageToDisplay>0) { |
[self loadPage:pageToDisplay-1]; |
} |
[self loadPage:pageToDisplay]; |
[self loadPage:pageToDisplay+1]; |
} |
|
for (NSUInteger idx=0; idx<coreTextViewCount; idx++) { |
CoreTextView* ctView = pageViews[idx]; |
if(!ctView || ctView.pageInfo == nil) |
continue; |
|
NSInteger pageToLoad = ctView.pageInfo.pageNumber; |
|
// If there is a selected frame in a page that is not being displayed, unselect it and force a redraw |
if (pageDisplayed != pageToLoad && ctView.selectedFrame != NSUIntegerMax) { |
ctView.selectedFrame = NSUIntegerMax; |
if (ctView.pageInfo.pageLayer != nil) { |
ctView.pageInfo.pageLayer = nil; |
} |
} |
|
if (ctView.pageInfo.pageLayer == nil) { |
NSInteger pageToLoad = ctView.pageInfo.pageNumber; |
|
// Create the CALayer for the CoreTextView |
|
CALayer* theLayer = NULL; |
theLayer = [CALayer layer]; |
#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 40000 |
theLayer.contentsScale = [[UIScreen mainScreen] scale]; |
#endif |
|
CGRect bounds = [ctView bounds]; |
theLayer.position = CGPointMake(bounds.size.width/2, bounds.size.height/2); |
|
theLayer.bounds = bounds; |
theLayer.backgroundColor = [[UIColor clearColor] CGColor]; |
|
[theLayer setDelegate: ctView.caLayerDrawDelegate]; |
|
CALayer* viewLayer = [ctView layer]; |
NSArray* subLayers = [viewLayer sublayers]; |
// Remove any previous layer |
for (CALayer* aSubLayer in subLayers) { |
[aSubLayer removeFromSuperlayer]; |
} |
[viewLayer addSublayer:theLayer]; |
|
theLayer.name = [NSString stringWithFormat:@"%d", pageToLoad, nil]; |
|
NSNumber* pageToLoadNum = [NSNumber numberWithInt:pageToLoad]; |
CoreTextViewPageInfo* pageInfoEntry = (CoreTextViewPageInfo*)[pagesInfo objectForKey:pageToLoadNum]; |
if (pageInfoEntry == NULL) { |
// Need new CoreTextViewPageInfo |
pageInfoEntry = [[[CoreTextViewPageInfo alloc] initWithLayer:theLayer pageNumber:pageToLoad] autorelease]; |
[pagesInfo setObject:pageInfoEntry forKey:pageToLoadNum]; |
} |
else { |
pageInfoEntry.pageLayer = theLayer; |
} |
pageInfoEntry.needsRedrawOnLoad = YES; |
|
ctView.pageInfo = pageInfoEntry; |
|
[theLayer setNeedsDisplay]; |
[theLayer display]; // Displays the layer |
|
CGRect frame = [self frame]; |
frame.origin.x = frame.size.width * pageToLoad; |
frame.origin.y = 0; |
ctView.frame = frame; |
[self addSubview:ctView]; |
} |
} |
|
// Post accessibility notification letting accessibility know that major screen change has occured |
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, NULL); |
} |
|
|
- (void)refreshPage { |
// Mark current page as needing redisplay |
CoreTextViewPageInfo* pageInfo = [pagesInfo objectForKey:[NSNumber numberWithInt:pageDisplayed]]; |
pageInfo.needsRedrawOnLoad = YES; |
[self loadPage:pageDisplayed]; |
[self setNeedsDisplay]; |
} |
|
|
- (void)refreshDoc { |
// Mark all pages in doc as needing redisplay |
for (NSNumber* pageNumber in pagesInfo) { |
CoreTextViewPageInfo* pageInfo = [pagesInfo objectForKey:pageNumber]; |
pageInfo.needsRedrawOnLoad = YES; |
} |
|
// Re-load current and adjacent cached pages |
[self loadPage:pageDisplayed-1]; |
[self loadPage:pageDisplayed]; |
[self loadPage:pageDisplayed+1]; |
|
[self setNeedsDisplay]; |
} |
|
-(void)switchPageMovingUp:(BOOL)up { |
// Change to prev/next page |
NSInteger newPage = pageDisplayed + (up ? 1 : -1); |
NSInteger curPageDisplayed = pageDisplayed; |
|
if (newPage >= 0 && newPage < pageCount) { |
[self setPage:newPage]; |
} |
|
if (curPageDisplayed != pageDisplayed) { |
// update the scroll view to the appropriate page |
CGRect frame = self.frame; |
frame.origin.x = frame.size.width * pageDisplayed; |
frame.origin.y = 0; |
[self scrollRectToVisible:frame animated:YES]; |
} |
} |
|
- (void)pageUp { |
[self switchPageMovingUp:YES]; |
} |
|
- (void)pageDown { |
[self switchPageMovingUp:NO]; |
} |
|
- (void)relayoutDoc { |
// Do not do relayout untill all secondary thread operations are finished |
[operationQueue waitUntilAllOperationsAreFinished]; |
|
// Re-layout of cached doc text requires recreating all cached CoreTextViews |
for (NSUInteger idx=0; idx<coreTextViewCount; idx++) { |
CoreTextView* ctView = pageViews[idx]; |
if(ctView) { |
if (ctView.pageInfo != nil) { |
ctView.pageInfo.pageLayer = nil; |
} |
[ctView removeFromSuperview]; |
[ctView release]; |
} |
pageViews[idx] = [[CoreTextView alloc] initWithScrollView:self]; |
} |
|
// Flag for relayout of all pages |
[self relayoutDocFromPage:0]; |
} |
|
- (BOOL)pageSharesAPreviousPageFrameSetter:(NSInteger)pageNumber { |
|
// Determine if page shared a framesetter from a previous page |
|
if (pageNumber < 1) |
return NO; |
|
CoreTextViewPageInfo* page =[pagesInfo objectForKey:[NSNumber numberWithInt:pageNumber]]; |
|
NSArray* framesToDraw = page.framesToDraw; |
// If page has no frames to draw, it will not be using a framesetter at all |
if (framesToDraw == nil) |
return NO; |
|
// see if the setter for first free flow text frame of this page matches the setter from a previous page last free flow frame |
for (NSUInteger idx=0; idx<[framesToDraw count]; idx++) { |
CoreTextViewFrameInfo* frameInfo = [framesToDraw objectAtIndex:idx]; |
if (frameInfo.frameType == ASDFrameTypeTextFlow) { |
NSInteger curPrevPage = pageNumber - 1; |
do { |
CoreTextViewPageInfo* prevPage =[pagesInfo objectForKey:[NSNumber numberWithInt:curPrevPage]]; |
if (prevPage == nil) |
return NO; |
|
if (prevPage.lastFreeFlowFrame) |
return ([prevPage.lastFreeFlowFrame setter] == [frameInfo setter]); |
} while (--curPrevPage > 0); |
|
break; // we've already examined the first free flow text frame for this page - no point in going on |
} |
} |
|
return NO; |
} |
|
- (void)relayoutDocFromPage:(NSInteger)pageStart { |
|
// It may be the case that the page that we wish to start laying out from shares the framesetter |
// with a previous page. If that is the case, then it is best to start laying out from the first |
// page that contains that framesetter. For example, if we change the font in page 9 and it shares |
// the framesetter with page 8, if we don't start laying out from page 8, page 9 will not get the |
// new font as it is using the cached framesetter from page 8 to draw |
while ([self pageSharesAPreviousPageFrameSetter:pageStart]) |
pageStart -= 1; |
|
// If the current page displayed exceeds pageStart, then we need to reload all those pages first |
// synchrounously. Any remainging pages can be loaded asyncronously |
CoreTextViewPageInfo* page =[pagesInfo objectForKey:[NSNumber numberWithInt:pageDisplayed]]; |
NSUInteger offsetToStopAt = page.rangeStart; |
NSInteger pageToStopAt = NSIntegerMax; |
if (offsetToStopAt == 0 && ([document framesForPage:pageDisplayed] != nil)) |
pageToStopAt = pageDisplayed; |
|
// Remove any pages we need to relayout from memory |
NSMutableArray* pagesToRemove = [[[NSMutableArray alloc] init] autorelease]; |
for (NSNumber* pageNumObj in pagesInfo) { |
page =[pagesInfo objectForKey:pageNumObj]; |
if (page.pageNumber >= pageStart) { |
CALayer *aLayer = page.pageLayer; |
if (aLayer) { |
page.pageLayer = nil; |
} |
page.needsReLayout = YES; |
[pagesToRemove addObject:pageNumObj]; |
} |
} |
for (NSNumber* pageNumObj in pagesToRemove) { |
[pagesInfo removeObjectForKey:pageNumObj]; |
pageCount -= 1; |
} |
|
if (pageStart < pageDisplayed) { |
// process any pages we need to load synchronously |
NSInteger curPage = pageStart; |
|
while (true) { |
pageDisplayed = curPage+1; // forces loadPage to draw the page into prevCoreTextView |
[self loadPage:curPage]; |
page =[pagesInfo objectForKey:[NSNumber numberWithInt:curPage]]; |
if (curPage++ == pageToStopAt || page.rangeEnd > offsetToStopAt) { |
break; |
} |
} |
|
pageDisplayed = curPage+1; // forces loadPage to draw the page into prevCoreTextView |
[self loadPage:curPage]; |
|
pageDisplayed = NSIntegerMax; // force reload of surrounding pages around curPage into appropriate views |
[self setPage:curPage-1]; |
|
} |
else { |
// easy case - we are just relaying out from the current page displayed |
[self loadPage:pageDisplayed-1]; |
[self loadPage:pageDisplayed]; |
[self loadPage:pageDisplayed+1]; |
} |
|
[self loadAsyncPageAfter:pageCount - 1]; |
|
// update the scroll view to the appropriate page |
CGRect frame = self.frame; |
frame.origin.x = frame.size.width * pageDisplayed; |
frame.origin.y = 0; |
[self scrollRectToVisible:frame animated:NO]; |
|
[self setNeedsDisplay]; |
} |
|
|
|
#pragma mark - |
#pragma mark Font Family & Font Feature/Option changes |
|
|
|
- (void)fontFamilyChange:(NSString*)fontFamilyName { |
// Don't change font settings until all secondary thread operations are done |
[operationQueue waitUntilAllOperationsAreFinished]; |
|
UIFont* fontSelected = [UIFont fontWithName:fontFamilyName size:12.0]; |
if ([fontSelected.familyName isEqual:fontFamilyName]) { |
if (pageViews[currCoreTextView].selectedFrame == NSUIntegerMax) { |
// No selected frame, apply font change to entire doc text |
NSRange range = NSMakeRange(0, [document.attributedString length]); |
[document setFontWithName:fontSelected.fontName range:range features:(viewOptions & CoreTextViewOptionsFeatureMask)]; |
[self relayoutDocFromPage:pageDisplayed]; |
} |
else { |
// Change font only for selected frame |
// (note that this only applies for "text" type frames) |
CoreTextViewPageInfo* page = pageViews[currCoreTextView].pageInfo; |
CoreTextViewFrameInfo* frame = [page.framesToDraw objectAtIndex:pageViews[currCoreTextView].selectedFrame]; |
if (frame.frameType == ASDFrameTypeTextFlow) { |
[document setFontWithName:fontSelected.fontName range:frame.stringRange features:(viewOptions & CoreTextViewOptionsFeatureMask)]; |
[self relayoutDocFromPage:pageDisplayed]; |
} |
else if (frame.frameType == ASDFrameTypeText) { |
ApplyFontNameToString(frame.value, fontSelected.fontName, frame.stringRange, (viewOptions & CoreTextViewOptionsFeatureMask)); |
[frame refreshTextFrame]; |
[self refreshPage]; |
} |
} |
|
} |
} |
|
- (void)optionsChange:(NSString*)optionName { |
// Don't change font feature settings until all secondary thread operations are done |
[operationQueue waitUntilAllOperationsAreFinished]; |
|
// Set up our font feature bitfield for given optionName |
|
ASDFeaturesBits optionBitSelected = 0; |
|
if (([optionName rangeOfString:ASD_SMALL_CAPITALS]).length > 0 ) { |
optionBitSelected = ASDFeaturesSmallCaps; |
} |
else if (([optionName rangeOfString:ASD_RARE_LIGATURES]).length > 0 ) { |
optionBitSelected = ASDFeaturesLigatures; |
} |
else if (([optionName rangeOfString:ASD_PROP_NUMBERS]).length > 0 ) { |
optionBitSelected = ASDFeaturesPropNumbers; |
} |
else if (([optionName rangeOfString:ASD_STYLISTIC_VARS]).length > 0 ) { |
optionBitSelected = ASDFeaturesStylisticVariants; |
} |
|
if (optionBitSelected && ((optionBitSelected & ASDFeaturesFeatureMask) != 0)) { |
// Font features are most of the time exclusive so for simplicity we allow one feature at a time |
viewOptions &= ~ASDFeaturesFeatureMask; |
viewOptions |= optionBitSelected; |
} |
else { |
// Passing @"" as the option is used to turn off all options |
if ([optionName length] == 0) { |
viewOptions = 0; |
} else { |
viewOptions ^= optionBitSelected; |
} |
} |
|
if (pageViews[currCoreTextView].selectedFrame == NSUIntegerMax) { |
// No selected frame, apply font features to all doc text |
NSRange range = NSMakeRange(0, [document.attributedString length]); |
[document setFontFeatures:(viewOptions & CoreTextViewOptionsFeatureMask) range:range]; |
[self relayoutDocFromPage:pageDisplayed]; |
} |
else { |
// Apply font features to selected frame |
// (note that this only applies for "text" type frames) |
CoreTextViewPageInfo* page = [pagesInfo objectForKey:[NSNumber numberWithInt:pageDisplayed]]; |
CoreTextViewFrameInfo* frame = [page.framesToDraw objectAtIndex:pageViews[currCoreTextView].selectedFrame]; |
if (frame.frameType == ASDFrameTypeTextFlow) { |
[document setFontFeatures:(viewOptions & CoreTextViewOptionsFeatureMask) range:frame.stringRange]; |
[self relayoutDocFromPage:pageDisplayed]; |
} |
else if (frame.frameType == ASDFrameTypeText) { |
ApplyFontFeaturesToString(frame.value, frame.stringRange, (viewOptions & CoreTextViewOptionsFeatureMask)); |
[frame refreshTextFrame]; |
[self refreshPage]; |
} |
} |
} |
|
|
|
@end |
|
|
#pragma mark - |
#pragma mark CoreTextViewDraw definition and implementation |
|
// CoreTextViewDraw is our UIView subclass that handles drawing |
@interface CoreTextViewDraw : UIView { |
CoreTextView* target; |
} |
|
- (CoreTextViewDraw*)initWithView:(CoreTextView*)view; |
- (void)drawLayer:(CALayer *)theLayer inContext:(CGContextRef)context; |
|
@end |
|
@implementation CoreTextViewDraw |
|
- (CoreTextViewDraw*)initWithView:(CoreTextView*)view { |
self = [super init]; |
if ( self != nil ) |
{ |
target = view; // do not retain the view object as the CoreTextViewDraw object is owned by the view itself |
} |
return self; |
} |
|
- (void)dealloc { |
[super dealloc]; |
} |
|
|
- (void)drawLayer:(CALayer *)theLayer inContext:(CGContextRef)context |
{ |
// our layer does the drawing |
[target drawIntoLayer:theLayer inContext:context]; |
} |
|
@end |
|
#pragma mark - |
#pragma mark CoreTextView implementation |
|
@implementation CoreTextView |
|
@synthesize selectedFrame; |
@synthesize pageInfo; |
@synthesize scrollView; |
@synthesize caLayerDrawDelegate; |
@synthesize layoutOnlyOnDraw; |
|
- (NSString*)description { |
return [NSString stringWithFormat:@"<CoreTextView %p> selectedFrame: %u pageInfo: %@ Delegate %p: layoutOnDraw: %@", |
self, selectedFrame, pageInfo, caLayerDrawDelegate, layoutOnlyOnDraw ? @"YES" : @"NO"]; |
} |
|
|
// Helper method to reset view cached contents |
- (void)reset { |
[pageInfo release]; |
pageInfo = nil; |
|
if (caLayerDrawDelegate == nil) { |
caLayerDrawDelegate = [[CoreTextViewDraw alloc] initWithView:self]; |
} |
|
// clean out the accessible elements |
[_accessibleElements removeAllObjects]; |
|
// No selected frame in our view |
selectedFrame = NSUIntegerMax; |
} |
|
|
- (id)initWithScrollView:(CoreTextScrollView*)theScrollView { |
|
self = [super initWithFrame:theScrollView.frame]; |
if ( self != nil ) |
{ |
scrollView = [theScrollView retain]; |
caLayerDrawDelegate = [[CoreTextViewDraw alloc] initWithView:self]; |
// No selected frame in our view |
selectedFrame = NSUIntegerMax; |
pageInfo = nil; |
layoutOnlyOnDraw = NO; |
} |
|
return self; |
} |
|
|
-(id)initWithCoder:(NSCoder *)aDecoder |
{ |
self = [super initWithCoder:aDecoder]; |
if(self != nil) |
{ |
scrollView = nil; |
pageInfo = nil; |
caLayerDrawDelegate = nil; |
selectedFrame = NSUIntegerMax; |
layoutOnlyOnDraw = YES; |
} |
return self; |
} |
|
|
- (void)awakeFromNib |
{ |
CALayer *layer = [self layer]; |
|
scrollView = nil; |
[self reset]; |
|
// clear the view's background color so that our background |
// fits within the rounded border |
CGColorRef backgroundColor = [self.backgroundColor CGColor]; |
self.backgroundColor = [UIColor clearColor]; |
layer.backgroundColor = backgroundColor; |
|
[self setNeedsDisplay]; |
} |
|
|
- (void)dealloc { |
[scrollView release]; |
[pageInfo release]; |
[caLayerDrawDelegate release]; |
|
[super dealloc]; |
} |
|
|
#pragma mark - |
#pragma mark Touch handling |
|
|
// Handles the start of a touch |
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event |
{ |
// For this sample, a double-tap selects a frame in the current view, if applicable |
NSUInteger numTaps = [[touches anyObject] tapCount]; |
if(numTaps >= 2) { |
[self selectFrameAtPosition:[[touches anyObject] locationInView:self]]; |
} |
|
[super touchesBegan:touches withEvent:event]; |
} |
|
|
// Marks frame closest to given position as selected |
- (void)selectFrameAtPosition:(CGPoint)position { |
NSUInteger index = 0; |
|
CGRect layoutBounds = [self bounds]; |
// Adjust position for CT style flipped in Y |
position.y = (layoutBounds.size.height - position.y); |
|
// Walk through frames for current view, find closest frame |
for (CoreTextViewFrameInfo* frameInfo in [self framesToDrawForPage]) { |
CGRect bounds = CGPathGetBoundingBox([frameInfo path]); |
if (CGRectContainsPoint(bounds, position)) { |
|
if (index != selectedFrame) { |
selectedFrame = index; |
[scrollView refreshPage]; |
} |
else if (selectedFrame != NSUIntegerMax) { |
selectedFrame = NSUIntegerMax; |
[scrollView refreshPage]; |
} |
break; |
} |
index += 1; |
} |
} |
|
|
#pragma mark - |
#pragma mark Accessibility |
|
|
- (CGRect)accessibilityBoundingBoxForRange:(NSRange)range forFrame:(CoreTextViewFrameInfo *)frameInfo withContext:(CGContextRef)context |
{ |
// first, we need the actual frame and the lines that are in the frame |
CTFrameRef frame = [frameInfo frame]; |
NSArray* lines = (NSArray*)CTFrameGetLines(frame); |
CFRange stringRange = CFRangeMake(range.location, range.length); |
CGRect returnValue = CGRectNull; |
CFIndex lineIdx; |
|
// iterate over each line in the frame |
for ( lineIdx = 0; lineIdx < [lines count]; lineIdx++ ) |
{ |
CTLineRef line = (CTLineRef)[lines objectAtIndex:lineIdx]; |
|
// get the full line range, and the offset for our string range |
CFRange lineRange = CTLineGetStringRange(line); |
|
// compensate for the string offseter, and compute where our range will end |
lineRange.location += frameInfo.stringOffsetForSetter; |
CFIndex lineEnd = (lineRange.location + lineRange.length); |
|
// if the range of the request string is within this line ... |
if ( stringRange.location >= lineRange.location && stringRange.location < lineEnd ) |
{ |
// compute which portion of this line we want to show |
CFRange lineHighlightRange = CFRangeMake(stringRange.location - frameInfo.stringOffsetForSetter, stringRange.length); |
|
// don't go over the end of this line |
if ( stringRange.location + stringRange.length >= lineEnd ) |
{ |
lineHighlightRange.length = lineEnd - stringRange.location; |
} |
|
CGPoint lineOrigin; |
CTFrameGetLineOrigins( frame, CFRangeMake(lineIdx, 1), &lineOrigin); |
|
CGFloat offsetInLine = CTLineGetOffsetForStringIndex(line, lineHighlightRange.location, NULL); |
|
NSRange selRange = NSMakeRange(lineHighlightRange.location+frameInfo.stringOffsetForSetter, lineHighlightRange.length); |
NSAttributedString* selectionStr = [[scrollView document].attributedString attributedSubstringFromRange:selRange]; |
|
CGFloat ascent, descent, leading; |
CGRect frameRect = CGPathGetBoundingBox( CTFrameGetPath(frame) ); |
|
// Create a line with just our substring so that we can get the bounds |
CGRect selStringBounds; |
CTLineRef selStringLineRef = AUTO_RELEASED_CTREF(CTLineCreateWithAttributedString((CFAttributedStringRef)selectionStr)); |
selStringBounds.size.width = CTLineGetTypographicBounds(selStringLineRef, NULL, NULL, NULL); |
CTLineGetTypographicBounds(line, &ascent, &descent, &leading); |
|
selStringBounds.origin.x = (frameRect.origin.x + offsetInLine + lineOrigin.x); |
selStringBounds.origin.y = (lineOrigin.y + frameRect.origin.y) - (descent + leading); |
selStringBounds.size.height = ascent + descent + leading; |
|
// add a bit of padding |
selStringBounds = CGRectInset(selStringBounds, -2.0, -2.0); |
|
// union this rect with our return value |
returnValue = CGRectUnion(returnValue, selStringBounds); |
|
// get out of here if there is nothing else to do |
if ( (stringRange.location + stringRange.length >= lineEnd) && |
((lineIdx+1) < [lines count]) ) |
{ |
stringRange.location += lineHighlightRange.length; |
stringRange.length -= lineHighlightRange.length; |
} |
else |
{ |
break; |
} |
} |
} |
|
returnValue.origin.y = [self bounds].size.height - (returnValue.origin.y + returnValue.size.height); |
|
return returnValue; |
} |
|
|
- (NSMutableArray *)accessibleElements |
{ |
if ( _accessibleElements != nil ) |
{ |
// if the count is 0 then the elements were cleared for some reason |
// so we want to rebuild the array |
if ( [_accessibleElements count] == 0 ) |
{ |
[self accessibilityUpdateElements]; |
} |
return _accessibleElements; |
} |
|
_accessibleElements = [[NSMutableArray alloc] init]; |
|
// if we just created the array then we need to populate it |
[self accessibilityUpdateElements]; |
|
return _accessibleElements; |
} |
|
|
- (void)accessibilityUpdateElements |
{ |
UIGraphicsBeginImageContext([self bounds].size); |
CGContextRef context = UIGraphicsGetCurrentContext(); |
|
[_accessibleElements removeAllObjects]; |
|
for ( CoreTextViewFrameInfo* frameInfo in [self framesToDrawForPage] ) |
{ |
// wrap a pool around this so we dont stick too much on the primary autorelease pool |
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; |
|
CGRect bounds = CGPathGetBoundingBox([frameInfo path]); |
bounds.origin.y = [self bounds].size.height - (bounds.origin.y + bounds.size.height); |
|
NSString *label = nil; |
|
// First, see if there is a pre-defined label |
label = [frameInfo accessibilityLabel]; |
|
if ( [label length] > 0 ) |
{ |
TextAccessibilityElement *accessibleElement = [[TextAccessibilityElement alloc] initWithAccessibilityContainer:self]; |
[accessibleElement setAccessibilityLabel:label]; |
[accessibleElement setContextBounds:bounds]; |
|
if ( [frameInfo frameType] == ASDFrameTypeTextFlow ) |
{ |
[accessibleElement setAccessibilityTraits:UIAccessibilityTraitImage]; |
} |
[_accessibleElements addObject:accessibleElement]; |
[accessibleElement release]; |
|
// we don't need to do anything else for this element, continue on |
continue; |
} |
|
id frameValue = [frameInfo value]; |
|
// Get a string for the frame value |
if ( [frameValue isKindOfClass:[AttributedStringDoc class]] ) |
{ |
NSRange range = [frameInfo stringRange]; |
label = [[[[scrollView document] attributedString] attributedSubstringFromRange:range] string]; |
} |
else if ( [frameValue isKindOfClass:[NSString class]] ) |
{ |
label = frameValue; |
} |
else if ( [frameValue isKindOfClass:[NSAttributedString class]] ) |
{ |
label = [frameValue string]; |
} |
|
// For text-based frames |
if ( [frameInfo frameType] == ASDFrameTypeTextFlow || |
[frameInfo frameType] == ASDFrameTypeText ) |
{ |
// Now we want to provide paragraph-by-paragraph navigation for a VoiceOver user |
// so we are going to break up label by paragraph and make each paragraph it's own |
// accessibility element |
NSRange range = NSMakeRange(0, 0); |
NSUInteger rangeOffset = [frameInfo stringRange].location; |
|
while ( NSMaxRange(range) < [label length] ) |
{ |
NSUInteger start, end, contentsEnd; |
|
[label getParagraphStart:&start end:&end contentsEnd:&contentsEnd forRange:NSMakeRange(NSMaxRange(range),0)]; |
|
// Grab the string for this paragraph and trim off any extra spaces |
range = NSMakeRange(start, contentsEnd-start); |
NSString *paragraphLabel = [label substringWithRange:range]; |
|
if ( [[paragraphLabel stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length] > 0 ) |
{ |
// finally, create the accessibility element for this paragraph |
TextAccessibilityElement *accessibleElement = [[TextAccessibilityElement alloc] initWithAccessibilityContainer:self]; |
|
[accessibleElement setAccessibilityLabel:paragraphLabel]; |
|
CGRect rect = [self accessibilityBoundingBoxForRange:NSMakeRange(range.location + rangeOffset, range.length) forFrame:frameInfo withContext:context]; |
|
// note that we are setting the "contextBounds" at this point. We can not |
// set the accessibilityFrame yet because we may not have a window at this point |
[accessibleElement setContextBounds:rect]; |
|
[_accessibleElements addObject:accessibleElement]; |
[accessibleElement release]; |
} |
|
range = NSMakeRange(start, end-start); |
} |
} |
else if ( [frameInfo frameType] == ASDFrameTypePicture ) |
{ |
// If the picture did not already have an accessibility label, lets just use the filename |
TextAccessibilityElement *accessibleElement = [[TextAccessibilityElement alloc] initWithAccessibilityContainer:self]; |
[accessibleElement setAccessibilityLabel:label]; |
[accessibleElement setAccessibilityTraits:UIAccessibilityTraitImage]; |
[_accessibleElements addObject:accessibleElement]; |
[accessibleElement release]; |
} |
|
[pool release]; |
} |
|
UIGraphicsEndImageContext(); |
} |
|
- (BOOL)isAccessibilityElement |
{ |
// CoreTextView contains accessibility elements but is not itself an accessibility element |
return NO; |
} |
|
- (NSInteger)accessibilityElementCount |
{ |
return [[self accessibleElements] count]; |
} |
|
- (id)accessibilityElementAtIndex:(NSInteger)index |
{ |
if ( index >= 0 && index < [self accessibilityElementCount] ) |
{ |
return [[self accessibleElements] objectAtIndex:index]; |
} |
else |
{ |
return nil; |
} |
} |
|
- (NSInteger)indexOfAccessibilityElement:(id)element |
{ |
return [[self accessibleElements] indexOfObject:element]; |
} |
|
|
#pragma mark - |
#pragma mark View Drawing |
|
|
// Get array of the CTFrames to draw for this view |
- (NSArray*)framesToDrawForPage |
{ |
NSMutableArray* framesToDraw = nil; |
AttributedStringDoc* theDocument = scrollView.document; |
NSMutableDictionary* thePagesInfo = scrollView.pagesInfo; |
NSInteger pageNumber = pageInfo.pageNumber; |
|
// Get the array cached from CoreTextViewPageInfo, if any |
if (thePagesInfo && [thePagesInfo count] > pageNumber) { |
framesToDraw = (NSMutableArray*)((CoreTextViewPageInfo*)[thePagesInfo objectForKey:[NSNumber numberWithInt:pageNumber]]).framesToDraw; |
if (framesToDraw) { |
return framesToDraw; |
} |
} |
|
framesToDraw = [NSMutableArray arrayWithCapacity:0]; |
CGRect layoutBounds; |
|
if (([[UIScreen mainScreen] bounds]).size.width < 500) { |
// running on an iPhone |
layoutBounds = CGRectMake(IPHONE_HORIZONTAL_MARGIN, IPHONE_VERTICAL_MARGIN, |
([self bounds]).size.width - (IPHONE_HORIZONTAL_MARGIN*2), |
([self bounds]).size.height - (IPHONE_VERTICAL_MARGIN*2)); |
} |
else { |
// running on an iPad |
layoutBounds = CGRectMake(IPAD_HORIZONTAL_MARGIN, IPAD_VERTICAL_MARGIN, |
([self bounds]).size.width - (IPAD_HORIZONTAL_MARGIN*2), |
([self bounds]).size.height - (IPAD_VERTICAL_MARGIN*2)); |
} |
|
// offset drawing area slightly if we intend to draw page numbers |
if (theDocument.showPageNumbers) { |
layoutBounds.origin.y += 20; |
} |
|
CFIndex startIndex = 0; |
CFIndex curIndex = 0; |
CoreTextViewFrameInfo* prevFreeFlowFrame = nil; |
|
if (pageNumber > 0) { |
CoreTextViewPageInfo* prevPageInfo = [thePagesInfo objectForKey:[NSNumber numberWithInt:pageNumber-1]]; |
curIndex = startIndex = prevPageInfo.rangeEnd; |
prevFreeFlowFrame = [prevPageInfo lastFreeFlowFrame]; |
} |
|
NSArray* pageFrames = [theDocument framesForPage:pageNumber]; |
if (pageFrames) { |
NSEnumerator* framesEnumerator = [pageFrames objectEnumerator]; |
id frameInfo; |
while ((frameInfo = [framesEnumerator nextObject]) != NULL) { |
|
// Update frame bounds |
CGMutablePathRef path = CGPathCreateMutable(); |
CGRect frameBounds = [theDocument boundsForFrame:frameInfo]; |
CoreTextViewFrameInfo* frameForDisplay = [[[CoreTextViewFrameInfo alloc] initWithFrameType:[theDocument typeForFrame:frameInfo] path:path] autorelease]; |
|
[frameForDisplay setAccessibilityLabel:[theDocument accessibilityLabelForFrame:frameInfo]]; |
frameBounds.origin.y = ((layoutBounds.size.height - frameBounds.origin.y) - frameBounds.size.height); |
CGPathAddRect(path, NULL, frameBounds); |
CGPathRelease(path); |
|
// Get attributed string data for frame, if applicable, and create framesetter |
id value = [theDocument objectForFrame:frameInfo]; |
if (value) { |
frameForDisplay.value = value; |
|
if ([theDocument typeForFrame:frameInfo] == ASDFrameTypeText) { |
CTFramesetterRef frameSetter = AUTO_RELEASED_CTREF(CTFramesetterCreateWithAttributedString((CFAttributedStringRef)value)); |
|
[frameForDisplay setSetter:frameSetter]; |
} |
else { |
// Not a text-based frame |
[frameForDisplay setSetter:NULL]; |
[frameForDisplay setFrame:NULL]; |
} |
|
} |
else if ([theDocument typeForFrame:frameInfo] == ASDFrameTypeTextFlow) { |
frameForDisplay.value = theDocument; |
curIndex = [frameForDisplay setFramesetterForStringOffset:curIndex previousFreeFlowFrame:prevFreeFlowFrame]; |
prevFreeFlowFrame = frameForDisplay; |
} |
|
[framesToDraw addObject:frameForDisplay]; |
} |
} |
else { |
|
// No previous frames, so generate our frame "layout" |
|
int column; |
NSUInteger columnCount = [theDocument columnsForPage:pageNumber]; |
CGRect* columnRects = (CGRect*)calloc(columnCount, sizeof(*columnRects)); |
|
// Start by setting the first column to cover the entire view. |
columnRects[0] = layoutBounds; |
|
// Divide the columns equally across the screen's width. |
CGFloat columnWidth = CGRectGetWidth(layoutBounds) / columnCount; |
for (column = 0; column < columnCount - 1; column++) { |
CGRectDivide(columnRects[column], &columnRects[column], &columnRects[column + 1], columnWidth, CGRectMinXEdge); |
} |
|
// Inset all columns by a few pixels of margin. |
for (column = 0; column < columnCount; column++) { |
columnRects[column] = CGRectInset(columnRects[column], 10.0, 10.0); |
} |
|
for (column = 0; column < columnCount; column++) { |
CGMutablePathRef path = CGPathCreateMutable(); |
CGPathAddRect(path, NULL, columnRects[column]); |
CoreTextViewFrameInfo* frameForDisplay = [[[CoreTextViewFrameInfo alloc] initWithFrameType:ASDFrameTypeTextFlow path:path] autorelease]; |
CGPathRelease(path); |
|
frameForDisplay.value = theDocument; |
curIndex = [frameForDisplay setFramesetterForStringOffset:curIndex previousFreeFlowFrame:prevFreeFlowFrame]; |
prevFreeFlowFrame = frameForDisplay; |
|
[framesToDraw addObject:frameForDisplay]; |
} |
|
free(columnRects); |
|
} |
|
CoreTextViewPageInfo* pageInfoEntry = [thePagesInfo objectForKey:[NSNumber numberWithInt:pageNumber]];; |
pageInfoEntry.framesToDraw = framesToDraw; |
pageInfoEntry.rangeStart = startIndex; |
pageInfoEntry.rangeEnd = curIndex; |
pageInfoEntry.lastFreeFlowFrame = prevFreeFlowFrame; |
|
|
return framesToDraw; |
} |
|
|
// Draws highlight for a specific line in a frame (line origin passed in) |
- (void) hilightRangeForLine:(CTLineRef)line withLineOrigin:(CGPoint)lineOrigin inFrame:(CTFrameRef)frame forSetterStringOffset:(NSUInteger)setterStringOffset forStringRange:(CFRange)stringRange inContext:(CGContextRef)context { |
CGFloat offsetInLine = CTLineGetOffsetForStringIndex(line, stringRange.location, NULL); |
|
// Get the string range (including offset) |
NSRange selRange = {stringRange.location+setterStringOffset, stringRange.length }; |
NSAttributedString* selectionStr = [scrollView.document.attributedString attributedSubstringFromRange:selRange]; |
|
CGFloat ascent, descent, leading; |
CGRect frameRect = CGPathGetBoundingBox( CTFrameGetPath(frame) ); |
|
CGRect selStringBounds; |
CTLineRef selStringLineRef = AUTO_RELEASED_CTREF(CTLineCreateWithAttributedString((CFAttributedStringRef)selectionStr)); |
selStringBounds.size.width = CTLineGetTypographicBounds(selStringLineRef, NULL, NULL, NULL); |
CTLineGetTypographicBounds(line, &ascent, &descent, &leading); |
|
// Determine line bounds given frame and line origin and typographic bounds |
selStringBounds.origin.x = (frameRect.origin.x + offsetInLine + lineOrigin.x); |
selStringBounds.origin.y = (lineOrigin.y + frameRect.origin.y) - (descent + leading); |
selStringBounds.size.height = ascent + descent + leading; |
|
CGContextSetStrokeColorWithColor(context, [UIColor blueColor].CGColor); |
|
//Set the width of the pen mark |
CGContextSetLineWidth(context, 1.0); |
CGContextStrokeRect(context, selStringBounds); |
} |
|
|
// NOTE: hilightSelectedRangesForFrame does not work for Left To Right text or when a word/range splits between two frames |
- (void) hilightSelectedRangesForFrame:(CoreTextViewFrameInfo*)frameInfo inContext:(CGContextRef)context { |
NSArray* selectedRanges = scrollView.selectionRanges; |
|
CTFrameRef frame = [frameInfo frame]; |
CFRange frameRange = { frameInfo.stringRange.location, frameInfo.stringRange.length }; |
|
// Get and draw highlight for each line in selectedRanges |
for (NSArray* rangeArray in selectedRanges) { |
NSUInteger start = [(NSNumber*)[rangeArray objectAtIndex:0] unsignedIntValue]; |
if (start >= frameRange.location && start < (frameRange.location + frameRange.length)) { |
CFRange stringRange = CFRangeMake(start, [(NSNumber*)[rangeArray objectAtIndex:1] unsignedIntValue]); |
NSArray* lines = (NSArray*)CTFrameGetLines(frame); |
CFIndex lineIdx; |
for (lineIdx=0; lineIdx<[lines count]; lineIdx++) { |
CTLineRef line = (CTLineRef)[lines objectAtIndex:lineIdx]; |
CFRange lineRange = CTLineGetStringRange(line); |
lineRange.location += frameInfo.stringOffsetForSetter; |
CFIndex lineEnd = (lineRange.location + lineRange.length); |
if (stringRange.location >= lineRange.location && stringRange.location < lineEnd) { |
// found line in selectedRanges |
CFRange lineHighlightRange = CFRangeMake(stringRange.location - frameInfo.stringOffsetForSetter, stringRange.length); |
if (stringRange.location + stringRange.length >= lineEnd) { |
lineHighlightRange.length = lineEnd - stringRange.location; |
} |
|
CGPoint lineOrigin; |
CTFrameGetLineOrigins( frame, CFRangeMake(lineIdx, 1), &lineOrigin); |
[self hilightRangeForLine:line withLineOrigin:lineOrigin inFrame:frame forSetterStringOffset:frameInfo.stringOffsetForSetter forStringRange:(CFRange)lineHighlightRange inContext:context]; |
|
if ((stringRange.location + stringRange.length >= lineEnd) && ((lineIdx+1) < [lines count])) { |
stringRange.location += lineHighlightRange.length; |
stringRange.length -= lineHighlightRange.length; |
} |
else { |
break; |
} |
|
} |
} |
} |
} |
} |
|
|
- (void)drawIntoLayer:(CALayer *)theLayer inContext:(CGContextRef)context; |
{ |
NSInteger pageBeingDrawn = pageInfo.pageNumber; |
|
CGColorRef backgroundColor = nil; |
NSArray* pageFrames = [scrollView.document framesForPage:pageBeingDrawn]; |
if (pageFrames) { |
backgroundColor = [scrollView.document copyColorForPage:pageBeingDrawn]; |
} |
else { |
// Default to white background |
backgroundColor = [[UIColor whiteColor] CGColor]; |
CFRetain(backgroundColor); |
} |
theLayer.backgroundColor = backgroundColor; |
CGColorRelease(backgroundColor); |
|
CGContextSetTextMatrix(context, CGAffineTransformIdentity); |
|
// Set the usual "flipped" Core Text draw matrix |
CGContextTranslateCTM(context, 0, ([self bounds]).size.height ); |
CGContextScaleCTM(context, 1.0, -1.0); |
|
static int layoutCount = 0; |
NSArray* framesToDraw = pageInfo.framesToDraw; |
if (framesToDraw == nil) { |
framesToDraw = [self framesToDrawForPage]; |
layoutCount++; |
} |
NSEnumerator* framesEnumerator = [framesToDraw objectEnumerator]; |
CoreTextViewFrameInfo* frameInfo; |
|
// Draw each frame |
NSUInteger frameIndex = 0; |
while ((frameInfo = [framesEnumerator nextObject]) != NULL) { |
CGPathRef path = [frameInfo path]; |
ASDFrameType type = frameInfo.frameType; |
|
if ((type == ASDFrameTypeText) || (type == ASDFrameTypeTextFlow)) { |
CTFrameRef frame = [frameInfo frame]; |
if (frame == NULL) { |
frame = AUTO_RELEASED_CTREF(CTFramesetterCreateFrame([frameInfo setter], CFRangeMake(0, 0), path, NULL)); |
[frameInfo setFrame:frame]; |
} |
if (!layoutOnlyOnDraw) { |
CTFrameDraw(frame, context); |
|
// After drawing frame, draw selected frame highlight rects |
[self hilightSelectedRangesForFrame:frameInfo inContext:context]; |
} |
} |
else if (type == ASDFrameTypePicture && !layoutOnlyOnDraw) { |
// This document 'frame' is an image, so draw it using CGImage |
CGRect rect = CGPathGetBoundingBox(path); |
NSString* filePath = frameInfo.value; |
CGDataProviderRef pngDP = CGDataProviderCreateWithFilename([filePath fileSystemRepresentation]); |
if (pngDP) { |
CGImageRef img = CGImageCreateWithPNGDataProvider(pngDP, NULL, true, kCGRenderingIntentPerceptual); // true for interpolate, false for not-interpolate |
if (img) { |
CGContextDrawImage(context, rect, img); |
CGImageRelease(img); |
} |
CGDataProviderRelease(pngDP); |
} |
} |
|
// Draw user-selected frame rect, if any |
if (frameIndex == selectedFrame) { |
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor); |
|
// Set the width of the pen mark |
CGContextSetLineWidth(context, 2.0); |
CGContextStrokeRect(context, CGPathGetBoundingBox(path)); |
} |
|
frameIndex += 1; |
} |
|
#if DEBUG_LAYOUT_DRAW_COUNTS |
if (TRUE) |
#else |
if (scrollView.document.showPageNumbers && !layoutOnlyOnDraw) |
#endif |
{ |
// Draw the page number (and debug draw counts) |
CTFontRef sysUIFont = AUTO_RELEASED_CTREF(CTFontCreateUIFontForLanguage(kCTFontSystemFontType, 12.0, NULL)); |
if (sysUIFont) { |
|
NSDictionary* attributes = [NSDictionary dictionaryWithObjectsAndKeys:(id)sysUIFont, (NSString*)kCTFontAttributeName, nil]; |
NSAttributedString* myPageNumberString = nil; |
NSNumberFormatter * formatter = [[[NSNumberFormatter alloc] init] autorelease]; |
#if DEBUG_LAYOUT_DRAW_COUNTS |
static int drawCount = 0; |
drawCount++; |
NSString* pageNumber = [NSString stringWithFormat:@"%@ [L%@/D%@]", [formatter stringFromNumber: [NSNumber numberWithInt: pageBeingDrawn+1]], [formatter stringFromNumber: [NSNumber numberWithInt: layoutCount]], [formatter stringFromNumber: [NSNumber numberWithInt: drawCount]], nil]; |
#else |
NSString* pageNumber = [NSString stringWithFormat:@"%@", [formatter stringFromNumber: [NSNumber numberWithInt: pageBeingDrawn+1]]]; |
#endif |
if ( [pageNumber length] > 0 ) { |
myPageNumberString = [[[NSAttributedString alloc] initWithString:[pageNumber length] > 0 ? pageNumber : @"" attributes:attributes] autorelease]; |
|
CTLineRef ctLine = AUTO_RELEASED_CTREF(CTLineCreateWithAttributedString((CFAttributedStringRef)myPageNumberString)); |
if ( ctLine != nil ) |
{ |
CGContextSetTextPosition(context, theLayer.bounds.size.width/2 - CTLineGetTypographicBounds(ctLine, NULL, NULL, NULL)/2, 20); |
CTLineDraw(ctLine, context); |
} |
} |
} |
} |
|
pageInfo.needsRedrawOnLoad = !layoutOnlyOnDraw; |
pageInfo.needsReLayout = NO; |
} |
|
|
@end |
|
|
#pragma mark - |
#pragma mark AsyncLayerOperation implementation |
|
|
@implementation AsyncLayerOperation |
|
|
-(id)init |
{ |
// Can only create instance with layer |
[self release]; |
[NSException raise:NSInternalInconsistencyException format:@"%@: must be initialized with a layer (use -initWithLayer:)", NSStringFromClass([self class])]; |
return nil; |
} |
|
|
-(id)initWithCoreTextScrollView:(CoreTextScrollView*)scrollView forNextPageToLoad:(NSInteger)nextPageToLoad { |
if(scrollView != nil) |
{ |
self = [super init]; |
if(self != nil) |
{ |
_scrollView = [scrollView retain]; |
_nextPageToLoad = nextPageToLoad; |
} |
} |
else |
{ |
[self release]; |
[NSException raise:NSInvalidArgumentException format:@"%@: scroll view must not be nil", NSStringFromClass([self class])]; |
} |
return self; |
} |
|
|
+(id)operationWithCoreTextScrollView:(CoreTextScrollView*)scrollView forNextPageToLoad:(NSInteger)nextPageToLoad { |
return [[[self alloc] initWithCoreTextScrollView:scrollView forNextPageToLoad:nextPageToLoad] autorelease]; |
} |
|
|
-(void)dealloc { |
[_scrollView release]; |
[super dealloc]; |
} |
|
|
// Our NSOperation main function |
-(void)main { |
if (_nextPageToLoad < 0) { |
NSLog(@"Do we not have any content to display?"); |
return; |
} |
|
NSInteger currentPage = _nextPageToLoad; |
NSUInteger startPosition = ((CoreTextViewPageInfo*)[_scrollView.pagesInfo objectForKey:[NSNumber numberWithInt:currentPage++]]).rangeEnd; |
|
AttributedStringDoc* doc = _scrollView.document; |
if ( ( (startPosition < (doc.attributedString).length) || ([doc framesForPage:currentPage] != nil)) ) { |
|
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; |
CoreTextView* ctView = [_scrollView loadPage:currentPage]; |
if (ctView) { |
CALayer* theLayer = ctView.pageInfo.pageLayer; |
[CATransaction begin]; |
ctView.layoutOnlyOnDraw = YES; |
[theLayer display]; |
ctView.layoutOnlyOnDraw = NO; |
[CATransaction commit]; |
[ctView.pageInfo.pageLayer setDelegate:nil]; // set layer delegate to nil otherwise when ctView gets released on the autorelease pool, it will try to |
// message the delegate to deallocate but the delegate is already gone |
ctView.pageInfo.pageLayer = nil; // do not cache the layer - this will also force a redraw when page is displayed |
|
[_scrollView addPageToScrollView]; // the page can now be added to the scrollView as it has been drawn |
[_scrollView loadAsyncPageAfter:currentPage]; |
} |
[pool release]; |
} |
|
} |
|
@end |