MyDocument_Pasteboard.m
/*  | 
File: MyDocument_Pasteboard.m  | 
Abstract: This category reads and writes data from the pasteboard to support copy/paste and dragging.  | 
Version: 1.2  | 
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) 2012 Apple Inc. All Rights Reserved.  | 
*/  | 
#import "MyDocument_Pasteboard.h"  | 
#import "Transaction.h"  | 
@implementation MyDocument(Pasteboard)  | 
/* Formatting methods in support of writing to the pasteboard */  | 
- (NSString *)stringFromTransactions:(NSArray *)transactions { | 
// When we are writing out NSStringPboardType, we create a string with one line per transaction and tabs between items in the transaction  | 
NSMutableString *result = [NSMutableString string];  | 
NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];  | 
[dateFormatter setDateStyle:NSDateFormatterShortStyle];  | 
    for (Transaction *transaction in transactions) { | 
NSString *descriptionString = transaction.descriptionString, *type = transaction.type, *accountType = transaction.accountType;  | 
[result appendFormat:@"%@\t%.2f\t%@\t%@\t%@\n", [dateFormatter stringFromDate:transaction.date], transaction.amount, (descriptionString ? descriptionString : @""), (type ? type : @""), (accountType ? accountType : @"")];  | 
}  | 
return result;  | 
}  | 
- (void)addCell:(NSString *)contents table:(NSTextTable *)table row:(NSInteger)row column:(NSInteger)col alignment:(NSTextAlignment)alignment toText:(NSMutableAttributedString *)text { | 
// This is an auxiliary method to append a new cell to the attributed string we are creating in the following method  | 
NSUInteger textLength = [text length];  | 
NSTextTableBlock *block = [[NSTextTableBlock alloc] initWithTable:table startingRow:row rowSpan:1 startingColumn:col columnSpan:1];  | 
NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];  | 
[block setWidth:1.0f type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockBorder];  | 
[block setWidth:5.0f type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockPadding edge:NSMinXEdge];  | 
[block setWidth:5.0f type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockPadding edge:NSMaxXEdge];  | 
[block setVerticalAlignment:NSTextBlockMiddleAlignment];  | 
[block setBorderColor:[NSColor colorWithCalibratedWhite:0.75f alpha:1.0f]];  | 
[style setTextBlocks:[NSArray arrayWithObject:block]];  | 
[style setAlignment:alignment];  | 
[text replaceCharactersInRange:NSMakeRange(textLength, 0) withString:[NSString stringWithFormat:@"%@\n", (contents ? contents : @"")]];  | 
[text addAttribute:NSParagraphStyleAttributeName value:style range:NSMakeRange(textLength, [text length] - textLength)];  | 
[style release];  | 
[block release];  | 
}  | 
- (NSAttributedString *)attributedStringFromTransactions:(NSArray *)transactions { | 
// When we are writing out NSRTFPboardType, we create a table with one row per transaction and one cell per item in the transaction  | 
NSMutableAttributedString *result = [[[NSMutableAttributedString alloc] initWithString:@"\n"] autorelease];  | 
NSAttributedString *returnString = [[[NSAttributedString alloc] initWithString:@"\n"] autorelease];  | 
NSUInteger i, count = [transactions count];  | 
Transaction *transaction;  | 
NSTextTable *table = [[[NSTextTable alloc] init] autorelease];  | 
NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];  | 
[dateFormatter setDateStyle:NSDateFormatterShortStyle];  | 
[table setNumberOfColumns:5];  | 
[table setLayoutAlgorithm:NSTextTableAutomaticLayoutAlgorithm];  | 
[table setCollapsesBorders:YES];  | 
[table setHidesEmptyCells:NO];  | 
    for (i = 0; i < count; i++) { | 
transaction = [transactions objectAtIndex:i];  | 
[self addCell:[dateFormatter stringFromDate:transaction.date] table:table row:i column:0 alignment:NSLeftTextAlignment toText:result];  | 
[self addCell:[NSString stringWithFormat:@"%.2f", transaction.amount] table:table row:i column:1 alignment:NSRightTextAlignment toText:result];  | 
[self addCell:transaction.descriptionString table:table row:i column:2 alignment:NSLeftTextAlignment toText:result];  | 
[self addCell:transaction.type table:table row:i column:3 alignment:NSLeftTextAlignment toText:result];  | 
[self addCell:transaction.accountType table:table row:i column:4 alignment:NSLeftTextAlignment toText:result];  | 
}  | 
[result appendAttributedString:returnString];  | 
return result;  | 
}  | 
/* Methods for writing to the pasteboard */  | 
- (NSArray *)writablePasteboardTypes { | 
return [NSArray arrayWithObjects:kSpendDocumentType, NSFilesPromisePboardType, NSFilenamesPboardType, NSRTFPboardType, NSStringPboardType, nil];  | 
}  | 
- (BOOL)writeSelectionToPasteboard:(NSPasteboard *)pboard types:(NSArray *)types { | 
BOOL result = NO;  | 
NSMutableArray *typesToDeclare = [NSMutableArray array];  | 
NSArray *writableTypes = [self writablePasteboardTypes];  | 
NSString *type;  | 
    for (type in writableTypes) { | 
if ([types containsObject:type]) [typesToDeclare addObject:type];  | 
}  | 
    if ([typesToDeclare count] > 0) { | 
[pboard declareTypes:typesToDeclare owner:self];  | 
        for (type in typesToDeclare) { | 
if ([self writeSelectionToPasteboard:pboard type:type]) result = YES;  | 
}  | 
}  | 
return result;  | 
}  | 
- (BOOL)writeSelectionToPasteboard:(NSPasteboard *)pboard type:(NSString *)type { | 
BOOL result = NO;  | 
NSArray *transactions = [_transactionController selectedObjects];  | 
    if (transactions && [transactions count] > 0) { | 
        if ([type isEqualToString:kSpendDocumentType]) { | 
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:transactions];  | 
if (data && [data length] > 0) result = [pboard setData:data forType:kSpendDocumentType];  | 
        } else if ([type isEqualToString:NSFilesPromisePboardType]) { | 
result = [pboard setPropertyList:[NSArray arrayWithObject:kSpendExtension] forType:NSFilesPromisePboardType];  | 
        } else if ([type isEqualToString:NSFilenamesPboardType]) { | 
// we do not have a file already in existence, so we wish to handle this type lazily to delay file creation until actually requested  | 
result = YES;  | 
        } else if ([type isEqualToString:NSRTFPboardType]) { | 
NSAttributedString *attrStr = [self attributedStringFromTransactions:transactions];  | 
if (attrStr && [attrStr length] > 0) result = [pboard setData:[attrStr RTFFromRange:NSMakeRange(0, [attrStr length]) documentAttributes:nil] forType:NSRTFPboardType];  | 
        } else if ([type isEqualToString:NSStringPboardType]) { | 
NSString *string = [self stringFromTransactions:transactions];  | 
if (string && [string length] > 0) result = [pboard setString:string forType:NSStringPboardType];  | 
}  | 
}  | 
return result;  | 
}  | 
- (NSURL *)writeSelectionToDestination:(NSURL *)destination { | 
// This is the method that we call when our file promise is being redeemed  | 
// We write out a file to the directory specified, and return the file's URL (or nil in case of failure)  | 
NSArray *transactions = [_transactionController selectedObjects];  | 
NSIndexSet *indexSet = [_transactionController selectionIndexes];  | 
NSString *name = [NSString stringWithFormat:@"iSpend[%ld..%ld].%@", (long)[indexSet firstIndex], (long)[indexSet lastIndex], kSpendExtension], *path = [[destination path] stringByAppendingPathComponent:name];  | 
NSURL *fileURL = [NSURL fileURLWithPath:path];  | 
NSDocumentController *controller = [NSDocumentController sharedDocumentController];  | 
NSError *error = nil;  | 
MyDocument *newDocument = [controller openUntitledDocumentAndDisplay:NO error:&error];  | 
BOOL succeeded = NO;  | 
    if (newDocument) { | 
[newDocument setTransactions:[NSArray arrayWithArray:transactions]];  | 
if ([newDocument writeToURL:fileURL ofType:kSpendDocumentType error:&error]) succeeded = YES;  | 
[controller removeDocument:newDocument];  | 
}  | 
return (succeeded ? fileURL : nil);  | 
}  | 
- (void)pasteboard:(NSPasteboard *)pboard provideDataForType:(NSString *)type { | 
// We expect that -tableView:namesOfPromisedFilesDroppedAtDestination:forDraggedRowsWithIndexes: will usually be called instead,  | 
// but we implement this method to create a file if NSFilenamesPboardType is ever requested directly  | 
    if ([type isEqualToString:NSFilenamesPboardType]) { | 
NSURL *fileURL = [self writeSelectionToDestination:[NSURL fileURLWithPath:NSTemporaryDirectory()]];  | 
if (fileURL) [pboard setPropertyList:[NSArray arrayWithObject:[fileURL path]] forType:NSFilenamesPboardType];  | 
}  | 
}  | 
- (void)copy:(id)sender { | 
[self writeSelectionToPasteboard:[NSPasteboard generalPasteboard] types:[self writablePasteboardTypes]];  | 
}  | 
/* Parsing methods in support of reading from the pasteboard */  | 
- (BOOL)addTransactionsFromString:(NSString *)string { | 
// If we are reading in NSStringPboardType, we parse the string into lines and create a transaction per line  | 
BOOL result = NO;  | 
NSUInteger length = [string length], location = 0;  | 
NSRange lineRange;  | 
Transaction *transaction;  | 
    while (location < length) { | 
lineRange = [string lineRangeForRange:NSMakeRange(location, 1)];  | 
transaction = [[Transaction alloc] initWithString:[string substringWithRange:lineRange]];  | 
        if (transaction) { | 
[_transactionController addObject:transaction];  | 
[transaction release];  | 
result = YES;  | 
}  | 
location = NSMaxRange(lineRange);  | 
}  | 
return result;  | 
}  | 
- (BOOL)addTransactionsFromAttributedString:(NSAttributedString *)attributedString { | 
// If we are reading in NSRTFPboardType, we parse the string into lines and create a transaction per line,  | 
// unless there is a table present, in which case we parse the string into table rows and create a transaction per row  | 
BOOL result = NO;  | 
NSString *string = [attributedString string];  | 
NSUInteger length = [string length], location = 0, rowLocation;  | 
NSRange lineRange, tableRange, blockRange;  | 
NSArray *textBlocks;  | 
NSTextTableBlock *block, *nextBlock;  | 
NSTextTable *table;  | 
Transaction *transaction;  | 
    while (location < length) { | 
lineRange = [string lineRangeForRange:NSMakeRange(location, 1)];  | 
textBlocks = [[attributedString attribute:NSParagraphStyleAttributeName atIndex:location effectiveRange:NULL] textBlocks];  | 
table = nil;  | 
        if (textBlocks && [textBlocks count] > 0) { | 
block = [textBlocks objectAtIndex:0];  | 
if ([block isKindOfClass:[NSTextTableBlock class]]) table = [block table];  | 
}  | 
        if (table) { | 
// If a table is present, parse it into rows  | 
tableRange = [attributedString rangeOfTextTable:table atIndex:location];  | 
rowLocation = location;  | 
            while (location < NSMaxRange(tableRange)) { | 
// Go through the table by cells, looking for row boundaries  | 
block = [[[attributedString attribute:NSParagraphStyleAttributeName atIndex:location effectiveRange:NULL] textBlocks] objectAtIndex:0];  | 
blockRange = [attributedString rangeOfTextBlock:block atIndex:location];  | 
nextBlock = (NSMaxRange(blockRange) < NSMaxRange(tableRange)) ? [[[attributedString attribute:NSParagraphStyleAttributeName atIndex:NSMaxRange(blockRange) effectiveRange:NULL] textBlocks] objectAtIndex:0] : nil;  | 
                if (!nextBlock || [nextBlock startingRow] != [block startingRow]) { | 
// This is the last cell in a row  | 
transaction = [[Transaction alloc] initWithString:[string substringWithRange:NSMakeRange(rowLocation, NSMaxRange(blockRange) - rowLocation)]];  | 
                    if (transaction) { | 
[_transactionController addObject:transaction];  | 
[transaction release];  | 
result = YES;  | 
}  | 
rowLocation = NSMaxRange(blockRange);  | 
}  | 
location = NSMaxRange(blockRange);  | 
}  | 
        } else { | 
// If no table is present, create a transaction per line as in the string case  | 
transaction = [[Transaction alloc] initWithString:[string substringWithRange:lineRange]];  | 
            if (transaction) { | 
[_transactionController addObject:transaction];  | 
[transaction release];  | 
result = YES;  | 
}  | 
location = NSMaxRange(lineRange);  | 
}  | 
}  | 
return result;  | 
}  | 
- (BOOL)addTransactionsFromPasteboardData:(NSData *)data { | 
// If we are reading in our custom pasteboard type, we use NSKeyedUnarchiver  | 
BOOL result = NO;  | 
NSArray *array = [NSKeyedUnarchiver unarchiveObjectWithData:data];  | 
    if (array && [array isKindOfClass:[NSArray class]]) { | 
[_transactionController addObjects:array];  | 
result = YES;  | 
}  | 
return result;  | 
}  | 
- (BOOL)addTransactionsFromFileData:(NSData *)data { | 
// If we are reading in our custom file type, we create a new document and extract its transactions  | 
BOOL result = NO;  | 
NSDocumentController *controller = [NSDocumentController sharedDocumentController];  | 
NSError *error = nil;  | 
MyDocument *newDocument = [controller openUntitledDocumentAndDisplay:NO error:&error];  | 
    if (newDocument) { | 
        if ([newDocument readFromData:data ofType:kSpendDocumentType error:&error]) { | 
[_transactionController addObjects:[newDocument transactions]];  | 
result = YES;  | 
}  | 
[controller removeDocument:newDocument];  | 
}  | 
return result;  | 
}  | 
/* Methods for reading from the pasteboard */  | 
- (NSArray *)readablePasteboardTypes { | 
return [NSArray arrayWithObjects:kSpendDocumentType, NSFilenamesPboardType, NSRTFPboardType, NSStringPboardType, nil];  | 
}  | 
- (BOOL)readSelectionFromPasteboard:(NSPasteboard *)pboard { | 
// We go through the available types in our preferred order, and return after the first one that succeeds  | 
BOOL result = NO;  | 
NSArray *availableTypes = [pboard types], *readableTypes = [self readablePasteboardTypes];  | 
NSEnumerator *enumerator = [readableTypes objectEnumerator];  | 
NSString *type;  | 
    while (!result && (type = [enumerator nextObject])) { | 
        if ([availableTypes containsObject:type]) { | 
result = [self readSelectionFromPasteboard:pboard type:type];  | 
}  | 
}  | 
return result;  | 
}  | 
- (BOOL)readSelectionFromPasteboard:(NSPasteboard *)pboard type:(NSString *)type { | 
BOOL result = NO;  | 
    if ([type isEqualToString:kSpendDocumentType]) { | 
NSData *data = [pboard dataForType:kSpendDocumentType];  | 
if (data && [data length] > 0) result = [self addTransactionsFromPasteboardData:data];  | 
    } else if ([type isEqualToString:NSFilenamesPboardType]) { | 
NSArray *files = [pboard propertyListForType:NSFilenamesPboardType];  | 
        for (NSString *filePath in files) {  | 
            if ([[filePath pathExtension] isEqualToString:kSpendExtension]) { | 
// This is a file of our custom type  | 
NSData *data = [NSData dataWithContentsOfFile:filePath];  | 
if (data && [data length] > 0 && [self addTransactionsFromFileData:data]) result = YES;  | 
            } else { | 
// Treat the file as a text file  | 
NSAttributedString *attrStr = [[[NSAttributedString alloc] initWithPath:filePath documentAttributes:nil] autorelease];  | 
if (attrStr && [attrStr length] > 0 && [self addTransactionsFromAttributedString:attrStr]) result = YES;  | 
}  | 
}  | 
    } else if ([type isEqualToString:NSRTFPboardType]) { | 
NSData *data = [pboard dataForType:NSRTFPboardType];  | 
NSAttributedString *attrStr = [[[NSAttributedString alloc] initWithRTF:data documentAttributes:NULL] autorelease];  | 
if (attrStr && [attrStr length] > 0) result = [self addTransactionsFromAttributedString:attrStr];  | 
    } else if ([type isEqualToString:NSStringPboardType]) { | 
NSString *string = [pboard stringForType:NSStringPboardType];  | 
if (string && [string length] > 0) result = [self addTransactionsFromString:string];  | 
}  | 
return result;  | 
}  | 
- (void)paste:(id)sender { | 
[self readSelectionFromPasteboard:[NSPasteboard generalPasteboard]];  | 
}  | 
/* Method for enabling services use */  | 
- (id)validRequestorForSendType:(NSString *)sendType returnType:(NSString *)returnType { | 
if ((!sendType || [[self writablePasteboardTypes] containsObject:sendType]) && (!returnType || [[self readablePasteboardTypes] containsObject:returnType]) && (!sendType || [[_transactionController selectedObjects] count] > 0)) return self;  | 
// We are not actually a subclass of NSResponder; if we were, we would pass this on to super.  | 
// In this particular application, we know that no responder above the document level handles copy/paste; if there were one, we would pass this on to it.  | 
return nil;  | 
}  | 
/* Methods for providing services */  | 
+ (void)importData:(NSPasteboard *)pboard userData:(NSString *)data error:(NSString **)error { | 
// -[NSWindowController currentDocument] works only when app is active, so we use this alternative means of finding the front document  | 
MyDocument *document = [[[NSApp makeWindowsPerform:@selector(windowController) inOrder:YES] windowController] document];  | 
if (document) [document readSelectionFromPasteboard:pboard];  | 
}  | 
+ (void)exportData:(NSPasteboard *)pboard userData:(NSString *)data error:(NSString **)error { | 
MyDocument *document = [[[NSApp makeWindowsPerform:@selector(windowController) inOrder:YES] windowController] document];  | 
if (document) [document writeSelectionToPasteboard:pboard types:[document writablePasteboardTypes]];  | 
}  | 
@end  | 
Copyright © 2012 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2012-06-07